diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..feed3f4a9 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# These are supported funding model platforms + +patreon: totaljs +open_collective: totalplatform +ko_fi: totaljs +liberapay: totaljs +buy_me_a_coffee: totaljs +custom: https://www.totaljs.com/support/ diff --git a/.gitignore b/.gitignore index 80c35ff5f..0b4524423 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ *.log .idea test/tmp/* -node_modules \ No newline at end of file +node_modules +test/test-framework-debug.js.json +test/test-framework-release.js.json \ No newline at end of file diff --git a/.npmignore b/.npmignore index 11d4be540..f823ea1f3 100644 --- a/.npmignore +++ b/.npmignore @@ -7,7 +7,10 @@ helpers/ test/ *.markdown *.md +.editorconfig +.gitingore .git* +*.yml *.DS_Store *.sublime-project *.sublime-workspace diff --git a/503.html b/503.html new file mode 100644 index 000000000..9b82dfe69 --- /dev/null +++ b/503.html @@ -0,0 +1,65 @@ + + + + @(Please wait) + + + + + + + + +
+
+
10
+
@(Please wait)
+
+ @(Application is starting …) +
+ + @{foreach m in model} + + + + + @{end} +
@{m.key}
+
+
+ + + + + \ No newline at end of file diff --git a/bin/totaljs b/bin/totaljs index cd8150b5c..8ce0e9ab4 100755 --- a/bin/totaljs +++ b/bin/totaljs @@ -6,12 +6,31 @@ var path = require('path'); var os = require('os'); var Utils = require('total.js/utils'); var Internal = require('total.js/internal'); -var $type = 1; +var $type = 0; var isDirectory = false; function display_help() { + console.log('--------------------------------------------------------'); + console.log('TEMPLATES'); + console.log('--------------------------------------------------------'); + console.log(''); + console.log('without arguments : creates emptyproject'); + console.log('flow : creates emptyproject-flow'); + console.log('dashboard : creates emptyproject-dashboard'); + console.log('flowboard : creates emptyproject-flowboard'); + console.log('spa : creates emptyproject-jcomponent'); + console.log('pwa : creates emptyproject-pwa'); + console.log('rest : creates emptyproject-restservice'); + console.log('cms : downloads Total.js CMS'); + console.log('eshop : downloads Total.js Eshop'); + console.log('superadmin : downloads Total.js SuperAdmin'); + console.log('openplatform : downloads Total.js OpenPlatform'); + console.log('helpdesk : downloads Total.js HelpDesk'); + console.log(''); + console.log('--------------------------------------------------------'); + console.log('TOOLS'); + console.log('--------------------------------------------------------'); console.log(''); - console.log('without arguments : creates empty-project'); console.log('-translate : creates a resource file with the localized text from views'); console.log('-translate "TEXT" : creates an identificator for the resource'); console.log('-translate filename : parses and creates a resource file from the text file'); @@ -21,7 +40,8 @@ function display_help() { console.log('-merge source target : merges first resource into the second "-merge source target"'); console.log('-clean source : cleans a resource file "-clean source"'); console.log('-minify filename : minifies .js, .css or .html file into filename.min.[extension]'); - console.log('-v or -version : Total.js version'); + console.log('-bundle filename : makes a bundle from the current directory'); + console.log('-package filename : makes a package from the current directory'); console.log('-install : run "totaljs -install help" to see what can be installed'); console.log('8000 : starts a server'); console.log(''); @@ -286,25 +306,20 @@ function clean_resource(content) { } function parse_csv(content) { + var output = {}; var max = 0; - - content.split('\n').forEach(function(line) { - line = line.trim(); - if (!line) - return; - var arr = line.split(';'); - if (arr.length <= 1) - return; - - var key = arr[0].trim().replace(/(^\")|(\"$)/g, ''); - var value = arr[1].trim().replace(/(^\")|(\"$)/g, ''); - if (!key || !value) - return; - - max = Math.max(key.length, max); - output[key] = value; - }); + var csv = content.parseCSV(';'); + + for (var i = 1; i < csv.length; i++) { + var line = csv[i]; + var key = line.a || ''; + var val = line.b || ''; + if (key) { + max = Math.max(key.length, max); + output[key] = val; + } + } var builder = []; max += 10; @@ -320,7 +335,7 @@ function main() { console.log(''); console.log('|==================================================|'); - console.log('| Total.js - Web framework for Node.js |'); + console.log('| Total.js - www.totaljs.com |'); console.log('| Version: v' + require('total.js').version_header.padRight(39) + '|'); console.log('|==================================================|'); console.log(''); @@ -337,11 +352,11 @@ function main() { var port = cmd.parseInt(); if (port) { - F.config['directory-temp'] = '~' + path.join(os.tmpdir(), 'totaljs' + dir.hash()); - F.config['directory-public'] = '~' + dir; - F.config['allow-compile-html'] = false; - F.config['allow-compile-script'] = false; - F.config['allow-compile-style'] = false; + CONF.directory_temp = '~' + path.join(os.tmpdir(), 'totaljs' + dir.hash()); + CONF.directory_public = '~' + dir; + CONF.allow_compile_html = false; + CONF.allow_compile_script = false; + CONF.allow_compile_style = false; F.accept('.less', 'text/less'); @@ -393,32 +408,32 @@ function main() { F.route('/proxy/', function() { var self = this; var method = self.req.method; - U.request(self.query.url, [self.req.method], method === 'POST' || method === 'PUT' || method === 'DELETE' ? self.body : null, (err, response, status, headers) => self.content(response, headers['content-type'])); + U.request(self.query.url, [self.req.method], method === 'POST' || method === 'PUT' || method === 'DELETE' ? self.body : null, (err, response, status, headers) => self.content(response, headers ? headers['content-type'] : 'text/plain')); }, ['get', 'post', 'put', 'delete'], 5120); return; } } - if (cmd === '-v' || cmd === '-version') + if (!$type && (cmd === '-v' || cmd === '-version')) return; - if (cmd === '-t' || cmd === '-translate') { + if (!$type && (cmd === '-t' || cmd === '-translate')) { $type = 4; continue; } - if (cmd === '-merge') { + if (!$type && cmd === '-merge') { merge(process.argv[i + 1] || '', process.argv[i + 2] || ''); return; } - if (cmd === '-translate-csv' || cmd === '-translatecsv' || cmd === '-c') { + if (!$type && (cmd === '-translate-csv' || cmd === '-translatecsv' || cmd === '-c')) { $type = 6; continue; } - if (cmd === '-csv') { + if (!$type && cmd === '-csv') { var tmp = process.argv[i + 1] || ''; var tt = path.join(path.dirname(tmp), path.basename(tmp, '.csv') + '.resource'); fs.writeFileSync(tt, '// Total.js resource file\n// Created: ' + new Date().format('yyyy-MM-dd HH:mm') + '\n' + parse_csv(fs.readFileSync(tmp).toString('utf8'))); @@ -428,45 +443,57 @@ function main() { console.log(''); console.log('output : ' + tt); console.log(''); - return; + continue; } - if (cmd === '-i' || cmd === '-install') { + if (!$type && (cmd === '-i' || cmd === '-install')) { - var libs = ['jc', 'jc.min', 'jcta', 'jcta.min', 'jctajr', 'jctajr.min', 'ta', 'jr', 'jr.jc']; + var libs = ['jc', 'jc.min', 'jcta', 'jcta.min', 'jctajr', 'jctajr.min', 'ta', 'jr', 'jr.jc', 'spa', 'spa.min']; var tmp = process.argv[i + 1] || ''; if (tmp === 'help') { - return console.log('Following libs can be installed: jc, jc.min, jcta.min, jctajr.min, ta, jr, jr.jc'); + return console.log('Following libs can be installed: jc, jc.min, jcta.min, jctajr.min, ta, jr, jr.jc, spa'); } - if (!tmp || libs.indexOf(tmp) < 0) + if (!tmp || libs.indexOf(tmp) < 0) return console.log('Unknown library: "' + tmp + '"'); - + console.log(''); console.log('Installing: ' + tmp); var url = ''; switch(tmp) { - case 'jc': - url = 'https://rawgit.com/totaljs/jComponent/master/jc.js'; + case 'jc': + url = 'https://rawgit.com/totaljs/components/master/0dependencies/jc.js'; + break; + case 'jc.min': + url = 'https://rawgit.com/totaljs/components/master/0dependencies/jc.min.js'; + break; + case 'jcta.min': + url = 'https://rawgit.com/totaljs/components/master/0dependencies/jcta.min.js'; + break; + case 'jctajr.min': + url = 'https://rawgit.com/totaljs/components/master/0dependencies/jctajr.min.js'; break; - case 'jc.min': - url = 'https://rawgit.com/totaljs/jComponent/master/jc.min.js'; + case 'spa': + case 'spa.min': + url = 'https://rawgit.com/totaljs/components/master/0dependencies/spa.min.js'; break; - case 'jcta.min': - url = 'https://rawgit.com/totaljs/jComponent/master/jcta.min.js'; + case 'spa@14': + case 'spa.min@14': + url = 'https://rawgit.com/totaljs/components/master/0dependencies/spa.min@14.js'; break; - case 'jctajr.min': - url = 'https://rawgit.com/totaljs/jComponent/master/jctajr.min.js'; + case 'spa@15': + case 'spa.min@15': + url = 'https://rawgit.com/totaljs/components/master/0dependencies/spa.min@15.js'; break; - case 'ta': + case 'ta': url = 'https://rawgit.com/totaljs/Tangular/master/Tangular.js'; break; - case 'jr': + case 'jr': url = 'https://rawgit.com/totaljs/jRouting/master/jrouting.js'; break; - case 'jr.jc': + case 'jr.jc': url = 'https://rawgit.com/totaljs/jRouting/master/jrouting.jcomponent.js'; break; } @@ -481,10 +508,10 @@ function main() { if (cmd === '-minify' || cmd === '-compress' || cmd === '-compile') { $type = 5; - continue; + break; } - if (cmd === '-clean') { + if (!$type && (cmd === '-clean')) { var tmp = process.argv[i + 1] || ''; var tt = path.join(path.dirname(tmp), path.basename(tmp, '.resource') + '-cleaned.txt'); fs.writeFileSync(tt, '// Total.js cleaned file\n// Created: ' + new Date().format('yyyy-MM-dd HH:mm') + '\n' + clean_resource(fs.readFileSync(tmp).toString('utf8'))); @@ -497,7 +524,77 @@ function main() { return; } - if (cmd === '-diff') { + if (!$type && (cmd === '-cms' || cmd === 'cms')) { + git(dir, 'cms'); + return; + } + + if (!$type && (cmd === '-eshop' || cmd === 'eshop')) { + git(dir, 'eshop'); + return; + } + + if (!$type && (cmd === '-superadmin' || cmd === 'superadmin')) { + git(dir, 'superadmin'); + return; + } + + if (!$type && (cmd === '-messenger' || cmd === 'messenger')) { + git(dir, 'messenger'); + return; + } + + if (!$type && (cmd === '-helpdesk' || cmd === 'helpdesk')) { + git(dir, 'helpdesk'); + return; + } + + if (!$type && (cmd === '-openplatform' || cmd === 'openplatform')) { + git(dir, 'openplatform'); + return; + } + + if (!$type && (cmd === '-flow' || cmd === 'flow')) { + git(dir, 'emptyproject-flow'); + return; + } + + if (!$type && (cmd === '-dashboard' || cmd === 'dashboard')) { + git(dir, 'emptyproject-dashboard'); + return; + } + + if (!$type && (cmd === '-flowboard' || cmd === 'flowboard')) { + git(dir, 'emptyproject-flowboard'); + return; + } + + if (!$type && (cmd === '-bundle' || cmd === 'bundle')) { + makebundle(dir, process.argv[i + 1] || ''); + return; + } + + if (!$type && (cmd === '-package' || cmd === 'package')) { + makepackage(dir, process.argv[i + 1] || ''); + return; + } + + if (!$type && (cmd === '-pwa' || cmd === 'pwa')) { + git(dir, 'emptyproject-pwa'); + return; + } + + if (!$type && (cmd === '-spa' || cmd === 'spa')) { + git(dir, 'emptyproject-jcomponent'); + return; + } + + if (!$type && (cmd === '-rest' || cmd === 'rest')) { + git(dir, 'emptyproject-restservice'); + return; + } + + if (!$type && cmd === '-diff') { diff(process.argv[i + 1] || '', process.argv[i + 2] || ''); return; } @@ -507,26 +604,28 @@ function main() { continue; } - if (cmd === '-m' || cmd === '-minimal' || cmd === '-minimum' || cmd === 'minimum') { + if (!$type && (cmd === '-m' || cmd === '-minimal' || cmd === '-minimum' || cmd === 'minimum')) { $type = 2; continue; } - if (cmd === '-n' || cmd === '-normal' || cmd === 'normal') { + if (!$type && (cmd === '-n' || cmd === '-normal' || cmd === 'normal')) { $type = 1; continue; } - if (cmd === '-h' || cmd === '-help' || cmd === '--help' || cmd === 'help') { + if (!$type && (cmd === '-h' || cmd === '-help' || cmd === '--help' || cmd === 'help')) { display_help(); return; } dir = arg; isDirectory = true; - break; } + if (!$type) + $type = 1; + if (dir === '.') dir = process.cwd(); @@ -582,6 +681,8 @@ function main() { var texts = {}; var max = 0; var count = 0; + var key; + var file; for (var i = 0, length = files.length; i < length; i++) { var filename = files[i]; @@ -600,22 +701,61 @@ function main() { continue; } - var key = 'T' + command.command.hash(); - var file = filename.substring(dir.length + 1); + key = 'T' + command.command.hash(); + file = filename.substring(dir.length + 1); texts[key] = command.command; if (resource[key]) { if (resource[key].indexOf(file) === -1) resource[key] += ', ' + file; - } - else + } else resource[key] = file; count++; max = Math.max(max, key.length); command = Internal.findLocalization(content, command.end); } + + if (ext === 'js') { + // ErrorBuilder + var tmp = content.match(/\$\.invalid\('[a-z-0-9]+'\)/gi); + if (tmp) { + for (var j = 0; j < tmp.length; j++) { + var m = (tmp[j] + ''); + m = m.substring(11, m.length - 2); + key = m; + file = filename.substring(dir.length + 1); + texts[key] = m; + if (resource[key]) { + if (resource[key].indexOf(file) === -1) + resource[key] += ', ' + file; + } else + resource[key] = file; + count++; + max = Math.max(max, key.length); + } + } + + // DBMS + tmp = content.match(/\.(error|err)\('[a-z-0-9]+'/gi); + if (tmp) { + for (var j = 0; j < tmp.length; j++) { + var m = (tmp[j] + ''); + m = m.substring(m.indexOf('(') + 2, m.length - 1); + key = m; + file = filename.substring(dir.length + 1); + texts[key] = m; + if (resource[key]) { + if (resource[key].indexOf(file) === -1) + resource[key] += ', ' + file; + } else + resource[key] = file; + count++; + max = Math.max(max, key.length); + } + } + } } var keys = Object.keys(resource); @@ -669,7 +809,7 @@ function main() { texts[key] = command.command; if (!resource[key]) { - output.push(key + ';"' + command.command.replace(/\"/g, '""') + '";'); + output.push(key + ';"' + command.command.replace(/"/g, '""') + '";'); resource[key] = true; count++; } @@ -701,23 +841,131 @@ function main() { } } - console.log('Downloading "empty-project" from www.totaljs.com'); + git(dir, 'emptyproject'); +} + +function git(dir, type) { + + var done = function() { + console.log('Installed: {0}'.format(type)); + console.log(); + }; - var url = 'https://cdn.totaljs.com/empty-project.package'; + U.ls(dir, function(fol, fil) { + + if (fol.length || fil.length) { + console.log('Directory "{0}"" is not empty.'.format(dir)); + console.log(); + return; + } - Utils.download(url, ['get'], function(err, response) { - var filename = path.join(dir, 'total.package'); - var stream = fs.createWriteStream(filename); - response.pipe(stream); - stream.on('finish', function() { - console.log('Unpacking file.'); - exec('totalpackage unpack total.package', function() { - fs.unlink(filename, NOOP); - console.log('Done.'); - console.log(''); + F.path.mkdir(dir); + exec('git clone https://github.com/totaljs/{0}.git {1}'.format(type, dir), function() { + F.path.mkdir(path.join(dir, '/node_modules/')); + F.rmdir(path.join(dir, '.git'), function() { + F.unlink(path.join(dir, '.gitignore'), function() { + F.path.exists(path.join(dir, 'package.json'), function(e) { + if (e) + exec('npm install total.js --save', done); + else + exec('npm install', done); + }); + }); }); }); }); } -main(); +function makebundle(dir, filename) { + + if (!filename) + filename = 'app.bundle'; + + var blacklist = {}; + blacklist['/bundle.json'] = 1; + blacklist['/debug.js'] = 1; + blacklist['/release.js'] = 1; + blacklist['/debug.pid'] = 1; + blacklist['/package.json'] = 1; + blacklist['/readme.md'] = 1; + blacklist['/license.txt'] = 1; + blacklist['/bundles/'] = 1; + blacklist['/tmp/'] = 1; + + if (filename[0] !== '/') + blacklist['/' + filename] = 1; + else + blacklist[filename] = 1; + + blacklist['/.git/'] = 1; + + if (filename.toLowerCase().lastIndexOf('.bundle') === -1) + filename += '.bundle'; + + blacklist[filename] = 1; + + console.log('--- CREATE BUNDLE PACKAGE --'); + console.log(''); + console.log('Directory :', dir); + console.log('Filename :', filename); + + F.backup(filename, U.path(dir), function(err, path) { + + if (err) + throw err; + + console.log('Success :', path.files.pluralize('# files', '# file', '# files', '# files') + ' (' + path.size.filesize() + ')'); + console.log(''); + + }, function(path) { + return blacklist[path] == null; + }); +} + +function makepackage(dir, filename) { + + if (!filename) + filename = 'noname.package'; + + var blacklist = {}; + blacklist['/bundle.json'] = 1; + blacklist['/debug.js'] = 1; + blacklist['/release.js'] = 1; + blacklist['/debug.pid'] = 1; + blacklist['/package.json'] = 1; + blacklist['/readme.md'] = 1; + blacklist['/license.txt'] = 1; + blacklist['/bundles/'] = 1; + blacklist['/tmp/'] = 1; + + if (filename[0] !== '/') + blacklist['/' + filename] = 1; + else + blacklist[filename] = 1; + + blacklist['/.git/'] = 1; + + if (filename.toLowerCase().lastIndexOf('.package') === -1) + filename += '.package'; + + blacklist[filename] = 1; + + console.log('--- CREATE PACKAGE --'); + console.log(''); + console.log('Directory :', dir); + console.log('Filename :', filename); + + F.backup(filename, U.path(dir), function(err, path) { + + if (err) + throw err; + + console.log('Success :', path.files.pluralize('# files', '# file', '# files', '# files') + ' (' + path.size.filesize() + ')'); + console.log(''); + + }, function(path) { + return blacklist[path] == null; + }); +} + +main(); \ No newline at end of file diff --git a/bin/tpm b/bin/tpm index 64a7a052c..a55e7e928 100755 --- a/bin/tpm +++ b/bin/tpm @@ -2,7 +2,7 @@ 'use strict'; -const VERSION = 'v3.0.0'; +const VERSION = 'v4.0.0'; const PADDING = 120; const Fs = require('fs'); @@ -528,6 +528,7 @@ function display_help() { log(''); log('--- --- --- --- ---'); log(''); + log('Creating packages:'); log(colors.red + '$ tpm create [important: package_name] [optional: package_directory_to_pack]' + colors.reset); log(''); log(colors.dim + 'EXAMPLE: tpm create my-project-template'); @@ -536,6 +537,15 @@ function display_help() { log(''); log('--- --- --- --- ---'); log(''); + log('Creating bundles:'); + log(colors.red + '$ tpm bundle [important: bundle_name] [optional: bundle_directory_to_pack]' + colors.reset); + log(''); + log(colors.dim + 'EXAMPLE: tpm bundle my-project-template'); + log('EXAMPLE: tpm bundle my-module'); + log('EXAMPLE: tpm bundle my-module /users/bundles/my-package/' + colors.reset); + log(''); + log('--- --- --- --- ---'); + log(''); log(colors.red + '$ tpm repository [repository_name] [repository_url]' + colors.reset); log(''); log(colors.dim + 'EXAMPLE: tpm repository local http://127.0.0.1:8000/'); @@ -804,10 +814,60 @@ function create() { log('Success :', path); log(''); - }, function(path) { - // return path.lastIndexOf('.package') === -1; - return true; - }); + }, () => true); +} + +function createbundle() { + + var target = process.cwd(); + var name = current_package; + var length = process.argv.length; + var blacklist = {}; + + blacklist['/bundle.json'] = 1; + blacklist['/debug.js'] = 1; + blacklist['/release.js'] = 1; + blacklist['/debug.pid'] = 1; + blacklist['/package.json'] = 1; + blacklist['/readme.md'] = 1; + blacklist['/license.txt'] = 1; + blacklist['/bundles/'] = 1; + blacklist['/tmp/'] = 1; + blacklist['/.git/'] = 1; + + if (length === 5) { + target = process.argv[4]; + name = process.argv[3]; + } else if (length === 4) { + current_package = process.argv[3]; + name = Path.join(process.cwd(), current_package); + } + + if (name.toLowerCase().lastIndexOf('.bundle') === -1) + name += '.bundle'; + + log(''); + log('--- CREATE BUNDLE PACKAGE --'); + log(''); + log('Package :', current_package); + log('Directory :', target); + + if (!isWindows) { + if (name[0] !== '/') + name = Path.join(process.cwd(), name); + } + + var backup = new Backup(); + + backup.backup(target, name, function(err, path) { + + if (err) + throw err; + + log('Success :', path); + log(''); + + }, path => blacklist[path] == null); } function unlink(path) { @@ -865,6 +925,11 @@ function main() { continue; } + if (cmd === 'bundle') { + $type = 7; + continue; + } + if (cmd === 'repository') { $type = 3; continue; @@ -938,6 +1003,10 @@ function main() { create(); break; + case 7: + createbundle(); + break; + case 3: repository_add(); break; diff --git a/builders.js b/builders.js index 4c89b6de6..ef1c17963 100755 --- a/builders.js +++ b/builders.js @@ -1,4 +1,4 @@ -// Copyright 2012-2018 (c) Peter Širka +// Copyright 2012-2020 (c) Peter Širka // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the @@ -21,37 +21,139 @@ /** * @module FrameworkBuilders - * @version 2.9.3 + * @version 3.4.4 */ 'use strict'; const REQUIRED = 'The field "@" is invalid.'; const DEFAULT_SCHEMA = 'default'; -const SKIP = { $$schema: true, $$result: true, $$callback: true, $$async: true, $$index: true, $$repository: true, $$can: true, $$controller: true }; +const SKIP = { $$schema: 1, $$async: 1, $$repository: 1, $$controller: 1, $$workflow: 1, $$parent: 1, $$keys: 1 }; const REGEXP_CLEAN_EMAIL = /\s/g; const REGEXP_CLEAN_PHONE = /\s|\.|-|\(|\)/g; -const REGEXP_NEWOPERATION = /^(async\s)?function(\s)?\([a-zA-Z0-9$]+\)|^function anonymous\(\$/; +const REGEXP_NEWOPERATION = /^(async\s)?function(\s)?\([a-zA-Z$\s]+\)|^function anonymous\(\$|^\([a-zA-Z$\s]+\)|^function\*\(\$|^\([a-zA-Z$\s]+\)/; const hasOwnProperty = Object.prototype.hasOwnProperty; const Qs = require('querystring'); +const MSG_OBSOLETE_NEW = 'You used older declaration of this delegate and you must rewrite it. Read more in docs.'; +const BOOL = { true: 1, on: 1, '1': 1 }; +const REGEXP_FILTER = /[a-z0-9-_]+:(\s)?(\[)?(String|Number|Boolean|Date)(\])?/i; var schemas = {}; +var schemasall = {}; +var schemacache = {}; var operations = {}; -var transforms = { pagination: {}, error: {}, transformbuilder: {}, restbuilder: {} }; +var tasks = {}; +var transforms = { pagination: {}, error: {}, restbuilder: {} }; +var restbuilderupgrades = []; function SchemaBuilder(name) { this.name = name; this.collection = {}; } -function SchemaOptions(error, model, options, callback, controller) { +const SchemaBuilderProto = SchemaBuilder.prototype; + +function SchemaOptions(error, model, options, callback, controller, name, schema) { this.error = error; this.value = this.model = model; - this.options = options; + this.options = options || EMPTYOBJECT; this.callback = this.next = callback; - this.controller = controller; + this.controller = (controller instanceof SchemaOptions || controller instanceof OperationOptions) ? controller.controller : controller; + this.name = name; + this.schema = schema; +} + +function TaskBuilder($) { + var t = this; + t.value = {}; + t.tasks = {}; + if ($ instanceof SchemaOptions || $ instanceof OperationOptions) { + t.error = $.error; + t.controller = $.controller; + } else { + if ($ instanceof Controller || $ instanceof WebSocketClient) + t.controller = $; + else if ($ instanceof ErrorBuilder) + t.error = $; + } } +TaskBuilder.prototype = { + + get user() { + return this.controller ? this.controller.user : null; + }, + + get session() { + return this.controller ? this.controller.session : null; + }, + + get sessionid() { + return this.controller && this.controller ? this.controller.req.sessionid : null; + }, + + get language() { + return (this.controller ? this.controller.language : '') || ''; + }, + + get ip() { + return this.controller ? this.controller.ip : null; + }, + + get id() { + return this.controller ? this.controller.id : null; + }, + + get req() { + return this.controller ? this.controller.req : null; + }, + + get res() { + return this.controller ? this.controller.res : null; + }, + + get params() { + return this.controller ? this.controller.params : null; + }, + + get files() { + return this.controller ? this.controller.files : null; + }, + + get body() { + return this.controller ? this.controller.body : null; + }, + + get query() { + return this.controller ? this.controller.query : null; + }, + + get model() { + return this.value; + }, + + set model(val) { + this.value = val; + }, + + get headers() { + return this.controller && this.controller.req ? this.controller.req.headers : null; + }, + + get ua() { + return this.controller && this.controller.req ? this.controller.req.ua : null; + }, + + get filter() { + var ctrl = this.controller; + if (ctrl && !ctrl.$filter) + ctrl.$filter = ctrl.$filterschema ? CONVERT(ctrl.query, ctrl.$filterschema) : ctrl.query; + return ctrl ? ctrl.$filter : EMPTYOBJECT; + } +}; + +const TaskBuilderProto = TaskBuilder.prototype; + SchemaOptions.prototype = { get user() { @@ -62,6 +164,26 @@ SchemaOptions.prototype = { return this.controller ? this.controller.session : null; }, + get keys() { + return this.model.$$keys; + }, + + get parent() { + return this.model.$$parent; + }, + + get repo() { + if (this.controller) + return this.controller.repository; + if (!this.model.$$repository) + this.model.$$repository = {}; + return this.model.$$repository; + }, + + get sessionid() { + return this.controller && this.controller ? this.controller.req.sessionid : null; + }, + get language() { return (this.controller ? this.controller.language : '') || ''; }, @@ -74,6 +196,14 @@ SchemaOptions.prototype = { return this.controller ? this.controller.id : null; }, + get req() { + return this.controller ? this.controller.req : null; + }, + + get res() { + return this.controller ? this.controller.res : null; + }, + get params() { return this.controller ? this.controller.params : null; }, @@ -88,14 +218,138 @@ SchemaOptions.prototype = { get query() { return this.controller ? this.controller.query : null; + }, + + get headers() { + return this.controller && this.controller.req ? this.controller.req.headers : null; + }, + + get ua() { + return this.controller && this.controller.req ? this.controller.req.ua : null; + }, + + get filter() { + var ctrl = this.controller; + if (ctrl && !ctrl.$filter) + ctrl.$filter = ctrl.$filterschema ? CONVERT(ctrl.query, ctrl.$filterschema) : ctrl.query; + return ctrl ? ctrl.$filter : EMPTYOBJECT; + } +}; + +var SchemaOptionsProto = SchemaOptions.prototype; + +SchemaOptionsProto.cancel = function() { + var self = this; + self.callback = self.next = null; + self.error = null; + self.controller = null; + self.model = null; + self.options = null; + return self; +}; + +SchemaOptionsProto.extend = function(data) { + var self = this; + var ext = self.schema.extensions[self.name]; + if (ext) { + for (var i = 0; i < ext.length; i++) + ext[i](self, data); + return true; } }; -SchemaOptions.prototype.DB = function() { +SchemaOptionsProto.redirect = function(url) { + this.callback(new F.callback_redirect(url)); + return this; +}; + +SchemaOptionsProto.clean = function() { + return this.model.$clean(); +}; + +SchemaOptionsProto.$async = function(callback, index) { + return this.model.$async(callback, index); +}; + +SchemaOptionsProto.$workflow = function(name, helper, callback, async) { + return this.model.$workflow(name, helper, callback, async); +}; + +SchemaOptionsProto.$transform = function(name, helper, callback, async) { + return this.model.$transform(name, helper, callback, async); +}; + +SchemaOptionsProto.$operation = function(name, helper, callback, async) { + return this.model.$operation(name, helper, callback, async); +}; + +SchemaOptionsProto.$hook = function(name, helper, callback, async) { + return this.model.$hook(name, helper, callback, async); +}; + +SchemaOptionsProto.$save = function(helper, callback, async) { + return this.model.$save(helper, callback, async); +}; + +SchemaOptionsProto.$insert = function(helper, callback, async) { + return this.model.$insert(helper, callback, async); +}; + +SchemaOptionsProto.$update = function(helper, callback, async) { + return this.model.$update(helper, callback, async); +}; + +SchemaOptionsProto.$patch = function(helper, callback, async) { + return this.model.$patch(helper, callback, async); +}; + +SchemaOptionsProto.$query = function(helper, callback, async) { + return this.model.$query(helper, callback, async); +}; + +SchemaOptionsProto.$delete = SchemaOptionsProto.$remove = function(helper, callback, async) { + return this.model.$remove(helper, callback, async); +}; + +SchemaOptionsProto.$get = SchemaOptionsProto.$read = function(helper, callback, async) { + return this.model.$get(helper, callback, async); +}; + +SchemaOptionsProto.push = function(type, name, helper, first) { + return this.model.$push(type, name, helper, first); +}; + +SchemaOptionsProto.next = function(type, name, helper) { + return this.model.$next(type, name, helper); +}; + +SchemaOptionsProto.output = function() { + return this.model.$output(); +}; + +SchemaOptionsProto.stop = function() { + return this.model.$stop(); +}; + +SchemaOptionsProto.response = function(index) { + return this.model.$response(index); +}; + +SchemaOptionsProto.DB = function() { return F.database(this.error); }; -SchemaOptions.prototype.success = function(a, b) { +SchemaOptionsProto.successful = function(callback) { + var self = this; + return function(err, a, b, c) { + if (err) + self.invalid(err); + else + callback.call(self, a, b, c); + }; +}; + +SchemaOptionsProto.success = function(a, b) { if (a && b === undefined && typeof(a) !== 'boolean') { b = a; @@ -106,39 +360,53 @@ SchemaOptions.prototype.success = function(a, b) { return this; }; -SchemaOptions.prototype.done = function(arg) { +SchemaOptionsProto.done = function(arg) { var self = this; return function(err, response) { + if (err) { - if (err && !(err instanceof ErrorBuilder)) { - self.error.push(err); - self.callback(); - } + if (self.error !== err) + self.error.push(err); - if (arg) - self.callback(SUCCESS(err == null, response)); + self.callback(); + } else if (arg) + self.callback(SUCCESS(err == null, arg === true ? response : arg)); else self.callback(SUCCESS(err == null)); }; }; -SchemaOptions.prototype.invalid = function(name, error, path, index) { - this.error.push(name, error, path, index); - this.callback(); - return this; +SchemaOptionsProto.invalid = function(name, error, path, index) { + var self = this; + + if (arguments.length) { + self.error.push(name, error, path, index); + self.callback(); + return self; + } + + return function(err) { + self.error.push(err); + self.callback(); + }; }; -SchemaOptions.prototype.repository = function(name, value) { +SchemaOptionsProto.repository = function(name, value) { return this.model && this.model.$repository ? this.model.$repository(name, value) : value; }; +SchemaOptionsProto.noop = function() { + this.callback(NoOp); + return this; +}; + /** * * Get a schema * @param {String} name * @return {Object} */ -SchemaBuilder.prototype.get = function(name) { +SchemaBuilderProto.get = function(name) { return this.collection[name]; }; @@ -147,7 +415,7 @@ SchemaBuilder.prototype.get = function(name) { * @alias * @return {SchemaBuilderEntity} */ -SchemaBuilder.prototype.create = function(name) { +SchemaBuilderProto.create = function(name) { this.collection[name] = new SchemaBuilderEntity(this, name); return this.collection[name]; }; @@ -157,19 +425,20 @@ SchemaBuilder.prototype.create = function(name) { * @param {String} name Schema name, optional. * @return {SchemaBuilder} */ -SchemaBuilder.prototype.remove = function(name) { +SchemaBuilderProto.remove = function(name) { if (name) { var schema = this.collection[name]; schema && schema.destroy(); schema = null; + delete schemasall[name.toLowerCase()]; delete this.collection[name]; } else { - delete schemas[this.name]; + exports.remove(this.name); this.collection = null; } }; -SchemaBuilder.prototype.destroy = function(name) { +SchemaBuilderProto.destroy = function(name) { return this.remove(name); }; @@ -182,7 +451,9 @@ function SchemaBuilderEntity(parent, name) { this.meta = {}; this.properties = []; this.inherits = []; + this.verifications = null; this.resourcePrefix; + this.extensions = {}; this.resourceName; this.transforms; this.workflows; @@ -195,6 +466,8 @@ function SchemaBuilderEntity(parent, name) { this.$onDefault; // Array of functions for inherits this.onValidate = F.onValidate; this.onSave; + this.onInsert; + this.onUpdate; this.onGet; this.onRemove; this.onQuery; @@ -208,22 +481,37 @@ function SchemaBuilderEntity(parent, name) { this.CurrentSchemaInstance.prototype.$$schema = this; } -SchemaBuilderEntity.prototype.allow = function() { +const SchemaBuilderEntityProto = SchemaBuilderEntity.prototype; + +SchemaBuilderEntityProto.allow = function() { var self = this; if (!self.fields_allow) self.fields_allow = []; - for (var i = 0, length = arguments.length; i < length; i++) { - if (arguments[i] instanceof Array) - arguments[i].forEach(item => self.fields_allow.push(item)); + var arr = arguments; + + if (arr.length === 1) + arr = arr[0].split(',').trim(); + + for (var i = 0, length = arr.length; i < length; i++) { + if (arr[i] instanceof Array) + arr[i].forEach(item => self.fields_allow.push(item)); else - self.fields_allow.push(arguments[i]); + self.fields_allow.push(arr[i]); } return self; }; -SchemaBuilderEntity.prototype.required = function(name, fn) { +SchemaBuilderEntityProto.before = function(name, fn) { + var self = this; + if (!self.preparation) + self.preparation = {}; + self.preparation[name] = fn; + return self; +}; + +SchemaBuilderEntityProto.required = function(name, fn) { var self = this; @@ -259,6 +547,59 @@ SchemaBuilderEntity.prototype.required = function(name, fn) { return self; }; +SchemaBuilderEntityProto.clear = function() { + var self = this; + + self.schema = {}; + self.properties = []; + self.fields = []; + self.verifications = null; + + if (self.preparation) + self.preparation = null; + + if (self.dependencies) + self.dependencies = null; + + if (self.fields_allow) + self.fields_allow = null; + + return self; +}; + +SchemaBuilderEntityProto.middleware = function(fn) { + var self = this; + if (!self.middlewares) + self.middlewares = []; + self.middlewares.push(fn); + return self; +}; + +function runmiddleware(opt, schema, callback, index, processor) { + + if (!index) + index = 0; + + var fn = schema.middlewares[index]; + + if (fn == null) { + callback.call(schema, opt); + return; + } + + if (processor) { + fn(opt, processor); + return; + } + + processor = function(stop) { + if (!stop) + runmiddleware(opt, schema, callback, index + 1, processor); + }; + + fn(opt, processor); +} + /** * Define type in schema * @param {String|String[]} name @@ -267,7 +608,7 @@ SchemaBuilderEntity.prototype.required = function(name, fn) { * @param {Number|String} [custom] Custom tag for search. * @return {SchemaBuilder} */ -SchemaBuilderEntity.prototype.define = function(name, type, required, custom) { +SchemaBuilderEntityProto.define = function(name, type, required, custom) { if (name instanceof Array) { for (var i = 0, length = name.length; i < length; i++) @@ -282,11 +623,20 @@ SchemaBuilderEntity.prototype.define = function(name, type, required, custom) { required = false; } + if (type == null) { + // remove + delete this.schema[name]; + this.properties = this.properties.remove(name); + if (this.dependencies) + this.dependencies = this.dependencies.remove(name); + this.fields = Object.keys(this.schema); + return this; + } + if (type instanceof SchemaBuilderEntity) type = type.name; - this.schema[name] = this.$parse(name, type, required, custom); - + var a = this.schema[name] = this.$parse(name, type, required, custom); switch (this.schema[name].type) { case 7: if (this.dependencies) @@ -298,16 +648,36 @@ SchemaBuilderEntity.prototype.define = function(name, type, required, custom) { this.fields = Object.keys(this.schema); - if (required) { - if (this.properties == null) - this.properties = []; + if (a.type === 7) + required = true; + + if (required) this.properties.indexOf(name) === -1 && this.properties.push(name); - } + else + this.properties = this.properties.remove(name); - return this; + return function(val) { + a.def = val; + return this; + }; +}; + +SchemaBuilderEntityProto.verify = function(name, fn, cache) { + var self = this; + + if (!self.verifications) + self.verifications = []; + + var cachekey; + + if (cache) + cachekey = self.name + '_verify_' + name + '_'; + + self.verifications.push({ name: name, fn: fn, cache: cache, cachekey: cachekey }); + return self; }; -SchemaBuilderEntity.prototype.inherit = function(group, name) { +SchemaBuilderEntityProto.inherit = function(group, name) { if (!name) { name = group; @@ -338,10 +708,30 @@ SchemaBuilderEntity.prototype.inherit = function(group, name) { copy_inherit(self, 'operations', schema.operations); copy_inherit(self, 'constants', schema.constants); + if (schema.middlewares) { + self.middlewares = []; + for (var i = 0; i < schema.middlewares.length; i++) + self.middlewares.push(schema.middlewares[i]); + } + + if (schema.verifications) { + self.verifications = []; + for (var i = 0; i < schema.verifications.length; i++) + self.verifications.push(schema.verifications[i]); + } + schema.properties.forEach(function(item) { - self.properties.indexOf(item) === -1 && self.properties.push(item); + if (self.properties.indexOf(item) === -1) + self.properties.push(item); }); + if (schema.preparation) { + self.preparation = {}; + Object.keys(schema.preparation).forEach(function(key) { + self.preparation[key] = schema.preparation[key]; + }); + } + if (schema.onPrepare) { if (!self.$onPrepare) self.$onPrepare = []; @@ -360,6 +750,12 @@ SchemaBuilderEntity.prototype.inherit = function(group, name) { if (!self.onSave && schema.onSave) self.onSave = schema.onSave; + if (!self.onInsert && schema.onInsert) + self.onInsert = schema.onInsert; + + if (!self.onUpdate && schema.onUpdate) + self.onUpdate = schema.onUpdate; + if (!self.onGet && schema.onGet) self.onGet = schema.onGet; @@ -398,7 +794,7 @@ function copy_inherit(schema, field, value) { * Set primary key * @param {String} name */ -SchemaBuilderEntity.prototype.setPrimary = function(name) { +SchemaBuilderEntityProto.setPrimary = function(name) { this.primary = name; return this; }; @@ -410,7 +806,7 @@ SchemaBuilderEntity.prototype.setPrimary = function(name) { * @param {Boolean} reverse Reverse results. * @return {Array|Object} Returns Array (with property names) if the model is undefined otherwise returns Object Name/Value. */ -SchemaBuilderEntity.prototype.filter = function(custom, model, reverse) { +SchemaBuilderEntityProto.filter = function(custom, model, reverse) { if (typeof(model) === 'boolean') { var tmp = reverse; @@ -479,7 +875,7 @@ function parseLength(lower, result) { return result; } -SchemaBuilderEntity.prototype.$parse = function(name, value, required, custom) { +SchemaBuilderEntityProto.$parse = function(name, value, required, custom) { var type = typeof(value); var result = {}; @@ -493,14 +889,19 @@ SchemaBuilderEntity.prototype.$parse = function(name, value, required, custom) { result.isArray = false; result.custom = custom || ''; - // 0 = undefined - // 1 = integer - // 2 = float - // 3 = string - // 4 = boolean - // 5 = date - // 6 = object - // 7 = custom object + // 0 = undefined + // 1 = integer + // 2 = float + // 3 = string + // 4 = boolean + // 5 = date + // 6 = object + // 7 = custom object + // 8 = enum + // 9 = keyvalue + // 10 = custom object type + // 11 = number2 + // 12 = object as filter if (value === null) return result; @@ -512,6 +913,14 @@ SchemaBuilderEntity.prototype.$parse = function(name, value, required, custom) { if (type === 'function') { + if (value === UID) { + result.type = 3; + result.length = 20; + result.raw = 'string'; + result.subtype = 'uid'; + return result; + } + if (value === Number) { result.type = 2; return result; @@ -542,6 +951,15 @@ SchemaBuilderEntity.prototype.$parse = function(name, value, required, custom) { return result; } + if (value instanceof SchemaBuilderEntity) + result.type = 7; + else { + result.type = 10; + if (!this.asyncfields) + this.asyncfields = []; + this.asyncfields.push(name); + } + return result; } @@ -572,11 +990,23 @@ SchemaBuilderEntity.prototype.$parse = function(name, value, required, custom) { return result; } + if (value.indexOf(',') !== -1) { + // multiple + result.type = 12; + return result; + } + if ((/^(string|text)+(\(\d+\))?$/).test(lower)) { result.type = 3; return parseLength(lower, result); } + if ((/^(capitalize2)+(\(\d+\))?$/).test(lower)) { + result.type = 3; + result.subtype = 'capitalize2'; + return parseLength(lower, result); + } + if ((/^(capitalize|camelcase|camelize)+(\(\d+\))?$/).test(lower)) { result.type = 3; result.subtype = 'capitalize'; @@ -589,6 +1019,13 @@ SchemaBuilderEntity.prototype.$parse = function(name, value, required, custom) { return parseLength(lower, result); } + if (lower.indexOf('base64') !== -1) { + result.type = 3; + result.raw = 'string'; + result.subtype = 'base64'; + return result; + } + if ((/^(upper|uppercase)+(\(\d+\))?$/).test(lower)) { result.subtype = 'uppercase'; result.type = 3; @@ -642,6 +1079,11 @@ SchemaBuilderEntity.prototype.$parse = function(name, value, required, custom) { return result; } + if (lower === 'number2') { + result.type = 11; + return result; + } + if (['int', 'integer', 'byte'].indexOf(lower) !== -1) { result.type = 1; return result; @@ -666,7 +1108,7 @@ SchemaBuilderEntity.prototype.$parse = function(name, value, required, custom) { return result; }; -SchemaBuilderEntity.prototype.getDependencies = function() { +SchemaBuilderEntityProto.getDependencies = function() { var dependencies = []; for (var name in this.schema) { @@ -692,7 +1134,7 @@ SchemaBuilderEntity.prototype.getDependencies = function() { * @param {Function(propertyName, value, path, entityName, model)} fn A validation function. * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.setValidate = function(properties, fn) { +SchemaBuilderEntityProto.setValidate = function(properties, fn) { if (fn === undefined && properties instanceof Array) { this.properties = properties; @@ -708,12 +1150,12 @@ SchemaBuilderEntity.prototype.setValidate = function(properties, fn) { return this; }; -SchemaBuilderEntity.prototype.setPrefix = function(prefix) { +SchemaBuilderEntityProto.setPrefix = function(prefix) { this.resourcePrefix = prefix; return this; }; -SchemaBuilderEntity.prototype.setResource = function(name) { +SchemaBuilderEntityProto.setResource = function(name) { this.resourceName = name; return this; }; @@ -723,7 +1165,7 @@ SchemaBuilderEntity.prototype.setResource = function(name) { * @param {Function(propertyName, isntPreparing, entityName)} fn * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.setDefault = function(fn) { +SchemaBuilderEntityProto.setDefault = function(fn) { this.onDefault = fn; return this; }; @@ -733,7 +1175,7 @@ SchemaBuilderEntity.prototype.setDefault = function(fn) { * @param {Function(name, value)} fn Must return a new value. * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.setPrepare = function(fn) { +SchemaBuilderEntityProto.setPrepare = function(fn) { this.onPrepare = fn; return this; }; @@ -743,46 +1185,183 @@ SchemaBuilderEntity.prototype.setPrepare = function(fn) { * @param {Function(error, model, helper, next(value), controller)} fn * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.setSave = function(fn, description) { +SchemaBuilderEntityProto.setSave = function(fn, description, filter) { + + if (typeof(description) === 'string' && REGEXP_FILTER.test(description)) { + filter = description; + description = null; + } + fn.$newversion = REGEXP_NEWOPERATION.test(fn.toString()); this.onSave = fn; - this.meta.save = description; + this.meta.save = description || null; + this.meta.savefilter = filter; + !fn.$newversion && OBSOLETE('Schema("{0}").setSave()'.format(this.name), MSG_OBSOLETE_NEW); return this; }; - -/** - * Set error handler - * @param {Function(error)} fn - * @return {SchemaBuilderEntity} - */ -SchemaBuilderEntity.prototype.setError = function(fn) { - this.onError = fn; +SchemaBuilderEntityProto.setSaveExtension = function(fn) { + var key = 'save'; + if (this.extensions[key]) + this.extensions[key].push(fn); + else + this.extensions[key] = [fn]; return this; }; /** - * Set getter handler + * Set insert handler * @param {Function(error, model, helper, next(value), controller)} fn * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.setGet = SchemaBuilderEntity.prototype.setRead = function(fn, description) { +SchemaBuilderEntityProto.setInsert = function(fn, description, filter) { + + if (typeof(description) === 'string' && REGEXP_FILTER.test(description)) { + filter = description; + description = null; + } + fn.$newversion = REGEXP_NEWOPERATION.test(fn.toString()); - this.onGet = fn; - this.meta.get = description; + this.onInsert = fn; + this.meta.insert = description || null; + this.meta.insertfilter = filter; + !fn.$newversion && OBSOLETE('Schema("{0}").setInsert()'.format(this.name), MSG_OBSOLETE_NEW); + return this; +}; + +SchemaBuilderEntityProto.setInsertExtension = function(fn) { + var key = 'insert'; + if (this.extensions[key]) + this.extensions[key].push(fn); + else + this.extensions[key] = [fn]; return this; }; /** - * Set query handler - * @param {Function(error, helper, next(value), controller)} fn + * Set update handler + * @param {Function(error, model, helper, next(value), controller)} fn + * @return {SchemaBuilderEntity} + */ +SchemaBuilderEntityProto.setUpdate = function(fn, description, filter) { + + if (typeof(description) === 'string' && REGEXP_FILTER.test(description)) { + filter = description; + description = null; + } + + fn.$newversion = REGEXP_NEWOPERATION.test(fn.toString()); + this.onUpdate = fn; + this.meta.update = description || null; + this.meta.updatefilter = filter; + !fn.$newversion && OBSOLETE('Schema("{0}").setUpdate()'.format(this.name), MSG_OBSOLETE_NEW); + return this; +}; + +SchemaBuilderEntityProto.setUpdateExtension = function(fn) { + var key = 'update'; + if (this.extensions[key]) + this.extensions[key].push(fn); + else + this.extensions[key] = [fn]; + return this; +}; + +/** + * Set patch handler + * @param {Function(error, model, helper, next(value), controller)} fn + * @return {SchemaBuilderEntity} + */ +SchemaBuilderEntityProto.setPatch = function(fn, description, filter) { + + if (typeof(description) === 'string' && REGEXP_FILTER.test(description)) { + filter = description; + description = null; + } + fn.$newversion = REGEXP_NEWOPERATION.test(fn.toString()); + this.onPatch = fn; + this.meta.patch = description || null; + this.meta.patchfilter = filter; + !fn.$newversion && OBSOLETE('Schema("{0}").setPatch()'.format(this.name), MSG_OBSOLETE_NEW); + return this; +}; + +SchemaBuilderEntityProto.setPatchExtension = function(fn) { + var key = 'patch'; + if (this.extensions[key]) + this.extensions[key].push(fn); + else + this.extensions[key] = [fn]; + return this; +}; + +/** + * Set error handler + * @param {Function(error)} fn + * @return {SchemaBuilderEntity} + */ +SchemaBuilderEntityProto.setError = function(fn) { + this.onError = fn; + return this; +}; + +/** + * Set getter handler + * @param {Function(error, model, helper, next(value), controller)} fn + * @return {SchemaBuilderEntity} + */ +SchemaBuilderEntityProto.setGet = SchemaBuilderEntityProto.setRead = function(fn, description, filter) { + + if (typeof(description) === 'string' && REGEXP_FILTER.test(description)) { + filter = description; + description = null; + } + + fn.$newversion = REGEXP_NEWOPERATION.test(fn.toString()); + this.onGet = fn; + this.meta.get = this.meta.read = description || null; + this.meta.getfilter = this.meta.readfilter = filter; + !fn.$newversion && OBSOLETE('Schema("{0}").setGet()'.format(this.name), MSG_OBSOLETE_NEW); + return this; +}; + +SchemaBuilderEntityProto.setGetExtension = SchemaBuilderEntityProto.setReadExtension = function(fn) { + var key = 'read'; + if (this.extensions[key]) + this.extensions[key].push(fn); + else + this.extensions[key] = [fn]; + return this; +}; + +/** + * Set query handler + * @param {Function(error, helper, next(value), controller)} fn * @param {String} description Optional. * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.setQuery = function(fn, description) { +SchemaBuilderEntityProto.setQuery = function(fn, description, filter) { + + if (typeof(description) === 'string' && REGEXP_FILTER.test(description)) { + filter = description; + description = null; + } + fn.$newversion = REGEXP_NEWOPERATION.test(fn.toString()); this.onQuery = fn; - this.meta.query = description; + this.meta.query = description || null; + this.meta.queryfilter = filter; + + !fn.$newversion && OBSOLETE('Schema("{0}").setQuery()'.format(this.name), MSG_OBSOLETE_NEW); + return this; +}; + +SchemaBuilderEntityProto.setQueryExtension = function(fn) { + var key = 'query'; + if (this.extensions[key]) + this.extensions[key].push(fn); + else + this.extensions[key] = [fn]; return this; }; @@ -792,10 +1371,27 @@ SchemaBuilderEntity.prototype.setQuery = function(fn, description) { * @param {String} description Optional. * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.setRemove = function(fn, description) { +SchemaBuilderEntityProto.setRemove = function(fn, description, filter) { + + if (typeof(description) === 'string' && REGEXP_FILTER.test(description)) { + filter = description; + description = null; + } + fn.$newversion = REGEXP_NEWOPERATION.test(fn.toString()); this.onRemove = fn; - this.meta.remove = description; + this.meta.remove = description || null; + this.meta.removefilter = filter; + !fn.$newversion && OBSOLETE('Schema("{0}").setRemove()'.format(this.name), MSG_OBSOLETE_NEW); + return this; +}; + +SchemaBuilderEntityProto.setRemoveExtension = function(fn) { + var key = 'remove'; + if (this.extensions[key]) + this.extensions[key].push(fn); + else + this.extensions[key] = [fn]; return this; }; @@ -806,14 +1402,16 @@ SchemaBuilderEntity.prototype.setRemove = function(fn, description) { * @param {String} description Optional. * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.constant = function(name, value, description) { +SchemaBuilderEntityProto.constant = function(name, value, description) { + + OBSOLETE('Constants will be removed from schemas.'); if (value === undefined) return this.constants ? this.constants[name] : undefined; !this.constants && (this.constants = {}); this.constants[name] = value; - this.meta['constant#' + name] = description; + this.meta['constant#' + name] = description || null; return this; }; @@ -824,17 +1422,33 @@ SchemaBuilderEntity.prototype.constant = function(name, value, description) { * @param {String} description Optional. * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.addTransform = function(name, fn, description) { +SchemaBuilderEntityProto.addTransform = function(name, fn, description, filter) { if (typeof(name) === 'function') { fn = name; name = 'default'; } + if (typeof(description) === 'string' && REGEXP_FILTER.test(description)) { + filter = description; + description = null; + } + fn.$newversion = REGEXP_NEWOPERATION.test(fn.toString()); !this.transforms && (this.transforms = {}); this.transforms[name] = fn; - this.meta['transform#' + name] = description; + this.meta['transform#' + name] = description || null; + this.meta['transformfilter#' + name] = filter; + !fn.$newversion && OBSOLETE('Schema("{0}").addTransform("{1}")'.format(this.name, name), MSG_OBSOLETE_NEW); + return this; +}; + +SchemaBuilderEntityProto.addTransformExtension = function(name, fn) { + var key = 'transform.' + name; + if (this.extensions[key]) + this.extensions[key].push(fn); + else + this.extensions[key] = [fn]; return this; }; @@ -845,17 +1459,33 @@ SchemaBuilderEntity.prototype.addTransform = function(name, fn, description) { * @param {String} description Optional. * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.addOperation = function(name, fn, description) { +SchemaBuilderEntityProto.addOperation = function(name, fn, description, filter) { if (typeof(name) === 'function') { fn = name; name = 'default'; } + if (typeof(description) === 'string' && REGEXP_FILTER.test(description)) { + filter = description; + description = null; + } + fn.$newversion = REGEXP_NEWOPERATION.test(fn.toString()); !this.operations && (this.operations = {}); this.operations[name] = fn; - this.meta['operation#' + name] = description; + this.meta['operation#' + name] = description || null; + this.meta['operationfilter#' + name] = filter; + !fn.$newversion && OBSOLETE('Schema("{0}").addOperation("{1}")'.format(this.name, name), MSG_OBSOLETE_NEW); + return this; +}; + +SchemaBuilderEntityProto.addOperationExtension = function(name, fn) { + var key = 'operation.' + name; + if (this.extensions[key]) + this.extensions[key].push(fn); + else + this.extensions[key] = [fn]; return this; }; @@ -866,29 +1496,61 @@ SchemaBuilderEntity.prototype.addOperation = function(name, fn, description) { * @param {String} description Optional. * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.addWorkflow = function(name, fn, description) { +SchemaBuilderEntityProto.addWorkflow = function(name, fn, description, filter) { if (typeof(name) === 'function') { fn = name; name = 'default'; } + if (typeof(description) === 'string' && REGEXP_FILTER.test(description)) { + filter = description; + description = null; + } + fn.$newversion = REGEXP_NEWOPERATION.test(fn.toString()); !this.workflows && (this.workflows = {}); this.workflows[name] = fn; - this.meta['workflow#' + name] = description; + this.meta['workflow#' + name] = description || null; + this.meta['workflowfilter#' + name] = filter; + !fn.$newversion && OBSOLETE('Schema("{0}").addWorkflow("{1}")'.format(this.name, name), MSG_OBSOLETE_NEW); + return this; +}; + +SchemaBuilderEntityProto.addWorkflowExtension = function(name, fn) { + var key = 'workflow.' + name; + if (this.extensions[key]) + this.extensions[key].push(fn); + else + this.extensions[key] = [fn]; return this; }; -SchemaBuilderEntity.prototype.addHook = function(name, fn, description) { +SchemaBuilderEntityProto.addHook = function(name, fn, description, filter) { if (!this.hooks) this.hooks = {}; + if (typeof(description) === 'string' && REGEXP_FILTER.test(description)) { + filter = description; + description = null; + } + fn.$newversion = REGEXP_NEWOPERATION.test(fn.toString()); !this.hooks[name] && (this.hooks[name] = []); this.hooks[name].push({ owner: F.$owner(), fn: fn }); - this.meta['hook#' + name] = description; + this.meta['hook#' + name] = description || null; + this.meta['hookfilter#' + name] = filter; + !fn.$newversion && OBSOLETE('Schema("{0}").addHook("{1}")'.format(this.name, name), MSG_OBSOLETE_NEW); + return this; +}; + +SchemaBuilderEntityProto.addHookExtension = function(name, fn) { + var key = 'hook.' + name; + if (this.extensions[key]) + this.extensions[key].push(fn); + else + this.extensions[key] = [fn]; return this; }; @@ -897,40 +1559,42 @@ SchemaBuilderEntity.prototype.addHook = function(name, fn, description) { * @param {String} name * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.find = function(name) { +SchemaBuilderEntityProto.find = function(name) { return this.parent.get(name); }; /** * Destroys current entity */ -SchemaBuilderEntity.prototype.destroy = function() { +SchemaBuilderEntityProto.destroy = function() { delete this.parent.collection[this.name]; - this.properties = undefined; - this.schema = undefined; - this.onDefault = undefined; - this.$onDefault = undefined; - this.onValidate = undefined; - this.onSave = undefined; - this.onRead = undefined; - this.onGet = undefined; - this.onRemove = undefined; - this.onQuery = undefined; - this.workflows = undefined; - this.operations = undefined; - this.transforms = undefined; - this.meta = undefined; - this.newversion = undefined; - this.properties = undefined; - this.hooks = undefined; - this.constants = undefined; - this.onPrepare = undefined; - this.$onPrepare = undefined; - this.onError = undefined; - this.gcache = undefined; - this.dependencies = undefined; - this.fields = undefined; - this.fields_allow = undefined; + delete this.properties; + delete this.schema; + delete this.onDefault; + delete this.$onDefault; + delete this.onValidate; + delete this.onSave; + delete this.onInsert; + delete this.onUpdate; + delete this.onRead; + delete this.onGet; + delete this.onRemove; + delete this.onQuery; + delete this.workflows; + delete this.operations; + delete this.transforms; + delete this.meta; + delete this.newversion; + delete this.properties; + delete this.hooks; + delete this.constants; + delete this.onPrepare; + delete this.$onPrepare; + delete this.onError; + delete this.gcache; + delete this.dependencies; + delete this.fields; + delete this.fields_allow; }; /** @@ -942,7 +1606,41 @@ SchemaBuilderEntity.prototype.destroy = function() { * @param {Boolean} skip Skips preparing and validation, optional * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.save = function(model, options, callback, controller, skip) { +SchemaBuilderEntityProto.save = function(model, options, callback, controller, skip) { + return this.execute('onSave', model, options, callback, controller, skip); +}; + +/** + * Execute onInsert delegate + * @param {Object} model + * @param {Object} options Custom options object, optional + * @param {Function(err, result)} callback + * @param {Controller} controller + * @param {Boolean} skip Skips preparing and validation, optional + * @return {SchemaBuilderEntity} + */ +SchemaBuilderEntityProto.insert = function(model, options, callback, controller, skip) { + return this.execute('onInsert', model, options, callback, controller, skip); +}; + +/** + * Execute onUpdate delegate + * @param {Object} model + * @param {Object} options Custom options object, optional + * @param {Function(err, result)} callback + * @param {Controller} controller + * @param {Boolean} skip Skips preparing and validation, optional + * @return {SchemaBuilderEntity} + */ +SchemaBuilderEntityProto.update = function(model, options, callback, controller, skip) { + return this.execute('onUpdate', model, options, callback, controller, skip); +}; + +SchemaBuilderEntityProto.patch = function(model, options, callback, controller, skip) { + return this.execute('onPatch', model, options, callback, controller, skip); +}; + +SchemaBuilderEntityProto.execute = function(TYPE, model, options, callback, controller, skip) { if (typeof(callback) === 'boolean') { skip = callback; @@ -963,7 +1661,25 @@ SchemaBuilderEntity.prototype.save = function(model, options, callback, controll callback = function(){}; var self = this; - var $type = 'save'; + var $type; + + switch (TYPE) { + case 'onInsert': + $type = 'insert'; + break; + case 'onUpdate': + $type = 'update'; + break; + case 'onPatch': + $type = 'patch'; + break; + default: + $type = 'save'; + break; + } + + if (!self[TYPE]) + return callback(new Error('Operation "{0}/{1}" not found'.format(self.name, $type))); self.$prepare(model, function(err, model) { @@ -972,21 +1688,37 @@ SchemaBuilderEntity.prototype.save = function(model, options, callback, controll return; } + if (controller instanceof SchemaOptions || controller instanceof OperationOptions) + controller = controller.controller; + if (model && !controller && model.$$controller) controller = model.$$controller; var builder = new ErrorBuilder(); + var $now; + + if (CONF.logger) + $now = Date.now(); + self.resourceName && builder.setResource(self.resourceName); self.resourcePrefix && builder.setPrefix(self.resourcePrefix); - if (!isGenerator(self, $type, self.onSave)) { - if (self.onSave.$newversion) - self.onSave(new SchemaOptions(builder, model, options, function(res) { - self.$process(arguments, model, $type, undefined, builder, res, callback); - }, controller)); - else - self.onSave(builder, model, options, function(res) { - self.$process(arguments, model, $type, undefined, builder, res, callback); + if (!isGenerator(self, $type, self[TYPE])) { + if (self[TYPE].$newversion) { + var opt = new SchemaOptions(builder, model, options, function(res) { + CONF.logger && F.ilogger(self.getLoggerName($type), controller, $now); + self.$process(arguments, model, $type, undefined, builder, res, callback, controller); + }, controller, $type, self); + + if (self.middlewares && self.middlewares.length) + runmiddleware(opt, self, self[TYPE]); + else + self[TYPE](opt); + + } else + self[TYPE](builder, model, options, function(res) { + CONF.logger && F.ilogger(self.getLoggerName($type), controller, $now); + self.$process(arguments, model, $type, undefined, builder, res, callback, controller); }, controller, skip !== true); return self; } @@ -996,13 +1728,20 @@ SchemaBuilderEntity.prototype.save = function(model, options, callback, controll var onError = function(err) { if (!err || callback.success) return; + callback.success = true; - builder.push(err); + + if (builder !== err) + builder.push(err); + self.onError && self.onError(builder, model, $type); callback(builder); }; var onCallback = function(res) { + + CONF.logger && F.ilogger(self.getLoggerName($type, name), controller, $now); + if (callback.success) return; @@ -1012,21 +1751,27 @@ SchemaBuilderEntity.prototype.save = function(model, options, callback, controll res = arguments[1]; } - var has = builder.hasError(); + var has = builder.is; has && self.onError && self.onError(builder, model, $type); callback.success = true; callback(has ? builder : null, res === undefined ? model : res); }; - if (self.onSave.$newversion) - async.call(self, self.onSave)(onError, new SchemaOptions(builder, model, options, onCallback, controller)); - else - async.call(self, self.onSave)(onError, builder, model, options, onCallback, controller, skip !== true); - }); + if (self[TYPE].$newversion) { + var opt = new SchemaOptions(builder, model, options, onCallback, controller, $type, self); + if (self.middlewares && self.middlewares.length) + runmiddleware(opt, self, () => async.call(self, self[TYPE])(onError, opt)); + else + async.call(self, self[TYPE])(onError, opt); + } else + async.call(self, self[TYPE])(onError, builder, model, options, onCallback, controller, skip !== true); + + }, controller ? controller.req : null); return self; }; + function isGenerator(obj, name, fn) { return obj.gcache[name] ? obj.gcache[name] : obj.gcache[name] = fn.toString().substring(0, 9) === 'function*'; } @@ -1037,7 +1782,7 @@ function isGenerator(obj, name, fn) { * @param {Function(err, result)} callback * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.get = SchemaBuilderEntity.prototype.read = function(options, callback, controller) { +SchemaBuilderEntityProto.get = SchemaBuilderEntityProto.read = function(options, callback, controller) { if (typeof(options) === 'function') { callback = options; @@ -1049,21 +1794,40 @@ SchemaBuilderEntity.prototype.get = SchemaBuilderEntity.prototype.read = functio var self = this; var builder = new ErrorBuilder(); + var $now; self.resourceName && builder.setResource(self.resourceName); self.resourcePrefix && builder.setPrefix(self.resourcePrefix); + if (controller instanceof SchemaOptions || controller instanceof OperationOptions) + controller = controller.controller; + + if (self.meta.getfilter && controller) { + controller.$filterschema = self.meta.getfilter; + controller.$filter = null; + } + + if (CONF.logger) + $now = Date.now(); + var output = self.default(); var $type = 'get'; if (!isGenerator(self, $type, self.onGet)) { - if (self.onGet.$newversion) - self.onGet(new SchemaOptions(builder, output, options, function(res) { - self.$process(arguments, output, $type, undefined, builder, res, callback); - }, controller)); - else + if (self.onGet.$newversion) { + var opt = new SchemaOptions(builder, output, options, function(res) { + CONF.logger && F.ilogger(self.getLoggerName($type), controller, $now); + self.$process(arguments, output, $type, undefined, builder, res, callback, controller); + }, controller, $type, self); + + if (self.middlewares && self.middlewares.length) + runmiddleware(opt, self, self.onGet); + else + self.onGet(opt); + } else self.onGet(builder, output, options, function(res) { - self.$process(arguments, output, $type, undefined, builder, res, callback); + CONF.logger && F.ilogger(self.getLoggerName($type), controller, $now); + self.$process(arguments, output, $type, undefined, builder, res, callback, controller); }, controller); return self; } @@ -1074,12 +1838,18 @@ SchemaBuilderEntity.prototype.get = SchemaBuilderEntity.prototype.read = functio if (!err || callback.success) return; callback.success = true; - builder.push(err); + + if (builder !== err) + builder.push(err); + self.onError && self.onError(builder, output, $type); callback(builder); }; var onCallback = function(res) { + + CONF.logger && F.ilogger(self.getLoggerName($type), controller, $now); + if (callback.success) return; @@ -1090,14 +1860,18 @@ SchemaBuilderEntity.prototype.get = SchemaBuilderEntity.prototype.read = functio } callback.success = true; - var has = builder.hasError(); + var has = builder.is; has && self.onError && self.onError(builder, output, $type); callback(has ? builder : null, res === undefined ? output : res); }; - if (self.onGet.$newversion) - async.call(self, self.onGet)(onError, new SchemaOptions(builder, output, options, onCallback, controller)); - else + if (self.onGet.$newversion) { + var opt = new SchemaOptions(builder, output, options, onCallback, controller, $type, self); + if (self.middlewares && self.middlewares.length) + runmiddleware(opt, self, () => async.call(self, self.onGet)(onError, opt)); + else + async.call(self, self.onGet)(onError, opt); + } else async.call(self, self.onGet)(onError, builder, output, options, onCallback, controller); return self; @@ -1109,7 +1883,7 @@ SchemaBuilderEntity.prototype.get = SchemaBuilderEntity.prototype.read = functio * @param {Function(err, result)} callback * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.remove = function(options, callback, controller) { +SchemaBuilderEntityProto.remove = function(options, callback, controller) { if (typeof(options) === 'function') { callback = options; @@ -1119,18 +1893,41 @@ SchemaBuilderEntity.prototype.remove = function(options, callback, controller) { var self = this; var builder = new ErrorBuilder(); var $type = 'remove'; + var $now; + + if (!self.onRemove) + return callback(new Error('Operation "{0}/{1}" not found'.format(self.name, $type))); + + if (controller instanceof SchemaOptions || controller instanceof OperationOptions) + controller = controller.controller; + + if (self.meta.removefilter && controller) { + controller.$filterschema = self.meta.removefilter; + controller.$filter = null; + } + + if (CONF.logger) + $now = Date.now(); self.resourceName && builder.setResource(self.resourceName); self.resourcePrefix && builder.setPrefix(self.resourcePrefix); if (!isGenerator(self, $type, self.onRemove)) { - if (self.onRemove.$newversion) - self.onRemove(new SchemaOptions(builder, undefined, options, function(res) { - self.$process(arguments, undefined, $type, undefined, builder, res, callback); - }, controller)); - else + if (self.onRemove.$newversion) { + + var opt = new SchemaOptions(builder, controller ? controller.body : undefined, options, function(res) { + CONF.logger && F.ilogger(self.getLoggerName($type), controller, $now); + self.$process(arguments, undefined, $type, undefined, builder, res, callback, controller); + }, controller, $type, self); + + if (self.middlewares && self.middlewares.length) + runmiddleware(opt, self, self.onRemove); + else + self.onRemove(opt); + } else self.onRemove(builder, options, function(res) { - self.$process(arguments, undefined, $type, undefined, builder, res, callback); + CONF.logger && F.ilogger(self.getLoggerName($type), controller, $now); + self.$process(arguments, undefined, $type, undefined, builder, res, callback, controller); }, controller); return self; } @@ -1141,13 +1938,18 @@ SchemaBuilderEntity.prototype.remove = function(options, callback, controller) { if (!err || callback.success) return; callback.success = true; - builder.push(err); + + if (builder !== err) + builder.push(err); + self.onError && self.onError(builder, EMPTYOBJECT, $type); callback(builder); }; var onCallback = function(res) { + CONF.logger && F.ilogger(self.getLoggerName($type, name), controller, $now); + if (callback.success) return; @@ -1157,15 +1959,19 @@ SchemaBuilderEntity.prototype.remove = function(options, callback, controller) { res = arguments[1]; } - var has = builder.hasError(); + var has = builder.is; has && self.onError && self.onError(builder, EMPTYOBJECT, $type); callback.success = true; callback(has ? builder : null, res === undefined ? options : res); }; - if (self.onRemove.$newversion) - async.call(self, self.onRemove)(onError, new SchemaOptions(builder, undefined, options, onCallback, controller)); - else + if (self.onRemove.$newversion) { + var opt = new SchemaOptions(builder, undefined, options, onCallback, controller, $type, self); + if (self.middlewares && self.middlewares.length) + runmiddleware(opt, self, () => async.call(self, self.onRemove)(onError, opt)); + else + async.call(self, self.onRemove)(onError, opt); + } else async.call(self, self.onRemove)(onError, builder, options, onCallback, controller); return self; @@ -1177,28 +1983,48 @@ SchemaBuilderEntity.prototype.remove = function(options, callback, controller) { * @param {Function(err, result)} callback * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.query = function(options, callback, controller) { +SchemaBuilderEntityProto.query = function(options, callback, controller) { if (typeof(options) === 'function') { callback = options; options = undefined; } + if (controller instanceof SchemaOptions || controller instanceof OperationOptions) + controller = controller.controller; + var self = this; var builder = new ErrorBuilder(); var $type = 'query'; + var $now; + + if (self.meta.queryfilter && controller) { + controller.$filterschema = self.meta.queryfilter; + controller.$filter = null; + } self.resourceName && builder.setResource(self.resourceName); self.resourcePrefix && builder.setPrefix(self.resourcePrefix); + if (CONF.logger) + $now = Date.now(); + if (!isGenerator(self, $type, self.onQuery)) { - if (self.onQuery.$newversion) - self.onQuery(new SchemaOptions(builder, undefined, options, function(res) { - self.$process(arguments, undefined, $type, undefined, builder, res, callback); - }, controller)); - else + if (self.onQuery.$newversion) { + var opt = new SchemaOptions(builder, undefined, options, function(res) { + CONF.logger && F.ilogger(self.getLoggerName($type), controller, $now); + self.$process(arguments, undefined, $type, undefined, builder, res, callback, controller); + }, controller, $type, self); + + if (self.middlewares && self.middlewares.length) + runmiddleware(opt, self, self.onQuery); + else + self.onQuery(opt); + + } else self.onQuery(builder, options, function(res) { - self.$process(arguments, undefined, $type, undefined, builder, res, callback); + CONF.logger && F.ilogger(self.getLoggerName($type), controller, $now); + self.$process(arguments, undefined, $type, undefined, builder, res, callback, controller); }, controller); return self; } @@ -1209,13 +2035,18 @@ SchemaBuilderEntity.prototype.query = function(options, callback, controller) { if (!err || callback.success) return; callback.success = true; - builder.push(err); + + if (builder !== err) + builder.push(err); + self.onError && self.onError(builder, EMPTYOBJECT, $type); callback(builder); }; var onCallback = function(res) { + CONF.logger && F.ilogger(self.getLoggerName($type), controller, $now); + if (callback.success) return; @@ -1225,15 +2056,19 @@ SchemaBuilderEntity.prototype.query = function(options, callback, controller) { res = arguments[1]; } - var has = builder.hasError(); + var has = builder.is; has && self.onError && self.onError(builder, EMPTYOBJECT, $type); callback.success = true; - callback(builder.hasError() ? builder : null, res); + callback(builder.is ? builder : null, res); }; - if (self.onQuery.$newversion) - async.call(self, self.onQuery)(onError, new SchemaOptions(builder, undefined, options, onCallback, controller)); - else + if (self.onQuery.$newversion) { + var opt = new SchemaOptions(builder, undefined, options, onCallback, controller, $type, self); + if (self.middlewares && self.middlewares.length) + runmiddleware(opt, self, () => async.call(self, self.onQuery)(onError, opt)); + else + async.call(self, self.onQuery)(onError, opt); + } else async.call(self, self.onQuery)(onError, builder, options, onCallback, controller); return self; @@ -1247,7 +2082,7 @@ SchemaBuilderEntity.prototype.query = function(options, callback, controller) { * @param {ErrorBuilder} builder ErrorBuilder, INTERNAL. * @return {ErrorBuilder} */ -SchemaBuilderEntity.prototype.validate = function(model, resourcePrefix, resourceName, builder, filter, path, index) { +SchemaBuilderEntityProto.validate = function(model, resourcePrefix, resourceName, builder, filter, path, index) { var self = this; @@ -1277,38 +2112,7 @@ SchemaBuilderEntity.prototype.validate = function(model, resourcePrefix, resourc else path = ''; - framework_utils.validate_builder.call(self, model, builder, self.name, self.parent.collection, self.name, index, filter, path); - - /* - if (!self.dependencies) - return builder; - - for (var i = 0, length = self.dependencies.length; i < length; i++) { - var key = self.dependencies[i]; - var schema = self.schema[key]; - var s = self.parent.collection[schema.raw]; - - if (!s) { - F.error(new Error('Schema "{0}" not found (validation).'.format(schema.raw))); - continue; - } - - - if (schema.isArray) { - var arr = model[key]; - for (var j = 0, jl = arr.length; j < jl; j++) { - if (model[key][j] != null || schema.required) { - if (!schema.can || schema.can(model)) - s.validate(model[key][j], resourcePrefix, resourceName, builder, filter, path + key + '[' + j + ']', j); - } - } - } else if (model[key] != null || schema.required) { - if (!schema.can || schema.can(model)) - s.validate(model[key], resourcePrefix, resourceName, builder, filter, path + key, -1); - } - } - */ - + framework_utils.validate_builder.call(self, model, builder, self, '', index, filter, path); return builder; }; @@ -1317,11 +2121,11 @@ SchemaBuilderEntity.prototype.validate = function(model, resourcePrefix, resourc * @alias SchemaBuilderEntity.default() * @return {Object} */ -SchemaBuilderEntity.prototype.create = function() { +SchemaBuilderEntityProto.create = function() { return this.default(); }; -SchemaBuilderEntity.prototype.Create = function() { +SchemaBuilderEntityProto.Create = function() { return this.default(); }; @@ -1330,11 +2134,11 @@ SchemaBuilderEntity.prototype.Create = function() { * @param {Object} obj * @return {Object} */ -SchemaBuilderEntity.prototype.$make = function(obj) { - return obj; // TODO remove +SchemaBuilderEntityProto.$make = function(obj) { + return obj; }; -SchemaBuilderEntity.prototype.$prepare = function(obj, callback) { +SchemaBuilderEntityProto.$prepare = function(obj, callback) { if (obj && typeof(obj.$save) === 'function') callback(null, obj); else @@ -1346,7 +2150,7 @@ SchemaBuilderEntity.prototype.$prepare = function(obj, callback) { * Create a default object according the schema * @return {SchemaInstance} */ -SchemaBuilderEntity.prototype.default = function() { +SchemaBuilderEntityProto.default = function() { var obj = this.schema; if (obj === null) @@ -1367,11 +2171,18 @@ SchemaBuilderEntity.prototype.default = function() { } } + if (type.def !== undefined) { + item[property] = typeof(type.def) === 'function' ? type.def() : type.def; + continue; + } + switch (type.type) { // undefined // object + // object: convertor case 0: case 6: + case 12: item[property] = type.isArray ? [] : null; break; // numbers: integer, float @@ -1379,9 +2190,13 @@ SchemaBuilderEntity.prototype.default = function() { case 2: item[property] = type.isArray ? [] : 0; break; + // numbers: default "null" + case 10: + item[property] = type.isArray ? [] : null; + break; // string case 3: - item[property] = type.isArray ? [] : ''; + item[property] = type.isArray ? [] : type.subtype === 'email' ? '@' : ''; break; // boolean case 4: @@ -1389,7 +2204,7 @@ SchemaBuilderEntity.prototype.default = function() { break; // date case 5: - item[property] = type.isArray ? [] : F.datetime; + item[property] = type.isArray ? [] : NOW; break; // schema case 7: @@ -1397,7 +2212,7 @@ SchemaBuilderEntity.prototype.default = function() { if (type.isArray) { item[property] = []; } else { - var tmp = this.find(type.raw); + var tmp = this.parent.collection[type.raw] || GETSCHEMA(type.raw); if (tmp) { item[property] = tmp.default(); } else { @@ -1406,6 +2221,7 @@ SchemaBuilderEntity.prototype.default = function() { } } break; + // enum + keyvalue case 8: case 9: @@ -1417,69 +2233,211 @@ SchemaBuilderEntity.prototype.default = function() { return item; }; -/** - * Create schema instance - * @param {function|object} model - * @param [filter] - * @param [callback] - * @returns {SchemaInstance} - */ -SchemaBuilderEntity.prototype.make = function(model, filter, callback, argument, novalidate) { - - if (typeof(model) === 'function') { - model.call(this, this); - return this; - } +function SchemaOptionsVerify(controller, builder) { + var t = this; + t.controller = (controller instanceof SchemaOptions || controller instanceof OperationOptions) ? controller.controller : controller; + t.callback = t.next = t.success = function(value) { + if (value !== undefined) + t.model[t.name] = value; + t.cache && CACHE(t.cachekey, { value: t.model[t.name] }, t.cache); + t.$next(); + }; + t.invalid = function(err) { + if (err) { + builder.push(err); + t.cache && CACHE(t.cachekey, { error: err }, t.cache); + } + t.model[t.name] = null; + t.$next(); + }; +} - if (typeof(filter) === 'function') { - var tmp = callback; - callback = filter; - filter = tmp; - } +SchemaOptionsVerify.prototype = { - var output = this.prepare(model); + get user() { + return this.controller ? this.controller.user : null; + }, - if (novalidate) { - callback && callback(null, output, argument); - return output; - } + get session() { + return this.controller ? this.controller.session : null; + }, - var builder = this.validate(output, undefined, undefined, undefined, filter); - if (builder.hasError()) { - this.onError && this.onError(builder, model, 'make'); - callback && callback(builder, null, argument); - return output; - } + get sessionid() { + return this.controller && this.controller ? this.controller.req.sessionid : null; + }, - callback && callback(null, output, argument); - return output; -}; + get language() { + return (this.controller ? this.controller.language : '') || ''; + }, -SchemaBuilderEntity.prototype.load = SchemaBuilderEntity.prototype.make; // Because JSDoc doesn't work with double asserting + get ip() { + return this.controller ? this.controller.ip : null; + }, + + get id() { + return this.controller ? this.controller.id : null; + }, + + get req() { + return this.controller ? this.controller.req : null; + }, + + get res() { + return this.controller ? this.controller.res : null; + }, + + get params() { + return this.controller ? this.controller.params : null; + }, + + get files() { + return this.controller ? this.controller.files : null; + }, + + get body() { + return this.controller ? this.controller.body : null; + }, + + get query() { + return this.controller ? this.controller.query : null; + }, + + get headers() { + return this.controller && this.controller.req ? this.controller.req.headers : null; + }, + + get ua() { + return this.controller && this.controller.req ? this.controller.req.ua : null; + } +}; + +/** + * Create schema instance + * @param {function|object} model + * @param [filter] + * @param [callback] + * @returns {SchemaInstance} + */ +SchemaBuilderEntityProto.make = function(model, filter, callback, argument, novalidate, workflow, req) { + + var self = this; + + if (typeof(model) === 'function') { + model.call(self, self); + return self; + } + + if (typeof(filter) === 'function') { + var tmp = callback; + callback = filter; + filter = tmp; + } + + var verifications = []; + var output = self.prepare(model, null, req, verifications); + + if (workflow) + output.$$workflow = workflow; + + if (novalidate) { + callback && callback(null, output, argument); + return output; + } + + var builder = self.validate(output, undefined, undefined, undefined, filter); + + if (builder.is) { + self.onError && self.onError(builder, model, 'make'); + callback && callback(builder, null, argument); + return output; + } else { + + if (self.verifications) + verifications.unshift({ model: output, entity: self }); + + if (!verifications.length) { + callback && callback(null, output, argument); + return output; + } + + var options = new SchemaOptionsVerify(req, builder); + + verifications.wait(function(item, next) { + + item.entity.verifications.wait(function(verify, resume) { + + options.value = item.model[verify.name]; + + // Empty values are skipped + if (options.value == null || options.value === '') { + resume(); + return; + } + + var cachekey = verify.cachekey; + + if (cachekey) { + cachekey += options.value + ''; + var cachevalue = F.cache.get2(cachekey); + if (cachevalue) { + if (cachevalue.error) + builder.push(cachevalue.error); + else + item.model[verify.name] = cachevalue.value; + resume(); + return; + } + } + + options.cache = verify.cache; + options.cachekey = cachekey; + options.entity = item.entity; + options.model = item.model; + options.name = verify.name; + options.$next = resume; + verify.fn(options); + + }, next, 3); // "3" means count of imaginary "threads" - we will see how it will work + + }, function() { + if (builder.is) { + self.onError && self.onError(builder, model, 'make'); + callback && callback(builder, null, argument); + } else + callback && callback(null, output, argument); + }); + + } +}; + +SchemaBuilderEntityProto.load = SchemaBuilderEntityProto.make; // Because JSDoc doesn't work with double asserting function autotrim(context, value) { return context.trim ? value.trim() : value; } -SchemaBuilderEntity.prototype.$onprepare = function(name, value, index, model) { +SchemaBuilderEntityProto.$onprepare = function(name, value, index, model, req) { var val = value; if (this.$onPrepare) { for (var i = 0, length = this.$onPrepare.length; i < length; i++) { - var tmp = this.$onPrepare[i](name, val, index, model); + var tmp = this.$onPrepare[i](name, val, index, model, req); if (tmp !== undefined) val = tmp; } } if (this.onPrepare) - val = this.onPrepare(name, val, index, model); + val = this.onPrepare(name, val, index, model, req); + + if (this.preparation && this.preparation[name]) + val = this.preparation[name](val, model, index, req); return val === undefined ? value : val; }; -SchemaBuilderEntity.prototype.$ondefault = function(property, create, entity) { +SchemaBuilderEntityProto.$ondefault = function(property, create, entity) { var val; @@ -1504,7 +2462,7 @@ SchemaBuilderEntity.prototype.$ondefault = function(property, create, entity) { * @param {String|Array} [dependencies] INTERNAL. * @return {SchemaInstance} */ -SchemaBuilderEntity.prototype.prepare = function(model, dependencies) { +SchemaBuilderEntityProto.prepare = function(model, dependencies, req, verifications) { var self = this; var obj = self.schema; @@ -1519,22 +2477,36 @@ SchemaBuilderEntity.prototype.prepare = function(model, dependencies) { var entity; var item = new self.CurrentSchemaInstance(); var defaults = self.onDefault || self.$onDefault ? true : false; + var keys = req && req.$patch ? [] : null; for (var property in obj) { var val = model[property]; + if (req && req.$patch && val === undefined) { + delete item[property]; + continue; + } + + var type = obj[property]; + keys && keys.push(property); + // IS PROTOTYPE? The problem was in e.g. "search" property, because search is in String prototypes. if (!hasOwnProperty.call(model, property)) val = undefined; - if (val === undefined && defaults) - val = self.$ondefault(property, false, self.name); + var def = type.def && typeof(type.def) === 'function'; + + if (val === undefined) { + if (type.def !== undefined) + val = def ? type.def() : type.def; + else if (defaults) + val = self.$ondefault(property, false, self.name); + } if (val === undefined) val = ''; - var type = obj[property]; var typeval = typeof(val); if (typeval === 'function') @@ -1548,11 +2520,11 @@ SchemaBuilderEntity.prototype.prepare = function(model, dependencies) { break; // number: integer case 1: - item[property] = self.$onprepare(property, framework_utils.parseInt(val), undefined, model); + item[property] = self.$onprepare(property, framework_utils.parseInt(val, def ? type.def() : type.def), undefined, model, req); break; // number: float case 2: - item[property] = self.$onprepare(property, framework_utils.parseFloat(val), undefined, model); + item[property] = self.$onprepare(property, framework_utils.parseFloat(val, def ? type.def() : type.def), undefined, model, req); break; // string @@ -1585,6 +2557,7 @@ SchemaBuilderEntity.prototype.prepare = function(model, dependencies) { tmp = ''; break; case 'zip': + tmp = tmp.replace(REGEXP_CLEAN_EMAIL, ''); if (tmp && !type.required && !tmp.isZIP()) tmp = ''; break; @@ -1596,6 +2569,9 @@ SchemaBuilderEntity.prototype.prepare = function(model, dependencies) { case 'capitalize': tmp = tmp.capitalize(); break; + case 'capitalize2': + tmp = tmp.capitalize(true); + break; case 'lowercase': tmp = tmp.toLowerCase(); break; @@ -1606,15 +2582,24 @@ SchemaBuilderEntity.prototype.prepare = function(model, dependencies) { if (tmp && !type.required && !tmp.isJSON()) tmp = ''; break; + case 'base64': + if (tmp && !type.required && !tmp.isBase64()) + tmp = ''; + break; } - item[property] = self.$onprepare(property, tmp, undefined, model); + if (!tmp && type.def !== undefined) + tmp = def ? type.def() : type.def; + + item[property] = self.$onprepare(property, tmp, undefined, model, req); break; // boolean case 4: tmp = val ? val.toString().toLowerCase() : null; - item[property] = self.$onprepare(property, tmp === 'true' || tmp === '1' || tmp === 'on', undefined, model); + if (type.def && (tmp == null || tmp === '')) + tmp = def ? type.def() : type.def; + item[property] = self.$onprepare(property, typeof(tmp) === 'string' ? !!BOOL[tmp] : tmp == null ? false : tmp, undefined, model, req); break; // date @@ -1631,38 +2616,50 @@ SchemaBuilderEntity.prototype.prepare = function(model, dependencies) { tmp = val; if (framework_utils.isDate(tmp)) - tmp = self.$onprepare(property, tmp, undefined, model); - else - tmp = (defaults ? isUndefined(self.$ondefault(property, false, self.name), null) : null); + tmp = self.$onprepare(property, tmp, undefined, model, req); + else { + if (type.def !== undefined) + tmp = def ? type.def() : type.def; + else + tmp = (defaults ? isUndefined(self.$ondefault(property, false, self.name), null) : null); + } item[property] = tmp; break; // object case 6: - item[property] = self.$onprepare(property, model[property], undefined, model); + // item[property] = self.$onprepare(property, model[property], undefined, model, req); + item[property] = self.$onprepare(property, val, undefined, model, req); + if (item[property] === undefined) + item[property] = null; break; // enum case 8: - tmp = self.$onprepare(property, model[property], undefined, model); + // tmp = self.$onprepare(property, model[property], undefined, model, req); + tmp = self.$onprepare(property, val, undefined, model, req); if (type.subtype === 'number' && typeof(tmp) === 'string') tmp = tmp.parseFloat(null); item[property] = tmp != null && type.raw.indexOf(tmp) !== -1 ? tmp : undefined; + if (item[property] == null && type.def) + item[property] = type.def; break; // keyvalue case 9: - tmp = self.$onprepare(property, model[property], undefined, model); + // tmp = self.$onprepare(property, model[property], undefined, model, req); + tmp = self.$onprepare(property, val, undefined, model, req); item[property] = tmp != null ? type.raw[tmp] : undefined; + if (item[property] == null && type.def) + item[property] = type.def; break; // schema case 7: if (!val) { - val = (defaults ? isUndefined(self.$ondefault(property, false, self.name), null) : null); - // val = defaults(property, false, self.name); + val = (type.def === undefined ? defaults ? isUndefined(self.$ondefault(property, false, self.name), null) : null : (def ? type.def() : type.def)); if (val === null) { item[property] = null; break; @@ -1677,20 +2674,45 @@ SchemaBuilderEntity.prototype.prepare = function(model, dependencies) { } } - entity = self.parent.get(type.raw); + entity = GETSCHEMA(type.raw); if (entity) { - item[property] = entity.prepare(val, undefined); - dependencies && dependencies.push({ name: type.raw, value: self.$onprepare(property, item[property], undefined, model) }); + + item[property] = entity.prepare(val, undefined, req, verifications); + item[property].$$parent = item; + item[property].$$controller = req; + + if (entity.verifications) + verifications.push({ model: item[property], entity: entity }); + + dependencies && dependencies.push({ name: type.raw, value: self.$onprepare(property, item[property], undefined, model, req) }); } else item[property] = null; + + break; + + case 10: + item[property] = type.raw(val == null ? '' : val.toString()); + if (item[property] === undefined) + item[property] = null; + break; + + // number: nullable + case 11: + item[property] = self.$onprepare(property, typeval === 'number' ? val : typeval === 'string' ? parseNumber(val) : null, undefined, model, req); + break; + + // object: convertor + case 12: + item[property] = self.$onprepare(property, val && typeval === 'object' && !(val instanceof Array) ? CONVERT(val, type.raw) : null, undefined, model, req); break; + } continue; } // ARRAY: if (!(val instanceof Array)) { - item[property] = (defaults ? isUndefined(self.$ondefault(property, false, self.name), []) : []); + item[property] = (type.def === undefined ? defaults ? isUndefined(self.$ondefault(property, false, self.name), EMPTYARRAY) : [] : (def ? type.def() : type.def)); continue; } @@ -1703,15 +2725,15 @@ SchemaBuilderEntity.prototype.prepare = function(model, dependencies) { switch (type.type) { case 0: - tmp = self.$onprepare(property, tmp, j, model); + tmp = self.$onprepare(property, tmp, j, model, req); break; case 1: - tmp = self.$onprepare(property, framework_utils.parseInt(tmp), j, model); + tmp = self.$onprepare(property, framework_utils.parseInt(tmp), j, model, req); break; case 2: - tmp = self.$onprepare(property, framework_utils.parseFloat(tmp), j, model); + tmp = self.$onprepare(property, framework_utils.parseFloat(tmp), j, model, req); break; case 3: @@ -1742,6 +2764,9 @@ SchemaBuilderEntity.prototype.prepare = function(model, dependencies) { case 'capitalize': tmp = tmp.capitalize(); break; + case 'capitalize2': + tmp = tmp.capitalize(true); + break; case 'lowercase': tmp = tmp.toLowerCase(); break; @@ -1752,15 +2777,19 @@ SchemaBuilderEntity.prototype.prepare = function(model, dependencies) { if (tmp && !type.required && !tmp.isJSON()) continue; break; + case 'base64': + if (tmp && !type.required && !tmp.isBase64()) + continue; + break; } - tmp = self.$onprepare(property, tmp, j, model); + tmp = self.$onprepare(property, tmp, j, model, req); break; case 4: if (tmp) tmp = tmp.toString().toLowerCase(); - tmp = self.$onprepare(property, tmp === 'true' || tmp === '1' || tmp === 'on', j, model); + tmp = self.$onprepare(property, BOOL[tmp], j, model, req); break; case 5: @@ -1772,33 +2801,50 @@ SchemaBuilderEntity.prototype.prepare = function(model, dependencies) { tmp = new Date(tmp); if (framework_utils.isDate(tmp)) - tmp = self.$onprepare(property, tmp, j, model); + tmp = self.$onprepare(property, tmp, j, model, req); else tmp = undefined; break; case 6: - tmp = self.$onprepare(property, tmp, j, model); + tmp = self.$onprepare(property, tmp, j, model, req); break; case 7: - entity = self.parent.get(type.raw); + + entity = self.parent.collection[type.raw] || GETSCHEMA(type.raw); + if (entity) { - tmp = entity.prepare(tmp, dependencies); - if (dependencies) - dependencies.push({ name: type.raw, value: self.$onprepare(property, tmp, j, model) }); + tmp = entity.prepare(tmp, dependencies, req, verifications); + tmp.$$parent = item; + tmp.$$controller = req; + dependencies && dependencies.push({ name: type.raw, value: self.$onprepare(property, tmp, j, model, req) }); } else - tmp = null; + throw new Error('Schema "{0}" not found'.format(type.raw)); + + tmp = self.$onprepare(property, tmp, j, model, req); + + if (entity.verifications && tmp) + verifications.push({ model: tmp, entity: entity }); - tmp = self.$onprepare(property, tmp, j, model); break; - } - if (tmp === undefined) - continue; + case 11: + tmp = self.$onprepare(property, typeval === 'number' ? tmp : typeval === 'string' ? parseNumber(tmp) : null, j, model, req); + if (tmp == null) + continue; + break; + + case 12: + tmp = self.$onprepare(property, tmp ? CONVERT(tmp, type.raw) : null, j, model, req); + if (tmp == null) + continue; + break; + } - item[property].push(tmp); + if (tmp !== undefined) + item[property].push(tmp); } } @@ -1806,14 +2852,28 @@ SchemaBuilderEntity.prototype.prepare = function(model, dependencies) { for (var i = 0, length = self.fields_allow.length; i < length; i++) { var name = self.fields_allow[i]; var val = model[name]; - if (val !== undefined) + if (val !== undefined) { item[name] = val; + keys && keys.push(name); + } } } + if (keys) + item.$$keys = keys; + return item; }; +function parseNumber(str) { + if (!str) + return null; + if (str.indexOf(',') !== -1) + str = str.replace(',', '.'); + var num = +str; + return isNaN(num) ? null : num; +} + /** * Transform an object * @param {String} name @@ -1824,13 +2884,14 @@ SchemaBuilderEntity.prototype.prepare = function(model, dependencies) { * @param {Object} controller Optional * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.transform = function(name, model, options, callback, skip, controller) { +SchemaBuilderEntityProto.transform = function(name, model, options, callback, skip, controller) { return this.$execute('transform', name, model, options, callback, skip, controller); }; -SchemaBuilderEntity.prototype.transform2 = function(name, options, callback, controller) { +SchemaBuilderEntityProto.transform2 = function(name, options, callback, controller) { if (typeof(options) === 'function') { + controller = callback; callback = options; options = undefined; } @@ -1839,7 +2900,7 @@ SchemaBuilderEntity.prototype.transform2 = function(name, options, callback, con return this.transform(name, this.create(), options, callback, true, controller); }; -SchemaBuilderEntity.prototype.$process = function(arg, model, type, name, builder, response, callback) { +SchemaBuilderEntityProto.$process = function(arg, model, type, name, builder, response, callback, controller) { var self = this; @@ -1849,17 +2910,24 @@ SchemaBuilderEntity.prototype.$process = function(arg, model, type, name, builde response = arg[1]; } - var has = builder.hasError(); + var has = builder.is; has && self.onError && self.onError(builder, model, type, name); - callback(has ? builder : null, response === undefined ? model : response, model); + + if (response !== NoOp) { + if (controller && response instanceof SchemaInstance && !response.$$controller) + response.$$controller = controller; + callback(has ? builder : null, response === undefined ? model : response, model); + } else + callback = null; + return self; }; -SchemaBuilderEntity.prototype.$process_hook = function(model, type, name, builder, result, callback) { +SchemaBuilderEntityProto.$process_hook = function(model, type, name, builder, result, callback) { var self = this; - var has = builder.hasError(); + var has = builder.is; has && self.onError && self.onError(builder, model, type, name); - callback(has ? builder : null, model); + callback(has ? builder : null, model, result); return self; }; @@ -1873,13 +2941,14 @@ SchemaBuilderEntity.prototype.$process_hook = function(model, type, name, builde * @param {Object} controller Optional * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.workflow = function(name, model, options, callback, skip, controller) { +SchemaBuilderEntityProto.workflow = function(name, model, options, callback, skip, controller) { return this.$execute('workflow', name, model, options, callback, skip, controller); }; -SchemaBuilderEntity.prototype.workflow2 = function(name, options, callback, controller) { +SchemaBuilderEntityProto.workflow2 = function(name, options, callback, controller) { if (typeof(options) === 'function') { + controller = callback; callback = options; options = undefined; } @@ -1897,7 +2966,7 @@ SchemaBuilderEntity.prototype.workflow2 = function(name, options, callback, cont * @param {Boolean} skip Skips preparing and validation, optional. * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.hook = function(name, model, options, callback, skip, controller) { +SchemaBuilderEntityProto.hook = function(name, model, options, callback, skip, controller) { var self = this; @@ -1923,36 +2992,50 @@ SchemaBuilderEntity.prototype.hook = function(name, model, options, callback, sk var hook = self.hooks ? self.hooks[name] : undefined; if (!hook || !hook.length) { - callback(null, model); + callback(null, model, EMPTYARRAY); return self; } + if (controller instanceof SchemaOptions || controller instanceof OperationOptions) + controller = controller.controller; + if (model && !controller && model.$$controller) controller = model.$$controller; var $type = 'hook'; - if (skip === true) { - var builder = new ErrorBuilder(); + if (skip === true || model instanceof SchemaInstance) { + var builder = new ErrorBuilder(); self.resourceName && builder.setResource(self.resourceName); self.resourcePrefix && builder.setPrefix(self.resourcePrefix); var output = []; + var $now; + + if (CONF.logger) + $now = Date.now(); async_wait(hook, function(item, next) { + if (item.fn.$newversion) { - if (item.fn.$newversion) - item.fn.call(self, new SchemaOptions(builder, model, options, function(result) { + var opt = new SchemaOptions(builder, model, options, function(result) { output.push(result == undefined ? model : result); next(); - }, controller)); - else + }, controller, 'hook.' + name, self); + + if (self.middlewares && self.middlewares.length) + runmiddleware(opt, self, item.fn); + else + item.fn.call(self, opt); + + } else item.fn.call(self, builder, model, options, function(result) { output.push(result == undefined ? model : result); next(); }, controller, skip !== true); }, function() { + CONF.logger && F.ilogger(self.getLoggerName($type, name), controller, $now); self.$process_hook(model, $type, name, builder, output, callback); }, 0); @@ -1968,58 +3051,74 @@ SchemaBuilderEntity.prototype.hook = function(name, model, options, callback, sk var builder = new ErrorBuilder(); var output = []; + var $now; self.resourceName && builder.setResource(self.resourceName); self.resourcePrefix && builder.setPrefix(self.resourcePrefix); + if (CONF.logger) + $now = Date.now(); + async_wait(hook, function(item, next, index) { if (!isGenerator(self, 'hook.' + name + '.' + index, item.fn)) { - if (item.fn.$newversion) + if (item.fn.$newversion) { item.fn.call(self, new SchemaOptions(builder, model, options, function(res) { + CONF.logger && F.ilogger(self.getLoggerName($type, name), controller, $now); output.push(res === undefined ? model : res); next(); - }, controller)); - else + }, controller, 'hook.' + name, self)); + } else { item.fn.call(self, builder, model, options, function(res) { + CONF.logger && F.ilogger(self.getLoggerName($type, name), controller, $now); output.push(res === undefined ? model : res); next(); }, controller, skip !== true); + } return; } callback.success = false; - if (item.fn.$newversion) + if (item.fn.$newversion) { + var opt = new SchemaOptions(builder, model, options, function(res) { + CONF.logger && F.ilogger(self.getLoggerName($type, name), controller, $now); + output.push(res == undefined ? model : res); + next(); + }, controller, 'hook.' + name, self); + async.call(self, item.fn)(function(err) { if (!err) return; - builder.push(err); - next(); - }, new SchemaOptions(builder, model, options, function(res) { - output.push(res == undefined ? model : res); + if (builder !== err) + builder.push(err); next(); - }, controller)); - else + }, opt); + + } else { async.call(self, item.fn)(function(err) { if (!err) return; - builder.push(err); + if (builder !== err) + builder.push(err); next(); }, builder, model, options, function(res) { + CONF.logger && F.ilogger(self.getLoggerName($type, name), controller, $now); output.push(res == undefined ? model : res); next(); }, controller, skip !== true); + } }, () => self.$process_hook(model, $type, name, builder, output, callback), 0); - }); + }, controller ? controller.req : null); return self; }; -SchemaBuilderEntity.prototype.hook2 = function(name, options, callback, controller) { +SchemaBuilderEntityProto.hook2 = function(name, options, callback, controller) { if (typeof(options) === 'function') { + controller = callback; callback = options; options = undefined; } @@ -2030,7 +3129,7 @@ SchemaBuilderEntity.prototype.hook2 = function(name, options, callback, controll return this.hook(name, this.create(), options, callback, true, controller); }; -SchemaBuilderEntity.prototype.$execute = function(type, name, model, options, callback, skip, controller) { +SchemaBuilderEntityProto.$execute = function(type, name, model, options, callback, skip, controller) { var self = this; if (typeof(name) !== 'string') { @@ -2054,26 +3153,50 @@ SchemaBuilderEntity.prototype.$execute = function(type, name, model, options, ca var ref = self[type + 's']; var item = ref ? ref[name] : undefined; + var $now; if (!item) { callback(new ErrorBuilder().push('', type.capitalize() + ' "{0}" not found.'.format(name))); return self; } + if (CONF.logger) + $now = Date.now(); + + if (controller instanceof SchemaOptions || controller instanceof OperationOptions) + controller = controller.controller; + if (model && !controller && model.$$controller) controller = model.$$controller; - if (skip === true) { + var opfilter = self.meta[type + 'filter#' + name]; + if (opfilter && controller) { + controller.$filterschema = opfilter; + controller.$filter = null; + } + + var key = type + '.' + name; + + if (skip === true || model instanceof SchemaInstance) { var builder = new ErrorBuilder(); self.resourceName && builder.setResource(self.resourceName); self.resourcePrefix && builder.setPrefix(self.resourcePrefix); - if (item.$newversion) - item.call(self, new SchemaOptions(builder, model, options, function(res) { - self.$process(arguments, model, type, name, builder, res, callback); - }, controller)); - else + if (item.$newversion) { + + var opt = new SchemaOptions(builder, model, options, function(res) { + CONF.logger && F.ilogger(self.getLoggerName(type, name), controller, $now); + self.$process(arguments, model, type, name, builder, res, callback, controller); + }, controller, key, self); + + if (self.middlewares && self.middlewares.length) + runmiddleware(opt, self, item); + else + item.call(self, opt); + + } else item.call(self, builder, model, options, function(res) { - self.$process(arguments, model, type, name, builder, res, callback); + CONF.logger && F.ilogger(self.getLoggerName(type, name), controller, $now); + self.$process(arguments, model, type, name, builder, res, callback, controller); }, controller, skip !== true); return self; } @@ -2085,19 +3208,32 @@ SchemaBuilderEntity.prototype.$execute = function(type, name, model, options, ca return; } + if (controller && model instanceof SchemaInstance && !model.$$controller) + model.$$controller = controller; + var builder = new ErrorBuilder(); self.resourceName && builder.setResource(self.resourceName); self.resourcePrefix && builder.setPrefix(self.resourcePrefix); - if (!isGenerator(self, type + '.' + name, item)) { - if (item.$newversion) - item.call(self, new SchemaOptions(builder, model, options, function(res) { - self.$process(arguments, model, type, name, builder, res, callback); - }, controller)); - else + var key = type + '.' + name; + + if (!isGenerator(self, key, item)) { + if (item.$newversion) { + var opt = new SchemaOptions(builder, model, options, function(res) { + CONF.logger && F.ilogger(self.getLoggerName(type, name), controller, $now); + self.$process(arguments, model, type, name, builder, res, callback, controller); + }, controller, key, self); + + if (self.middlewares && self.middlewares.length) + runmiddleware(opt, self, item); + else + item.call(self, opt); + + } else item.call(self, builder, model, options, function(res) { - self.$process(arguments, model, type, name, builder, res, callback); + CONF.logger && F.ilogger(self.getLoggerName(type, name), controller, $now); + self.$process(arguments, model, type, name, builder, res, callback, controller); }, controller); return; } @@ -2108,13 +3244,16 @@ SchemaBuilderEntity.prototype.$execute = function(type, name, model, options, ca if (!err || callback.success) return; callback.success = true; - builder.push(err); + if (builder !== err) + builder.push(err); self.onError && self.onError(builder, model, type, name); callback(builder); }; var onCallback = function(res) { + CONF.logger && F.ilogger(self.getLoggerName(type, name), controller, $now); + if (callback.success) return; @@ -2124,21 +3263,29 @@ SchemaBuilderEntity.prototype.$execute = function(type, name, model, options, ca res = arguments[1]; } - var has = builder.hasError(); + var has = builder.is; has && self.onError && self.onError(builder, model, type, name); callback.success = true; callback(has ? builder : null, res === undefined ? model : res); }; - if (item.$newversion) - async.call(self, item)(onError, new SchemaOptions(builder, model, options, onCallback, controller)); - else + if (item.$newversion) { + var opt = new SchemaOptions(builder, model, options, onCallback, controller, key, self); + if (self.middlewares && self.middlewares.length) + runmiddleware(opt, self, () => async.call(self, item)(onError, opt)); + else + async.call(self, item)(onError, opt); + } else async.call(self, item)(onError, builder, model, options, onCallback, controller); - }); + }, controller ? controller.req : null); return self; }; +SchemaBuilderEntityProto.getLoggerName = function(type, name) { + return this.name + '.' + type + (name ? ('(\'' + name + '\')') : '()'); +}; + /** * Run a workflow * @param {String} name @@ -2149,7 +3296,7 @@ SchemaBuilderEntity.prototype.$execute = function(type, name, model, options, ca * @param {Object} controller Optional * @return {SchemaBuilderEntity} */ -SchemaBuilderEntity.prototype.operation = function(name, model, options, callback, skip, controller) { +SchemaBuilderEntityProto.operation = function(name, model, options, callback, skip, controller) { var self = this; @@ -2191,21 +3338,32 @@ SchemaBuilderEntity.prototype.operation = function(name, model, options, callbac var builder = new ErrorBuilder(); var $type = 'operation'; + var $now; self.resourceName && builder.setResource(self.resourceName); self.resourcePrefix && builder.setPrefix(self.resourcePrefix); + if (controller instanceof SchemaOptions || controller instanceof OperationOptions) + controller = controller.controller; + if (model && !controller && model.$$controller) controller = model.$$controller; - if (!isGenerator(self, 'operation.' + name, operation)) { + if (CONF.logger) + $now = Date.now(); + + var key = $type + '.' + name; + + if (!isGenerator(self, key, operation)) { if (operation.$newversion) { operation.call(self, new SchemaOptions(builder, model, options, function(res) { - self.$process(arguments, model, $type, name, builder, res, callback); - }, controller)); + CONF.logger && F.ilogger(self.getLoggerName($type, name), controller, $now); + self.$process(arguments, model, $type, name, builder, res, callback, controller); + }, controller, key, self)); } else operation.call(self, builder, model, options, function(res) { - self.$process(arguments, model, $type, name, builder, res, callback); + CONF.logger && F.ilogger(self.getLoggerName($type, name), controller, $now); + self.$process(arguments, model, $type, name, builder, res, callback, controller); }, controller, skip !== true); return self; } @@ -2216,13 +3374,16 @@ SchemaBuilderEntity.prototype.operation = function(name, model, options, callbac if (!err || callback.success) return; callback.success = true; - builder.push(err); + if (builder !== err) + builder.push(err); self.onError && self.onError(builder, model, $type, name); callback(builder); }; var onCallback = function(res) { + CONF.logger && F.ilogger(self.getLoggerName($type, name), controller, $now); + if (callback.success) return; @@ -2232,23 +3393,24 @@ SchemaBuilderEntity.prototype.operation = function(name, model, options, callbac res = arguments[1]; } - var has = builder.hasError(); + var has = builder.is; has && self.onError && self.onError(builder, model, $type, name); callback.success = true; callback(has ? builder : null, res); }; if (operation.$newversion) - async.call(self, operation)(onError, new SchemaOptions(builder, model, options, onCallback, controller)); + async.call(self, operation)(onError, new SchemaOptions(builder, model, options, onCallback, controller, key, self)); else async.call(self, operation)(onError, builder, model, options, onCallback, controller, skip !== true); return self; }; -SchemaBuilderEntity.prototype.operation2 = function(name, options, callback, controller) { +SchemaBuilderEntityProto.operation2 = function(name, options, callback, controller) { if (typeof(options) === 'function') { + controller = callback; callback = options; options = undefined; } @@ -2263,7 +3425,7 @@ SchemaBuilderEntity.prototype.operation2 = function(name, options, callback, con * @param {Boolean} isCopied Internal argument. * @return {Object} */ -SchemaBuilderEntity.prototype.clean = function(m) { +SchemaBuilderEntityProto.clean = function(m) { return clone(m); }; @@ -2296,7 +3458,10 @@ function clone(obj) { o[i] = obj[i]; continue; } - o[i] = clone(obj[i]); + if (obj[i] instanceof SchemaInstance) + o[i] = obj[i].$clean(); + else + o[i] = clone(obj[i]); } return o; @@ -2311,11 +3476,16 @@ function clone(obj) { var val = obj[m]; - if (val instanceof SchemaInstance) { + if (val instanceof Array) { o[m] = clone(val); continue; } + if (val instanceof SchemaInstance) { + o[m] = val.$clean(); + continue; + } + var type = typeof(val); if (type !== 'object' || val instanceof Date) { if (type !== 'function') @@ -2323,7 +3493,12 @@ function clone(obj) { continue; } - o[m] = clone(obj[m]); + // Because here can be a problem with MongoDB.ObjectID + // I assume plain/simple model + if (val && val.constructor === Object) + o[m] = clone(obj[m]); + else + o[m] = val; } return o; @@ -2333,10 +3508,91 @@ function clone(obj) { * Returns prototype of instances * @returns {Object} */ -SchemaBuilderEntity.prototype.instancePrototype = function() { +SchemaBuilderEntityProto.instancePrototype = function() { return this.CurrentSchemaInstance.prototype; }; +SchemaBuilderEntityProto.cl = function(name, value) { + var o = this.schema[name]; + if (o && (o.type === 8 || o.type === 9)) { + if (value) + o.raw = value; + return o.raw; + } +}; + +SchemaBuilderEntityProto.props = function() { + + var self = this; + var keys = Object.keys(self.schema); + var prop = {}; + + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var meta = self.schema[key]; + var obj = {}; + + if (meta.required) + obj.required = meta.required; + + if (meta.length) + obj.length = meta.length; + + if (meta.isArray) + meta.array = true; + + switch (meta.type) { + case 1: + case 2: + case 11: + obj.type = 'number'; + break; + case 3: + obj.type = 'string'; + switch (meta.subtype) { + case 'uid': + obj.type = 'uid'; + delete obj.length; + break; + default: + obj.subtype = meta.subtype; + break; + } + break; + + case 4: + obj.type = 'boolean'; + break; + case 5: + obj.type = 'date'; + break; + case 7: + obj.type = 'schema'; + obj.name = meta.raw; + break; + case 8: + obj.type = 'enum'; + obj.items = meta.raw; + break; + case 9: + // obj.type = 'keyvalue'; + obj.type = 'enum'; // because it returns keys only + obj.items = Object.keys(meta.raw); + break; + // case 6: + // case 0: + // case 10: + default: + obj.type = 'object'; + break; + } + + prop[key] = obj; + } + + return prop; +}; + /** * SchemaInstance * @constructor @@ -2352,28 +3608,84 @@ SchemaInstance.prototype.$$schema = null; SchemaInstance.prototype.$async = function(callback, index) { var self = this; !callback && (callback = function(){}); - self.$$async = []; - self.$$result = []; - self.$$index = index; - self.$$callback = callback; - self.$$can = true; - setImmediate(async_continue, self); + + var a = self.$$async = {}; + + a.callback = callback; + a.index = index; + a.indexer = 0; + a.response = []; + a.fn = []; + a.op = []; + a.pending = 0; + + a.next = function() { + a.running = true; + var fn = a.fn ? a.fn.shift() : null; + if (fn) { + a.pending++; + fn.fn(a.done, a.indexer++); + fn.async && a.next(); + } + }; + + a.done = function() { + a.running = false; + a.pending--; + if (a.fn.length) + setImmediate(a.next); + else if (!a.pending && a.callback) + a.callback(null, a.index != null ? a.response[a.index] : a.response); + }; + + setImmediate(a.next); return self; }; -function async_continue(self) { - self.$$can = false; - async_queue(self.$$async, function() { - self.$$callback(null, self.$$index !== undefined ? self.$$result[self.$$index] : self.$$result); - self.$$callback = null; - }); +function async_wait(arr, onItem, onCallback, index) { + var item = arr[index]; + if (item) + onItem(item, () => async_wait(arr, onItem, onCallback, index + 1), index); + else + onCallback(); } -SchemaInstance.prototype.$repository = function(name, value) { +Object.defineProperty(SchemaInstance.prototype, '$parent', { + get: function() { + return this.$$parent; + }, + set: function(value) { + this.$$parent = value; + } +}); - if (this.$$repository === undefined) { - if (value === undefined) - return undefined; +SchemaInstance.prototype.$response = function(index) { + var a = this.$$async; + if (a) { + + if (index == null) + return a.response; + + if (typeof(index) === 'string') { + + if (index === 'prev') + return a.response[a.response.length - 1]; + + index = a.op.indexOf(index); + + if (index !== -1) + return a.response[index]; + + } else + return a.response[index]; + } +}; + +SchemaInstance.prototype.$repository = function(name, value) { + + if (this.$$repository === undefined) { + if (value === undefined) + return undefined; this.$$repository = {}; } @@ -2386,85 +3698,104 @@ SchemaInstance.prototype.$repository = function(name, value) { }; SchemaInstance.prototype.$index = function(index) { - if (typeof(index) === 'string') - this.$$index = (this.$$index || 0).add(index); - this.$$index = index; + var a = this.$$async; + if (a) { + if (typeof(index) === 'string') + a.index = (a.index || 0).add(index); + a.index = index; + } return this; }; SchemaInstance.prototype.$callback = function(callback) { - this.$$callback = callback; + var a = this.$$async; + if (a) + a.callback = callback; return this; }; -SchemaInstance.prototype.$response = SchemaInstance.prototype.$output = function() { - this.$$index = this.$$result.length; +SchemaInstance.prototype.$output = function() { + var a = this.$$async; + if (a) + a.index = true; return this; }; -SchemaInstance.prototype.$push = function(type, name, helper, first) { +SchemaInstance.prototype.$stop = function() { + this.async.length = 0; + return this; +}; - var self = this; - var fn; +const PUSHTYPE1 = { save: 1, insert: 1, update: 1, patch: 1 }; +const PUSHTYPE2 = { query: 1, get: 1, read: 1, remove: 1 }; - if (type === 'save') { +SchemaInstance.prototype.$push = function(type, name, helper, first, async, callback) { - helper = name; - name = undefined; + var self = this; + var fn; - fn = function(next) { + if (PUSHTYPE1[type]) { + fn = function(next, indexer) { self.$$schema[type](self, helper, function(err, result) { - self.$$result && self.$$result.push(err ? null : copy(result)); + var a = self.$$async; + a.response && (a.response[indexer] = err ? null : copy(result)); + if (a.index === true) + a.index = indexer; + callback && callback(err, a.response[indexer]); if (!err) return next(); next = null; - self.$$async = null; - self.$$callback(err, self.$$result); - self.$$callback = null; + a.callback(err, a.response); }, self.$$controller); }; - } else if (type === 'query' || type === 'get' || type === 'read' || type === 'remove') { - - helper = name; - name = undefined; - - fn = function(next) { + } else if (PUSHTYPE2[type]) { + fn = function(next, indexer) { self.$$schema[type](helper, function(err, result) { - self.$$result && self.$$result.push(err ? null : copy(result)); + var a = self.$$async; + a.response && (a.response[indexer] = err ? null : copy(result)); + if (a.index === true) + a.index = indexer; + callback && callback(err, a.response[indexer]); if (!err) return next(); next = null; - self.$$async = null; - self.$$callback(err, self.$$result); - self.$$callback = null; + a.callback(err, a.response); }, self.$$controller); }; - } else { - fn = function(next) { + fn = function(next, indexer) { self.$$schema[type](name, self, helper, function(err, result) { - self.$$result && self.$$result.push(err ? null : copy(result)); + var a = self.$$async; + a.response && (a.response[indexer] = err ? null : copy(result)); + if (a.index === true) + a.index = indexer; + callback && callback(err, a.response[indexer]); if (!err) return next(); next = null; - self.$$async = null; - self.$$callback(err, self.$$result); - self.$$callback = null; + a.callback(err, a.response); }, self.$$controller); }; } - if (first) - self.$$async.unshift(fn); - else - self.$$async.push(fn); + var a = self.$$async; + var obj = { fn: fn, async: async, index: a.length }; + var key = type === 'workflow' || type === 'transform' || type === 'operation' || type === 'hook' ? (type + '.' + name) : type; + + if (first) { + a.fn.unshift(obj); + a.op.unshift(key); + } else { + a.fn.push(obj); + a.op.push(key); + } return self; }; -SchemaInstance.prototype.$next = function(type, name, helper) { - return this.$push(type, name, helper, true); +SchemaInstance.prototype.$next = function(type, name, helper, async) { + return this.$push(type, name, helper, true, async); }; SchemaInstance.prototype.$exec = function(name, helper, callback) { @@ -2491,37 +3822,140 @@ SchemaInstance.prototype.$controller = function(controller) { return this; }; -SchemaInstance.prototype.$save = function(helper, callback) { - if (this.$$can && this.$$async) - this.$push('save', helper); - else +SchemaInstance.prototype.$save = function(helper, callback, async) { + + if (this.$$async && !this.$$async.running) { + if (typeof(helper) === 'function') { + async = callback; + callback = helper; + helper = null; + } else if (callback === true) { + var a = async; + async = true; + callback = a; + } + + this.$push('save', null, helper, null, async, callback); + + } else this.$$schema.save(this, helper, callback, this.$$controller); return this; }; -SchemaInstance.prototype.$query = function(helper, callback) { - if (this.$$can && this.$$async) - this.$push('query', helper); - else +SchemaInstance.prototype.$insert = function(helper, callback, async) { + + if (this.$$async && !this.$$async.running) { + + if (typeof(helper) === 'function') { + async = callback; + callback = helper; + helper = null; + } else if (callback === true) { + var a = async; + async = true; + callback = a; + } + + this.$push('insert', null, helper, null, async, callback); + + } else + this.$$schema.insert(this, helper, callback, this.$$controller); + return this; +}; + +SchemaInstance.prototype.$update = function(helper, callback, async) { + if (this.$$async && !this.$$async.running) { + if (typeof(helper) === 'function') { + async = callback; + callback = helper; + helper = null; + } else if (callback === true) { + var a = async; + async = true; + callback = a; + } + this.$push('update', null, helper, null, async, callback); + } else + this.$$schema.update(this, helper, callback, this.$$controller); + return this; +}; + +SchemaInstance.prototype.$patch = function(helper, callback, async) { + if (this.$$async && !this.$$async.running) { + if (typeof(helper) === 'function') { + async = callback; + callback = helper; + helper = null; + } else if (callback === true) { + var a = async; + async = true; + callback = a; + } + this.$push('patch', null, helper, null, async, callback); + } else + this.$$schema.patch(this, helper, callback, this.$$controller); + return this; +}; + +SchemaInstance.prototype.$query = function(helper, callback, async) { + + if (this.$$async && !this.$$async.running) { + + if (typeof(helper) === 'function') { + async = callback; + callback = helper; + helper = null; + } else if (callback === true) { + var a = async; + async = true; + callback = a; + } + + this.$push('query', null, helper, null, async, callback); + } else this.$$schema.query(this, helper, callback, this.$$controller); + return this; }; -SchemaInstance.prototype.$read = SchemaInstance.prototype.$get = function(helper, callback) { +SchemaInstance.prototype.$read = SchemaInstance.prototype.$get = function(helper, callback, async) { - if (this.$$can && this.$$async) - this.$push('get', helper); - else + if (this.$$async && !this.$$async.running) { + + if (typeof(helper) === 'function') { + async = callback; + callback = helper; + helper = null; + } else if (callback === true) { + var a = async; + async = true; + callback = a; + } + + this.$push('get', null, helper, null, async, callback); + } else this.$$schema.get(this, helper, callback, this.$$controller); return this; }; -SchemaInstance.prototype.$delete = SchemaInstance.prototype.$remove = function(helper, callback) { +SchemaInstance.prototype.$delete = SchemaInstance.prototype.$remove = function(helper, callback, async) { - if (this.$$can && this.$$async) - this.$push('remove', helper); - else + if (this.$$async && !this.$$async.running) { + + if (typeof(helper) === 'function') { + async = callback; + callback = helper; + helper = null; + } else if (callback === true) { + var a = async; + async = true; + callback = a; + } + + this.$push('remove', null, helper, null, async, callback); + + } else this.$$schema.remove(helper, callback, this.$$controller); return this; @@ -2535,41 +3969,88 @@ SchemaInstance.prototype.$destroy = function() { return this.$$schema.destroy(); }; -SchemaInstance.prototype.$transform = function(name, helper, callback) { +SchemaInstance.prototype.$transform = function(name, helper, callback, async) { - if (this.$$can && this.$$async) - this.$push('transform', name, helper); - else + if (this.$$async && !this.$$async.running) { + + if (typeof(helper) === 'function') { + async = callback; + callback = helper; + helper = null; + } else if (callback === true) { + var a = async; + async = true; + callback = a; + } + + this.$push('transform', name, helper, null, async, callback); + + } else this.$$schema.transform(name, this, helper, callback, undefined, this.$$controller); return this; }; -SchemaInstance.prototype.$workflow = function(name, helper, callback) { +SchemaInstance.prototype.$workflow = function(name, helper, callback, async) { - if (this.$$can && this.$$async) - this.$push('workflow', name, helper); - else + if (this.$$async && !this.$$async.running) { + + if (typeof(helper) === 'function') { + async = callback; + callback = helper; + helper = null; + } else if (callback === true) { + var a = async; + async = true; + callback = a; + } + + this.$push('workflow', name, helper, null, async, callback); + + } else this.$$schema.workflow(name, this, helper, callback, undefined, this.$$controller); return this; }; -SchemaInstance.prototype.$hook = function(name, helper, callback) { +SchemaInstance.prototype.$hook = function(name, helper, callback, async) { - if (this.$$can && this.$$async) - this.$push('hook', name, helper); - else + if (this.$$async && !this.$$async.running) { + + if (typeof(helper) === 'function') { + async = callback; + callback = helper; + helper = null; + } else if (callback === true) { + var a = async; + async = true; + callback = a; + } + + this.$push('hook', name, helper, null, async, callback); + + } else this.$$schema.hook(name, this, helper, callback, undefined, this.$$controller); return this; }; -SchemaInstance.prototype.$operation = function(name, helper, callback) { +SchemaInstance.prototype.$operation = function(name, helper, callback, async) { - if (this.$$can && this.$$async) - this.$push('operation', name, helper); - else + if (this.$$async && !this.$$async.running) { + + if (typeof(helper) === 'function') { + async = callback; + callback = helper; + helper = null; + } else if (callback === true) { + var a = async; + async = true; + callback = a; + } + + this.$push('operation', name, helper, null, async, callback); + } else this.$$schema.operation(name, this, helper, callback, undefined, this.$$controller); return this; @@ -2609,20 +4090,23 @@ SchemaInstance.prototype.$constant = function(name) { function ErrorBuilder(onResource) { this.items = []; - this.transformName = transforms['error_default']; + this.transformName = transforms.error_default; this.onResource = onResource; - this.resourceName = F.config['default-errorbuilder-resource-name']; - this.resourcePrefix = F.config['default-errorbuilder-resource-prefix'] || ''; + this.resourceName = CONF.default_errorbuilder_resource_name; + this.resourcePrefix = CONF.default_errorbuilder_resource_prefix || ''; this.isResourceCustom = false; this.count = 0; this.replacer = []; this.isPrepared = false; this.contentType = 'application/json'; - this.status = F.config['default-errorbuilder-status'] || 200; + this.status = CONF.default_errorbuilder_status || 200; // Hidden: when the .push() contains a classic Error instance // this.unexpected; + // A default path for .push() + // this.path; + !onResource && this._resource(); } @@ -2664,24 +4148,30 @@ global.EACHSCHEMA = exports.eachschema = function(group, fn) { } }; -exports.getschema = function(group, name, fn, timeout) { +global.$$$ = global.GETSCHEMA = exports.getschema = function(group, name, fn, timeout) { if (!name || typeof(name) === 'function') { + timeout = fn; fn = name; - name = group; - group = DEFAULT_SCHEMA; + } else + group = group + '/' + name; + + if (schemacache[group]) + group = schemacache[group]; + else { + if (group.indexOf('/') === -1) + group = DEFAULT_SCHEMA + '/' + group; + group = schemacache[group] = group.toLowerCase(); } - if (fn) { - framework_utils.wait(function() { - var g = schemas[group]; - return g && g.get(name) ? true : false; - }, err => fn(err, schemas[group].get(name)), timeout || 20000); - return; - } + if (fn) + framework_utils.wait(() => !!schemasall[group], err => fn(err, schemasall[group]), timeout || 20000); + else + return schemasall[group]; +}; - var g = schemas[group]; - return g ? g.get(name) : undefined; +exports.findschema = function(groupname) { + return schemasall[groupname.toLowerCase()]; }; exports.newschema = function(group, name) { @@ -2693,7 +4183,11 @@ exports.newschema = function(group, name) { schemas[group] = new SchemaBuilder(group); var o = schemas[group].create(name); + var key = group + '/' + name; + o.owner = F.$owner(); + schemasall[key.toLowerCase()] = o; + return o; }; @@ -2704,10 +4198,23 @@ exports.newschema = function(group, name) { */ exports.remove = function(group, name) { if (name) { + var g = schemas[group || DEFAULT_SCHEMA]; g && g.remove(name); - } else + var key = ((group || DEFAULT_SCHEMA) + '/' + name).toLowerCase(); + delete schemasall[key]; + + } else { + delete schemas[group]; + + var lower = group.toLowerCase(); + + Object.keys(schemasall).forEach(function(key) { + if (key.substring(0, group.length) === lower) + delete schemasall[key]; + }); + } }; global.EACHOPERATION = function(fn) { @@ -2824,7 +4331,7 @@ exports.prepare = function(name, model) { }; function isUndefined(value, def) { - return value === undefined ? def : value; + return value === undefined ? (def === EMPTYARRAY ? [] : def) : value; } // ====================================================== @@ -2843,6 +4350,14 @@ ErrorBuilder.prototype = { var self = this; !self.isPrepared && self.prepare(); return self._transform(); + }, + + get is() { + return this.items.length > 0; + }, + + get length() { + return this.items.length; } }; @@ -2891,7 +4406,7 @@ ErrorBuilder.prototype._resource = function() { ErrorBuilder.prototype._resource_handler = function(name) { var self = this; - return typeof(F) !== 'undefined' ? F.resource(self.resourceName || 'default', name) : ''; + return global.F ? F.resource(self.resourceName || 'default', name) : ''; }; ErrorBuilder.prototype.exception = function(message) { @@ -2911,6 +4426,8 @@ ErrorBuilder.prototype.add = function(name, error, path, index) { return this.push(name, error, path, index); }; +const ERRORBUILDERWHITE = { ' ': 1, ':': 1, ',': 1 }; + /** * Add an error (@alias for add) * @param {String} name Property name. @@ -2924,7 +4441,7 @@ ErrorBuilder.prototype.push = function(name, error, path, index, prefix) { this.isPrepared = false; if (name instanceof ErrorBuilder) { - if (name.hasError()) { + if (name !== this && name.is) { for (var i = 0, length = name.items.length; i < length; i++) this.items.push(name.items[i]); this.count = this.items.length; @@ -2962,9 +4479,25 @@ ErrorBuilder.prototype.push = function(name, error, path, index, prefix) { path = undefined; } - if (!error) + if (this.path && !path) + path = this.path; + + if (!error && typeof(name) === 'string') { + var m = name.length; + if (m > 15) + m = 15; + error = '@'; + for (var i = 0; i < m; i++) { + if (ERRORBUILDERWHITE[name[i]]) { + error = name; + name = ''; + break; + } + } + } + if (error instanceof Error) { // Why? The answer is in controller.callback(); It's a reason for throwing 500 - internal server error this.unexpected = true; @@ -2976,6 +4509,16 @@ ErrorBuilder.prototype.push = function(name, error, path, index, prefix) { return this; }; +ErrorBuilder.assign = function(arr) { + var builder = new ErrorBuilder(); + for (var i = 0; i < arr.length; i++) { + if (arr[i].error) + builder.items.push(arr[i]); + } + builder.count = builder.items.length; + return builder.count ? builder : null; +}; + /** * Remove error * @param {String} name Property name. @@ -3636,72 +5179,6 @@ UrlBuilder.prototype.toOne = function(keys, delimiter) { return builder.join(delimiter || '&'); }; -function TransformBuilder() {} - -TransformBuilder.transform = function(name, obj) { - - OBSOLETE('TransformBuilder', 'Builders.TransformBuilder will be removed in next versions.'); - - var index = 2; - - if (obj === undefined) { - obj = name; - name = transforms['transformbuilder_default']; - index = 1; - } - - var current = transforms['transformbuilder'][name]; - if (!current) { - F.error('Transformation "' + name + '" not found.', 'TransformBuilder.transform()'); - return obj; - } - - var sum = arguments.length - index; - if (sum <= 0) - return current.call(obj, obj); - - var arr = new Array(sum + 1); - var indexer = 1; - arr[0] = obj; - for (var i = index; i < arguments.length; i++) - arr[indexer++] = arguments[i]; - return current.apply(obj, arr); -}; - -/** - * STATIC: Create a transformation - * @param {String} name - * @param {Function} fn - * @param {Boolean} isDefault Default transformation for all TransformBuilders. - */ -TransformBuilder.addTransform = function(name, fn, isDefault) { - transforms['transformbuilder'][name] = fn; - isDefault && TransformBuilder.setDefaultTransform(name); -}; - -TransformBuilder.setDefaultTransform = function(name) { - if (name) - transforms['transformbuilder_default'] = name; - else - delete transforms['transformbuilder_default']; -}; - -function async_queue(arr, callback) { - var item = arr.shift(); - if (item) - item(() => async_queue(arr, callback)); - else - callback(); -} - -function async_wait(arr, onItem, onCallback, index) { - var item = arr[index]; - if (item) - onItem(item, () => async_wait(arr, onItem, onCallback, index + 1), index); - else - onCallback(); -} - function RESTBuilder(url) { this.$url = url; @@ -3721,14 +5198,69 @@ function RESTBuilder(url) { // this.$cache_expire; // this.$cache_nocache; // this.$redirect + + // Auto Total.js Error Handling + this.$errorbuilderhandling = true; } RESTBuilder.make = function(fn) { var instance = new RESTBuilder(); - fn(instance); + fn && fn(instance); return instance; }; +RESTBuilder.url = function(url) { + return new RESTBuilder(url); +}; + +RESTBuilder.GET = function(url, data) { + var builder = new RESTBuilder(url); + data && builder.raw(data); + return builder; +}; + +RESTBuilder.POST = function(url, data) { + var builder = new RESTBuilder(url); + builder.$method = 'post'; + builder.$type = 1; + data && builder.raw(data); + return builder; +}; + +RESTBuilder.PUT = function(url, data) { + var builder = new RESTBuilder(url); + builder.$method = 'put'; + builder.$type = 1; + builder.put(data); + return builder; +}; + +RESTBuilder.DELETE = function(url, data) { + var builder = new RESTBuilder(url); + builder.$method = 'delete'; + builder.$type = 1; + data && builder.raw(data); + return builder; +}; + +RESTBuilder.PATCH = function(url, data) { + var builder = new RESTBuilder(url); + builder.$method = 'patch'; + builder.$type = 1; + data && builder.raw(data); + return builder; +}; + +RESTBuilder.HEAD = function(url) { + var builder = new RESTBuilder(url); + builder.$method = 'head'; + return builder; +}; + +RESTBuilder.upgrade = function(fn) { + restbuilderupgrades.push(fn); +}; + /** * STATIC: Creates a transformation * @param {String} name @@ -3747,19 +5279,38 @@ RESTBuilder.setDefaultTransform = function(name) { delete transforms['restbuilder_default']; }; -RESTBuilder.prototype.setTransform = function(name) { +var RESTP = RESTBuilder.prototype; + +RESTP.promise = function(fn) { + var self = this; + return new Promise(function(resolve, reject) { + self.exec(function(err, result) { + if (err) + reject(err); + else + resolve(fn == null ? result : fn(result)); + }); + }); +}; + +RESTP.proxy = function(value) { + this.$proxy = value; + return this; +}; + +RESTP.setTransform = function(name) { this.$transform = name; return this; }; -RESTBuilder.prototype.url = function(url) { +RESTP.url = function(url) { if (url === undefined) return this.$url; this.$url = url; return this; }; -RESTBuilder.prototype.file = function(name, filename, buffer) { +RESTP.file = function(name, filename, buffer) { var obj = { name: name, filename: filename, buffer: buffer }; if (this.$files) this.$files.push(obj); @@ -3768,7 +5319,7 @@ RESTBuilder.prototype.file = function(name, filename, buffer) { return this; }; -RESTBuilder.prototype.maketransform = function(obj, data) { +RESTP.maketransform = function(obj, data) { if (this.$transform) { var fn = transforms['restbuilder'][this.$transform]; return fn ? fn(obj, data) : obj; @@ -3776,67 +5327,73 @@ RESTBuilder.prototype.maketransform = function(obj, data) { return obj; }; -RESTBuilder.prototype.timeout = function(number) { +RESTP.timeout = function(number) { this.$timeout = number; return this; }; -RESTBuilder.prototype.maxlength = function(number) { +RESTP.maxlength = function(number) { this.$length = number; this.$flags = null; return this; }; -RESTBuilder.prototype.auth = function(user, password) { - this.$headers['authorization'] = 'Basic ' + framework_utils.createBuffer(user + ':' + password).toString('base64'); +RESTP.auth = function(user, password) { + this.$headers['authorization'] = 'Basic ' + Buffer.from(user + ':' + password).toString('base64'); + return this; +}; + +RESTP.convert = function(convert) { + this.$convert = convert; return this; }; -RESTBuilder.prototype.schema = function(group, name) { +RESTP.schema = function(group, name) { this.$schema = exports.getschema(group, name); if (!this.$schema) throw Error('RESTBuilder: Schema "{0}" not found.'.format(name ? (group + '/' + name) : group)); return this; }; -RESTBuilder.prototype.noDnsCache = function() { +RESTP.noDnsCache = function() { this.$nodnscache = true; this.$flags = null; return this; }; -RESTBuilder.prototype.noCache = function() { +RESTP.noCache = function() { this.$nocache = true; return this; }; -RESTBuilder.prototype.make = function(fn) { +RESTP.make = function(fn) { fn.call(this, this); return this; }; -RESTBuilder.prototype.xhr = function() { +RESTP.xhr = function() { this.$headers['X-Requested-With'] = 'XMLHttpRequest'; return this; }; -RESTBuilder.prototype.method = function(method) { - this.$method = method.toLowerCase(); +RESTP.method = function(method, data) { + this.$method = method.charCodeAt(0) < 97 ? method.toLowerCase() : method; this.$flags = null; + data && this.raw(data); return this; }; -RESTBuilder.prototype.referer = RESTBuilder.prototype.referrer = function(value) { +RESTP.referer = RESTP.referrer = function(value) { this.$headers['Referer'] = value; return this; }; -RESTBuilder.prototype.origin = function(value) { +RESTP.origin = function(value) { this.$headers['Origin'] = value; return this; }; -RESTBuilder.prototype.robot = function() { +RESTP.robot = function() { if (this.$headers['User-Agent']) this.$headers['User-Agent'] += ' Bot'; else @@ -3844,7 +5401,7 @@ RESTBuilder.prototype.robot = function() { return this; }; -RESTBuilder.prototype.mobile = function() { +RESTP.mobile = function() { if (this.$headers['User-Agent']) this.$headers['User-Agent'] += ' iPhone'; else @@ -3852,7 +5409,7 @@ RESTBuilder.prototype.mobile = function() { return this; }; -RESTBuilder.prototype.put = function(data) { +RESTP.put = RESTP.PUT = function(data) { if (this.$method !== 'put') { this.$flags = null; this.$method = 'put'; @@ -3862,7 +5419,7 @@ RESTBuilder.prototype.put = function(data) { return this; }; -RESTBuilder.prototype.delete = function(data) { +RESTP.delete = RESTP.DELETE = function(data) { if (this.$method !== 'delete') { this.$flags = null; this.$method = 'delete'; @@ -3872,7 +5429,7 @@ RESTBuilder.prototype.delete = function(data) { return this; }; -RESTBuilder.prototype.get = function(data) { +RESTP.get = RESTP.GET = function(data) { if (this.$method !== 'get') { this.$flags = null; this.$method = 'get'; @@ -3881,7 +5438,7 @@ RESTBuilder.prototype.get = function(data) { return this; }; -RESTBuilder.prototype.post = function(data) { +RESTP.post = RESTP.POST = function(data) { if (this.$method !== 'post') { this.$flags = null; this.$method = 'post'; @@ -3891,7 +5448,25 @@ RESTBuilder.prototype.post = function(data) { return this; }; -RESTBuilder.prototype.json = function(data) { +RESTP.head = RESTP.HEAD = function() { + if (this.$method !== 'head') { + this.$flags = null; + this.$method = 'head'; + } + return this; +}; + +RESTP.patch = RESTP.PATCH = function(data) { + if (this.$method !== 'patch') { + this.$flags = null; + this.$method = 'patch'; + this.$type = 1; + } + data && this.raw(data); + return this; +}; + +RESTP.json = function(data) { if (this.$type !== 1) this.$flags = null; @@ -3905,7 +5480,7 @@ RESTBuilder.prototype.json = function(data) { return this; }; -RESTBuilder.prototype.urlencoded = function(data) { +RESTP.urlencoded = function(data) { if (this.$type !== 2) this.$flags = null; @@ -3918,15 +5493,25 @@ RESTBuilder.prototype.urlencoded = function(data) { return this; }; -RESTBuilder.prototype.accept = function(ext) { - var type = framework_utils.getContentType(ext); - if (this.$headers['Accept'] !== type) +RESTP.accept = function(ext) { + + var type; + + if (ext.length > 8) + type = ext; + else + type = framework_utils.getContentType(ext); + + if (this.$headers.Accept !== type) this.$flags = null; - this.$headers['Accept'] = type; + + this.$flags = null; + this.$headers.Accept = type; + return this; }; -RESTBuilder.prototype.xml = function(data) { +RESTP.xml = function(data, replace) { if (this.$type !== 3) this.$flags = null; @@ -3935,71 +5520,95 @@ RESTBuilder.prototype.xml = function(data) { this.$method = 'post'; this.$type = 3; + + if (replace) + this.$replace = true; + data && this.raw(data); return this; }; -RESTBuilder.prototype.redirect = function(value) { +RESTP.redirect = function(value) { this.$redirect = value; return this; }; -RESTBuilder.prototype.raw = function(value) { +RESTP.raw = function(value) { this.$data = value && value.$clean ? value.$clean() : value; return this; }; -RESTBuilder.prototype.cook = function(value) { +RESTP.plain = function() { + this.$plain = true; + return this; +}; + +RESTP.cook = function(value) { this.$flags = null; this.$persistentcookies = value !== false; return this; }; -RESTBuilder.prototype.cookies = function(obj) { +RESTP.cookies = function(obj) { this.$cookies = obj; return this; }; -RESTBuilder.prototype.cookie = function(name, value) { +RESTP.cookie = function(name, value) { !this.$cookies && (this.$cookies = {}); this.$cookies[name] = value; return this; }; -RESTBuilder.prototype.header = function(name, value) { +RESTP.header = function(name, value) { this.$headers[name] = value; return this; }; -RESTBuilder.prototype.cache = function(expire) { +RESTP.type = function(value) { + this.$headers['Content-Type'] = value; + return this; +}; + +function execrestbuilder(instance, callback) { + instance.exec(callback); +} + +RESTP.callback = function(callback) { + setImmediate(execrestbuilder, this, callback); + return this; +}; + +RESTP.cache = function(expire) { this.$cache_expire = expire; return this; }; -RESTBuilder.prototype.set = function(name, value) { +RESTP.insecure = function() { + this.$insecure = true; + return this; +}; +RESTP.set = function(name, value) { if (!this.$data) this.$data = {}; - if (typeof(name) !== 'object') { this.$data[name] = value; - return this; + } else { + var arr = Object.keys(name); + for (var i = 0, length = arr.length; i < length; i++) + this.$data[arr[i]] = name[arr[i]]; } - - var arr = Object.keys(name); - for (var i = 0, length = arr.length; i < length; i++) - this.$data[arr[i]] = name[arr[i]]; - return this; }; -RESTBuilder.prototype.rem = function(name) { +RESTP.rem = function(name) { if (this.$data && this.$data[name]) this.$data[name] = undefined; return this; }; -RESTBuilder.prototype.stream = function(callback) { +RESTP.stream = function(callback) { var self = this; var flags = self.$flags ? self.$flags : [self.$method]; @@ -4020,7 +5629,21 @@ RESTBuilder.prototype.stream = function(callback) { return U.download(self.$url, flags, self.$data, callback, self.$cookies, self.$headers, undefined, self.$timeout); }; -RESTBuilder.prototype.exec = function(callback) { +RESTP.keepalive = function() { + var self = this; + self.$keepalive = true; + return self; +}; + +RESTP.flags = function() { + var self = this; + !self.$flags && (self.$flags = []); + for (var i = 0; i < arguments.length; i++) + self.$flags(arguments[i]); + return self; +}; + +RESTP.exec = function(callback) { if (!callback) callback = NOOP; @@ -4030,6 +5653,13 @@ RESTBuilder.prototype.exec = function(callback) { if (self.$files && self.$method === 'get') self.$method = 'post'; + self.$callback = callback; + + if (restbuilderupgrades.length) { + for (var i = 0; i < restbuilderupgrades.length; i++) + restbuilderupgrades[i](self); + } + var flags = self.$flags ? self.$flags : [self.$method]; var key; @@ -4039,6 +5669,9 @@ RESTBuilder.prototype.exec = function(callback) { self.$persistentcookies && flags.push('cookies'); self.$length && flags.push('<' + self.$length); self.$redirect === false && flags.push('noredirect'); + self.$proxy && flags.push('proxy ' + self.$proxy); + self.$keepalive && flags.push('keepalive'); + self.$insecure && flags.push('insecure'); if (self.$files) { flags.push('upload'); @@ -4061,62 +5694,116 @@ RESTBuilder.prototype.exec = function(callback) { var data = F.cache.read2(key); if (data) { var evt = new framework_utils.EventEmitter2(); - process.nextTick(exec_removelisteners, evt); + setImmediate(exec_removelisteners, evt); callback(null, self.maketransform(this.$schema ? this.$schema.make(data.value) : data.value, data), data); return evt; } } - return U.request(self.$url, flags, self.$data, function(err, response, status, headers, hostname) { + self.$callback_key = key; + return U.request(self.$url, flags, self.$data, exec_callback, self.$cookies, self.$headers, undefined, self.$timeout, self.$files, self); +}; - var type = err ? '' : headers['content-type'] || ''; - var output = new RESTBuilderResponse(); +function exec_callback(err, response, status, headers, hostname, cookies, self) { + var callback = self.$callback; + var key = self.$callback_key; + var type = err ? '' : headers['content-type'] || ''; + var output = new RESTBuilderResponse(); + + if (type) { + var index = type.lastIndexOf(';'); + if (index !== -1) + type = type.substring(0, index).trim(); + } + + var ishead = response === headers; + + if (ishead) + response = ''; + + if (ishead) { + output.value = status < 400; + } else if (self.$plain) { + output.value = response; + } else { switch (type.toLowerCase()) { case 'text/xml': - output.value = response.parseXML(); + case 'application/xml': + output.value = response ? response.parseXML(self.$replace ? true : false) : {}; break; case 'application/x-www-form-urlencoded': - output.value = F.onParseQuery(response); + output.value = response ? F.onParseQuery(response) : {}; break; case 'application/json': - output.value = response.parseJSON(true); + case 'text/json': + output.value = response ? response.parseJSON(true) : null; break; default: - output.value = response.isJSON() ? response.parseJSON(true) : null; + output.value = response && response.isJSON() ? response.parseJSON(true) : null; break; } + } - if (output.value == null) - output.value = EMPTYOBJECT; + if (output.value == null) + output.value = EMPTYOBJECT; - output.response = response; - output.status = status; - output.headers = headers; - output.hostname = hostname; - output.cache = false; - output.datetime = F.datetime; + output.response = response; + output.status = status; + output.headers = headers; + output.hostname = hostname; + output.cache = false; + output.datetime = NOW; - if (self.$schema) { + var val; - if (err) - return callback(err, EMPTYOBJECT, output); + if (self.$schema) { - self.$schema.make(self.maketransform(output.value, output), function(err, model) { - !err && key && F.cache.add(key, output, self.$cache_expire); - callback(err, err ? EMPTYOBJECT : model, output); - output.cache = true; - }); + if (err) + return callback(err, EMPTYOBJECT, output); + + val = self.maketransform(output.value, output); + + if (self.$errorbuilderhandling) { + // Is the response Total.js ErrorBuilder? + if (val instanceof Array && val.length && val[0] && val[0].error) { + err = ErrorBuilder.assign(val); + if (err) + val = EMPTYOBJECT; + if (err) { + callback(err, EMPTYOBJECT, output); + return; + } + } + } - return; + self.$schema.make(val, function(err, model) { + !err && key && output.status === 200 && F.cache.add(key, output, self.$cache_expire); + callback(err, err ? EMPTYOBJECT : model, output); + output.cache = true; + }); + + } else { + !err && key && output.status === 200 && F.cache.add(key, output, self.$cache_expire); + + val = self.maketransform(output.value, output); + + if (self.$errorbuilderhandling) { + // Is the response Total.js ErrorBuilder? + if (val instanceof Array && val.length && val[0] && val[0].error) { + err = ErrorBuilder.assign(val); + if (err) + val = EMPTYOBJECT; + } } - !err && key && F.cache.add(key, output, self.$cache_expire); - callback(err, self.maketransform(output.value, output), output); - output.cache = true; + if (self.$convert && val && val !== EMPTYOBJECT) + val = CONVERT(val, self.$convert); - }, self.$cookies, self.$headers, undefined, self.$timeout, self.$files); -}; + callback(err, val, output); + output.cache = true; + } +} function exec_removelisteners(evt) { evt.removeAllListeners(); @@ -4158,60 +5845,155 @@ function $decodeURIComponent(value) { } } -global.NEWOPERATION = function(name, fn) { +global.NEWTASK = function(name, fn, filter) { + if (fn == null) { + delete tasks[name]; + } else { + tasks[name] = {}; + tasks[name].$owner = F.$owner(); + tasks[name].$filter = filter; + var append = function(key, fn) { + tasks[name][key] = fn; + }; + fn(append); + } +}; + +function taskrunner(obj, name, callback) { + obj.exec(name, callback); +} + +global.TASK = function(taskname, name, callback, options) { + var obj = new TaskBuilder(options); + obj.taskname = taskname; + + if (obj.controller) { + obj.controller.$filterschema = null; + obj.controller.$filter = null; + } + + name && setImmediate(taskrunner, obj, name, callback); + return obj; +}; + +global.NEWOPERATION = function(name, fn, repeat, stop, binderror, filter) { + + if (typeof(repeat) === 'string') { + filter = repeat; + repeat = null; + } + + if (typeof(stop) === 'string') { + filter = stop; + stop = null; + } + + if (typeof(binderror) === 'string') { + filter = binderror; + binderror = null; + } + + // @repeat {Number} How many times will be the operation repeated after error? + // @stop {Boolean} Stop when the error is thrown + // @binderror {Boolean} Binds error when chaining of operations + // @filter {Object} // Remove operation if (fn == null) { delete operations[name]; - return this; + } else { + operations[name] = fn; + operations[name].$owner = F.$owner(); + operations[name].$newversion = REGEXP_NEWOPERATION.test(fn.toString()); + operations[name].$repeat = repeat; + operations[name].$stop = stop !== false; + operations[name].$binderror = binderror === true; + operations[name].$filter = filter; + if (!operations[name].$newversion) + OBSOLETE('NEWOPERATION("{0}")'.format(name), MSG_OBSOLETE_NEW); } - - operations[name] = fn; - operations[name].$owner = F.$owner(); - operations[name].$newversion = REGEXP_NEWOPERATION.test(fn.toString()); - return this; }; -global.OPERATION = function(name, value, callback, param) { +function getLoggerNameOperation(name) { + return 'OPERATION(\'' + name + '\')'; +} + +function NoOp() {} + +global.OPERATION = function(name, value, callback, param, controller) { if (typeof(value) === 'function') { + controller = param; + param = callback; callback = value; value = EMPTYOBJECT; } + if (param instanceof Controller || param instanceof OperationOptions || param instanceof SchemaOptions || param instanceof TaskBuilder || param instanceof AuthOptions || param instanceof WebSocketClient) { + controller = param; + param = undefined; + } + + if (controller && controller.controller) + controller = controller.controller; + var fn = operations[name]; + var error = new ErrorBuilder(); + var $now; + + if (CONF.logger) + $now = Date.now(); if (fn) { + + if (fn.$filter && controller) { + controller.$filterschema = fn.$filter; + controller.$filter = null; + } + if (fn.$newversion) { - var self = new OperationOptions(error, value, param); - if (callback && callback !== NOOP) { - self.callback = function(value) { - if (arguments.length > 1) { - if (value instanceof Error || (value instanceof ErrorBuilder && value.hasError())) { - self.error.push(value); - value = EMPTYOBJECT; - } else - value = arguments[1]; - } else if (value instanceof Error || (value instanceof ErrorBuilder && value.hasError())) { + var self = new OperationOptions(error, value, param, controller); + self.$repeat = fn.$repeat; + self.callback = function(value) { + + CONF.logger && F.ilogger(getLoggerNameOperation(name), controller, $now); + if (arguments.length > 1) { + if (value instanceof Error || (value instanceof ErrorBuilder && value.is)) { self.error.push(value); value = EMPTYOBJECT; - } + } else + value = arguments[1]; + } else if (value instanceof Error || (value instanceof ErrorBuilder && value.is)) { + self.error.push(value); + value = EMPTYOBJECT; + } - callback(self.error.hasError() ? self.error : null, value, self.options); - return self; - }; - } else - self.callback = NOOP; + if (self.error.items.length && self.$repeat) { + self.error.clear(); + self.$repeat--; + fn(self); + } else { + if (callback) { + if (value === NoOp) + callback = null; + else + callback(self.error.is ? self.error : null, value, self.options); + } + } + return self; + }; fn(self); } else fn(error, value, function(value) { + CONF.logger && F.ilogger(getLoggerNameOperation(name), controller, $now); if (callback) { if (value instanceof Error) { error.push(value); value = EMPTYOBJECT; } - callback(error.hasError() ? error : null, value, param); + if (value !== NoOp) + callback(error.is ? error : null, value, param); } }); } else { @@ -4220,33 +6002,259 @@ global.OPERATION = function(name, value, callback, param) { } }; -function OperationOptions(error, value, options) { +global.RUN = function(name, value, callback, param, controller, result) { + + if (typeof(value) === 'function') { + result = controller; + controller = param; + param = callback; + callback = value; + value = EMPTYOBJECT; + } + + if (param instanceof global.Controller || (param && param.isWebSocket)) { + result = controller; + controller = param; + param = EMPTYOBJECT; + } else if (param instanceof OperationOptions || param instanceof SchemaOptions || param instanceof TaskBuilder || param instanceof AuthOptions) { + result = controller; + controller = param.controller; + } + + if (!result) { + if (typeof(param) === 'string') { + result = param; + param = EMPTYOBJECT; + } else if (typeof(controller) === 'string') { + result = controller; + controller = null; + } + } + + if (typeof(name) === 'string') + name = name.split(',').trim(); + + var error = new ErrorBuilder(); + var opt = new OperationOptions(error, value, param, controller); + + opt.meta = {}; + opt.meta.items = name; + opt.response = {}; + opt.errors = error; + + opt.callback = function(value) { + + CONF.logger && F.ilogger(getLoggerNameOperation(opt.name), controller, opt.duration); + + if (arguments.length > 1) { + if (value instanceof Error || (value instanceof ErrorBuilder && value.is)) { + opt.error.push(value); + value = EMPTYOBJECT; + } else + value = arguments[1]; + } else if (value instanceof Error || (value instanceof ErrorBuilder && value.is)) { + opt.error.push(value); + value = EMPTYOBJECT; + } + + if (opt.error.items.length && opt.$repeat > 0) { + opt.error.clear(); + opt.$repeat--; + opt.repeated++; + setImmediate(opt => opt.$current(opt), opt); + } else { + + if (opt.error.items.length) { + if (opt.$current.$binderror) + value = opt.error.output(false); + } + + if (opt.error.items.length && opt.$current.$stop) { + error.push(opt.error); + name = null; + opt.next = null; + callback(error, opt.response, opt); + } else { + + // Because "controller_json_workflow_multiple()" returns error instead of results + // error.push(opt.error); + + if (result && (result === opt.meta.current || result === opt.name)) + opt.output = value; + + opt.response[opt.name] = value; + opt.meta.prev = opt.meta.current; + opt.$next(); + } + } + }; + + name.wait(function(key, next, index) { + + var fn = operations[key]; + if (!fn) { + // What now? + // F.error('Operation "{0}" not found'.format(key), 'RUN()'); + return next(); + } + + opt.repeated = 0; + opt.error = new ErrorBuilder(); + opt.error.path = 'operation: ' + key; + opt.meta.index = index; + opt.name = opt.meta.current = key; + opt.$repeat = fn.$repeat; + opt.$current = fn; + opt.$next = next; + opt.meta.next = name[index]; + + if (CONF.logger) + opt.duration = Date.now(); + + fn(opt); + + }, () => callback(error.items.length ? error : null, result ? opt.output : opt.response, opt)); +}; + +function OperationOptions(error, value, options, controller) { + + if (!controller && options instanceof global.Controller) { + controller = options; + options = EMPTYOBJECT; + } else if (options === undefined) + options = EMPTYOBJECT; + + this.controller = controller; this.model = this.value = value; this.error = error; this.options = options; } -OperationOptions.prototype.DB = function() { +OperationOptions.prototype = { + + get user() { + return this.controller ? this.controller.user : null; + }, + + get session() { + return this.controller ? this.controller.session : null; + }, + + get sessionid() { + return this.controller && this.controller ? this.controller.req.sessionid : null; + }, + + get language() { + return (this.controller ? this.controller.language : '') || ''; + }, + + get ip() { + return this.controller ? this.controller.ip : null; + }, + + get id() { + return this.controller ? this.controller.id : null; + }, + + get req() { + return this.controller ? this.controller.req : null; + }, + + get res() { + return this.controller ? this.controller.res : null; + }, + + get params() { + return this.controller ? this.controller.params : null; + }, + + get files() { + return this.controller ? this.controller.files : null; + }, + + get body() { + return this.controller ? this.controller.body : null; + }, + + get query() { + return this.controller ? this.controller.query : null; + }, + + get headers() { + return this.controller && this.controller.req ? this.controller.req.headers : null; + }, + + get ua() { + return this.controller && this.controller.req ? this.controller.req.ua : null; + }, + + get filter() { + var ctrl = this.controller; + if (ctrl && !ctrl.$filter) + ctrl.$filter = ctrl.$filterschema ? CONVERT(ctrl.query, ctrl.$filterschema) : ctrl.query; + return ctrl ? ctrl.$filter : EMPTYOBJECT; + } + +}; + +const OperationOptionsProto = OperationOptions.prototype; + +SchemaOptionsProto.tasks = OperationOptionsProto.tasks = function(taskname, name, callback, options) { + return taskname ? TASK(taskname, name, callback, options || this) : new TaskBuilder(this); +}; + +OperationOptionsProto.cancel = function() { + var self = this; + self.callback = null; + self.error = null; + self.controller = null; + self.options = null; + self.model = self.value = null; + return self; +}; + +OperationOptionsProto.noop = function(nocustomresponse) { + var self = this; + !nocustomresponse && self.controller && self.controller.custom(); + self.callback(NoOp); + return self; +}; + +OperationOptionsProto.successful = function(callback) { + var self = this; + return function(err, a, b, c) { + if (err) + self.invalid(err); + else + callback.call(self, a, b, c); + }; +}; + +OperationOptionsProto.redirect = function(url) { + this.callback(new F.callback_redirect(url)); + return this; +}; + +OperationOptionsProto.DB = function() { return F.database(this.error); }; -OperationOptions.prototype.done = function(arg) { +OperationOptionsProto.done = function(arg) { var self = this; return function(err, response) { - - if (err && !(err instanceof ErrorBuilder)) { + if (err) { self.error.push(err); self.callback(); + } else { + if (arg) + self.callback(SUCCESS(err == null, arg === true ? response : arg)); + else + self.callback(SUCCESS(err == null)); } - - if (arg) - self.callback(SUCCESS(err == null, response)); - else - self.callback(SUCCESS(err == null)); }; }; -OperationOptions.prototype.success = function(a, b) { +OperationOptionsProto.success = function(a, b) { if (a && b === undefined && typeof(a) !== 'boolean') { b = a; @@ -4257,12 +6265,330 @@ OperationOptions.prototype.success = function(a, b) { return this; }; -OperationOptions.prototype.invalid = function(name, error, path, index) { - this.error.push(name, error, path, index); - this.callback(); +OperationOptionsProto.invalid = function(name, error, path, index) { + + var self = this; + + if (arguments.length) { + self.error.push(name, error, path, index); + self.callback(); + return self; + } + + return function(err) { + self.error.push(err); + self.callback(); + }; +}; + +function AuthOptions(req, res, flags, callback) { + this.req = req; + this.res = res; + this.flags = flags || []; + this.processed = false; + this.$callback = callback; +} + +AuthOptions.prototype = { + + get language() { + return this.req.language || ''; + }, + + get ip() { + return this.req.ip; + }, + + get params() { + return this.req.params; + }, + + get files() { + return this.req.files; + }, + + get body() { + return this.req.body; + }, + + get query() { + return this.req.query; + }, + + get headers() { + return this.req.headers; + }, + + get ua() { + return this.req ? this.req.ua : null; + } +}; + +const AuthOptionsProto = AuthOptions.prototype; + +AuthOptionsProto.roles = function() { + for (var i = 0; i < arguments.length; i++) + this.flags.push('@' + arguments[i]); return this; }; +SchemaOptionsProto.cookie = OperationOptionsProto.cookie = TaskBuilderProto.cookie = AuthOptionsProto.cookie = function(name, value, expire, options) { + var self = this; + if (value === undefined) + return self.req.cookie(name); + if (value === null) + expire = '-1 day'; + self.res.cookie(name, value, expire, options); + return self; +}; + +AuthOptionsProto.invalid = function(user) { + this.next(false, user); +}; + +AuthOptionsProto.done = function(response) { + var self = this; + return function(is, user) { + self.next(is, response ? response : user); + }; +}; + +AuthOptionsProto.success = function(user) { + this.next(true, user); +}; + +AuthOptionsProto.next = AuthOptionsProto.callback = function(is, user) { + + if (this.processed) + return; + + // @is "null" for callbacks(err, user) + // @is "true" + // @is "object" is as user but "user" must be "undefined" + + if (is instanceof Error || is instanceof ErrorBuilder) { + // Error handling + is = false; + } else if (is == null && user) { + // A callback error handling + is = true; + } else if (user == null && is && is !== true) { + user = is; + is = true; + } + + this.processed = true; + this.$callback(is, user, this); +}; + +AuthOptions.wrap = function(fn) { + if (REGEXP_NEWOPERATION.test(fn.toString())) { + var fnnew = function(req, res, flags, next) { + fn(new AuthOptions(req, res, flags, next)); + }; + fnnew.$newversion = true; + return fnnew; + } + return fn; +}; + +global.CONVERT = function(value, schema) { + var key = schema; + if (key.length > 50) + key = key.hash(); + var fn = F.convertors2 && F.convertors2[key]; + return fn ? fn(value) : convertorcompile(schema, value, key); +}; + +function convertorcompile(schema, data, key) { + var prop = schema.split(','); + var cache = []; + for (var i = 0, length = prop.length; i < length; i++) { + var arr = prop[i].split(':'); + var obj = {}; + + var type = arr[1].toLowerCase().trim(); + var size = 0; + var isarr = type[0] === '['; + if (isarr) + type = type.substring(1, type.length - 1); + + var index = type.indexOf('('); + if (index !== -1) { + size = +type.substring(index + 1, type.length - 1).trim(); + type = type.substring(0, index); + } + + obj.name = arr[0].trim(); + obj.size = size; + obj.type = type; + obj.array = isarr; + + switch (type) { + case 'string': + obj.fn = $convertstring; + break; + case 'number': + obj.fn = $convertnumber; + break; + case 'number2': + obj.fn = $convertnumber2; + break; + case 'boolean': + obj.fn = $convertboolean; + break; + case 'date': + obj.fn = $convertdate; + break; + case 'uid': + obj.fn = $convertuid; + break; + case 'upper': + obj.fn = (val, obj) => $convertstring(val, obj).toUpperCase(); + break; + case 'lower': + obj.fn = (val, obj) => $convertstring(val, obj).toLowerCase(); + break; + case 'capitalize': + obj.fn = (val, obj) => $convertstring(val, obj).capitalize(); + break; + case 'capitalize2': + obj.fn = (val, obj) => $convertstring(val, obj).capitalize(true); + break; + case 'base64': + obj.fn = val => typeof(val) === 'string' ? val.isBase64() ? val : '' : ''; + break; + case 'email': + obj.fn = function(val, obj) { + var tmp = $convertstring(val, obj); + return tmp.isEmail() ? tmp : ''; + }; + break; + case 'zip': + obj.fn = function(val, obj) { + var tmp = $convertstring(val, obj); + return tmp.isZIP() ? tmp : ''; + }; + break; + case 'phone': + obj.fn = function(val, obj) { + var tmp = $convertstring(val, obj); + return tmp.isPhone() ? tmp : ''; + }; + break; + case 'url': + obj.fn = function(val, obj) { + var tmp = $convertstring(val, obj); + return tmp.isURL() ? tmp : ''; + }; + break; + case 'json': + obj.fn = function(val, obj) { + var tmp = $convertstring(val, obj); + return tmp.isJSON() ? tmp : ''; + }; + break; + case 'object': + return val => val; + case 'search': + obj.fn = (val, obj) => $convertstring(val, obj).toSearch(); + break; + default: + obj.fn = val => val; + break; + } + + if (isarr) { + obj.fn2 = obj.fn; + obj.fn = function(val, obj) { + if (!(val instanceof Array)) + val = val == null || val == '' ? [] : [val]; + var output = []; + for (var i = 0, length = val.length; i < length; i++) { + var o = obj.fn2(val[i], obj); + switch (obj.type) { + case 'email': + case 'phone': + case 'zip': + case 'json': + case 'url': + case 'uid': + case 'date': + o && output.push(o); + break; + default: + output.push(o); + break; + } + } + return output; + }; + } + + cache.push(obj); + } + + var fn = function(data) { + var output = {}; + for (var i = 0, length = cache.length; i < length; i++) { + var item = cache[i]; + output[item.name] = item.fn(data[item.name], item); + } + return output; + }; + if (!F.convertors2) + F.convertors2 = {}; + F.convertors2[key] = fn; + return fn(data); +} + +function $convertstring(value, obj) { + return value == null ? '' : typeof(value) !== 'string' ? obj.size ? value.toString().max(obj.size) : value.toString() : obj.size ? value.max(obj.size) : value; +} + +function $convertnumber(value) { + if (value == null) + return 0; + if (typeof(value) === 'number') + return value; + var num = +value.toString().replace(',', '.'); + return isNaN(num) ? 0 : num; +} + +function $convertnumber2(value) { + if (value == null) + return null; + if (typeof(value) === 'number') + return value; + var num = +value.toString().replace(',', '.'); + return isNaN(num) ? null : num; +} + +function $convertboolean(value) { + return value == null ? false : value === true || value == '1' || value === 'true' || value === 'on'; +} + +function $convertuid(value) { + return value == null ? '' : typeof(value) === 'string' ? value.isUID() ? value : '' : ''; +} + +function $convertdate(value) { + + if (value == null) + return null; + + if (value instanceof Date) + return value; + + switch (typeof(value)) { + case 'string': + case 'number': + return value.parseDate(); + } + + return null; +} + // ====================================================== // EXPORTS // ====================================================== @@ -4273,18 +6599,18 @@ exports.ErrorBuilder = ErrorBuilder; exports.Pagination = Pagination; exports.Page = Page; exports.UrlBuilder = UrlBuilder; -exports.TransformBuilder = TransformBuilder; exports.SchemaOptions = SchemaOptions; exports.OperationOptions = OperationOptions; exports.RESTBuilderResponse = RESTBuilderResponse; +exports.AuthOptions = AuthOptions; global.RESTBuilder = RESTBuilder; global.RESTBuilderResponse = RESTBuilderResponse; global.ErrorBuilder = ErrorBuilder; -global.TransformBuilder = TransformBuilder; global.Pagination = Pagination; global.Page = Page; global.UrlBuilder = global.URLBuilder = UrlBuilder; global.SchemaBuilder = SchemaBuilder; +global.TaskBuilder = TaskBuilder; // Uninstall owners exports.uninstall = function(owner) { @@ -4302,11 +6628,102 @@ exports.uninstall = function(owner) { }); }; -exports.restart = function() { - schemas = {}; - operations = {}; - Object.keys(transforms).forEach(function(key) { - if (key.indexOf('_') === -1) - transforms[key] = {}; - }); +TaskBuilderProto.invalid = function(err, msg) { + var self = this; + if (!self.$done) { + !self.error && (self.error = new ErrorBuilder()); + self.error.push(err, msg); + self.done(); + } + return self; }; + +TaskBuilderProto.push = function(name, fn) { + var self = this; + self.tasks[name] = fn; + return self; +}; + +TaskBuilderProto.next = function() { + var self = this; + if (!self.$done) { + self.current && self.controller && CONF.logger && F.ilogger((self.name || 'tasks') + '.' + self.current, self.controller, self.$now); + self.prev = self.current; + for (var i = 0; i < arguments.length; i++) { + self.current = arguments[i]; + var task = self.tasks[self.current] || (self.taskname ? tasks[self.taskname] && tasks[self.taskname][self.current] : null); + if (task == null) + continue; + else { + task.call(self, self); + return self; + } + } + self.done(); + } + return self; +}; + +TaskBuilderProto.next2 = function(name) { + var self = this; + return function(err) { + if (err) + self.invalid(err); + else { + if (name == null) + self.done(); + else + self.next(name); + } + }; +}; + +TaskBuilderProto.done = function(data) { + var self = this; + self.$callback && self.$callback(self.error && self.error.is ? self.error : null, data || self.value); + self.$done = true; + return self; +}; + +TaskBuilderProto.done2 = function(send_value) { + var self = this; + return function(err, data) { + if (err) + self.invalid(err); + else + self.done(send_value ? data : null); + }; +}; + +TaskBuilderProto.success = function(data) { + return this.done(SUCCESS(true, data)); +}; + +TaskBuilderProto.success2 = function(send_value) { + var self = this; + return function(err, data) { + if (err) + self.invalid(err); + else + self.done(SUCCESS(true, send_value ? data : null)); + }; +}; + +TaskBuilderProto.callback = function(fn) { + var self = this; + self.$callback = fn; + return self; +}; + +TaskBuilderProto.exec = function(name, callback) { + var self = this; + + if (callback) + self.$callback = callback; + + if (CONF.logger) + self.$now = Date.now(); + + self.next(name); + return self; +}; \ No newline at end of file diff --git a/bundles.js b/bundles.js new file mode 100755 index 000000000..cba3ab529 --- /dev/null +++ b/bundles.js @@ -0,0 +1,385 @@ +// Copyright 2012-2020 (c) Peter Širka +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +/** + * @module FrameworkBundles + * @version 3.4.3 + */ + +require('./index'); + +const Fs = require('fs'); +const Path = require('path'); +const CONSOLE = process.argv.indexOf('restart') === -1; +const INTERNAL = { '/sitemap': 1, '/versions': 1, '/workflows': 1, '/dependencies': 1, '/config': 1, '/config-release': 1, '/config-debug': 1 }; +const isWindows = require('os').platform().substring(0, 3).toLowerCase() === 'win'; +const REGAPPEND = /\/--[a-z0-9]+/i; +const REGAPPENDREPLACE = /\/--/g; +const REGBK = /(-|_)bk\.bundle$/i; +const META = {}; + +META.version = 1; +META.created = new Date(); +META.total = 'v' + F.version_header; +META.node = F.version_node; +META.files = []; +META.skip = false; +META.directories = []; +META.ignore = () => true; + +exports.make = function(callback) { + + var path = F.path.root(); + var blacklist = {}; + + if (CONSOLE) { + console.log('--------------------- BUNDLING ---------------------'); + console.time('Done'); + } + + var isignore = false; + + try { + META.ignore = makeignore(Fs.readFileSync(Path.join(path, '.bundleignore')).toString('utf8').split('\n')); + isignore = true; + } catch (e) {} + + if (!isignore) { + try { + META.ignore = makeignore(Fs.readFileSync(Path.join(path, '.bundlesignore')).toString('utf8').split('\n')); + } catch (e) {} + } + + blacklist[CONF.directory_temp] = 1; + blacklist[CONF.directory_bundles] = 1; + blacklist[CONF.directory_src] = 1; + blacklist[CONF.directory_logs] = 1; + blacklist['/node_modules/'] = 1; + blacklist['/debug.pid'] = 1; + blacklist['/package-lock.json'] = 1; + + var Files = []; + var Dirs = []; + var Merge = []; + var Length = path.length; + var async = []; + + async.push(cleanFiles); + + async.push(function(next) { + META.skip && (async.length = 0); + next(); + }); + + async.push(function(next) { + var target = F.path.root(CONF.directory_src); + U.ls(F.path.root(CONF.directory_bundles), function(files) { + var dirs = {}; + files.wait(function(filename, resume) { + + if (!filename.endsWith('.bundle') || REGBK.test(filename)) + return resume(); + + if (CONSOLE) + console.log('-----', U.getName(filename)); + + var dbpath = CONF.directory_databases; + var pathupdate = CONF.directory_updates; + var pathstartup = '/startup'; + + F.restore(filename, target, resume, function(p, dir) { + + if (dir) { + if (!p.startsWith(dbpath) && META.directories.indexOf(p) === -1) + META.directories.push(p); + } else { + + var dirname = p.substring(0, p.length - U.getName(p).length); + if (dirname && dirname !== '/') + dirs[dirname] = true; + + // handle files in bundle to merge + var mergeme = 0; + + if (REGAPPEND.test(p)) { + mergeme = 3; + p = p.replace(REGAPPENDREPLACE, '/'); + } + + var exists = false; + try { + exists = Fs.statSync(Path.join(target, p)) != null; + } catch (e) {} + + if ((dirname === pathupdate || dirname === pathstartup) && !exists) { + try { + exists = Fs.statSync(Path.join(target, p + '_bk')) != null; + } catch (e) {} + } + + // A specific file like DB file or startup file or update script + if (exists && (p.startsWith(dbpath) || p.startsWith(pathupdate) || p.startsWith(pathstartup))) + return false; + + if (INTERNAL[p] || U.getExtension(p) === 'resource' || mergeme) { + var hash = p.hash(true).toString(16); + Merge.push({ name: p, filename: Path.join(target, p + hash), type: mergeme }); + META.files.push(p + hash); + return p + hash; + } + + if (META.files.indexOf(p) === -1) + META.files.push(p); + } + + return true; + }); + }, function() { + dirs = Object.keys(dirs); + dirs.length && Dirs.push.apply(Dirs, dirs); + next(); + }); + }); + }); + + async.push(function(next) { + if (Merge.length) { + copyFiles(Merge, function() { + for (var i = 0; i < Merge.length; i++) { + try { + Fs.unlinkSync(Merge[i].filename); + } catch(e) {} + } + next(); + }); + } else + next(); + }); + + async.push(function(next) { + U.ls(path, function(files, dirs) { + + for (var i = 0, length = dirs.length; i < length; i++) + Dirs.push(normalize(dirs[i].substring(Length))); + + for (var i = 0, length = files.length; i < length; i++) { + var file = files[i].substring(Length); + var type = 0; + + if (file.startsWith(CONF.directory_databases) || file.startsWith('/flow/') || file.startsWith('/dashboard/')) + type = 1; + else if (REGAPPEND.test(file)) { + file = file.replace(REGAPPENDREPLACE, '/'); + type = 3; + } else if (file.startsWith(CONF.directory_public)) + type = 2; + + Files.push({ name: file, filename: files[i], type: type }); + } + + next(); + }, function(p) { + p = normalize(p.substring(Length)); + return blacklist[p] == null && p.substring(0, 2) !== '/.'; + }); + }); + + async.push(function(next) { + createDirectories(Dirs, function() { + copyFiles(Files, next); + }); + }); + + async.push(function(next) { + Fs.writeFileSync(Path.join(F.path.root(CONF.directory_src), 'bundle.json'), JSON.stringify(META, null, '\t')); + next(); + }); + + async.async(function() { + CONSOLE && console.timeEnd('Done'); + callback(); + }); + +}; + +function makeignore(arr) { + + var ext; + var code = ['var path=P.substring(0,P.lastIndexOf(\'/\')+1);', 'var ext=U.getExtension(P);', 'var name=U.getName(P).replace(\'.\'+ ext,\'\');']; + + for (var i = 0; i < arr.length; i++) { + var item = arr[i]; + var index = item.lastIndexOf('*.'); + + if (index !== -1) { + // only extensions on this path + ext = item.substring(index + 2); + item = item.substring(0, index); + code.push('tmp=\'{0}\';'.format(item)); + code.push('if((!tmp||path===tmp)&&ext===\'{0}\')return;'.format(ext)); + continue; + } + + ext = U.getExtension(item); + if (ext) { + // only filename + index = item.lastIndexOf('/'); + code.push('tmp=\'{0}\';'.format(item.substring(0, index + 1))); + code.push('if(path===tmp&&U.getName(\'{0}\').replace(\'.{1}\', \'\')===name&&ext===\'{1}\')return;'.format(item.substring(index + 1), ext)); + continue; + } + + // all nested path + code.push('if(path.startsWith(\'{0}\'))return;'.format(item.replace('*', ''))); + } + + code.push('return true'); + return new Function('P', code.join('')); +} + +function normalize(path) { + return isWindows ? path.replace(/\\/g, '/') : path; +} + +function cleanFiles(callback) { + + var path = F.path.root(CONF.directory_src); + var length = path.length - 1; + var blacklist = {}; + + blacklist[CONF.directory_public] = 1; + blacklist[CONF.directory_private] = 1; + blacklist[CONF.directory_databases] = 1; + + var meta; + + try { + meta = U.parseJSON(Fs.readFileSync(Path.join(path, 'bundle.json')).toString('utf8'), true) || {}; + + if (CONF.bundling === 'shallow') { + META.skip = true; + callback(); + return; + } + + } catch (e) { + meta = {}; + } + + if (meta.files && meta.files.length) { + for (var i = 0, length = meta.files.length; i < length; i++) { + var filename = meta.files[i]; + var dir = filename.substring(0, filename.indexOf('/', 1) + 1); + if (!blacklist[dir]) { + try { + F.consoledebug('Remove', filename); + Fs.unlinkSync(Path.join(path, filename)); + } catch (e) {} + } + } + } + + if (meta.directories && meta.directories.length) { + meta.directories.quicksort('length', false); + for (var i = 0, length = meta.directories.length; i < length; i++) { + try { + if (!blacklist[meta.directories[i]]) + Fs.rmdirSync(Path.join(path, meta.directories[i])); + } catch (e) {} + } + } + + callback(); +} + +function createDirectories(dirs, callback) { + + var path = F.path.root(CONF.directory_src); + + try { + Fs.mkdirSync(path); + } catch(e) {} + + for (var i = 0, length = dirs.length; i < length; i++) { + var p = normalize(dirs[i]); + if (META.directories.indexOf(p) === -1) + META.directories.push(p); + try { + Fs.mkdirSync(Path.join(path, dirs[i])); + } catch (e) {} + } + + callback(); +} + +function copyFiles(files, callback) { + var path = F.path.root(CONF.directory_src); + files.wait(function(file, next) { + + if (!META.ignore(file.name)) + return next(); + + var filename = Path.join(path, file.name); + var exists = false; + var ext = U.getExtension(file.name); + var append = file.type === 3; + + try { + exists = Fs.statSync(filename) != null; + } catch (e) {} + + // DB file + if (file.type === 1 && exists) { + next(); + return; + } + + var p = normalize(file.name); + + if (file.type !== 1 && META.files.indexOf(p) === -1) + META.files.push(p); + + if (exists && (ext === 'resource' || (!ext && file.name.substring(1, 7) === 'config') || INTERNAL[file.name])) + append = true; + + if (CONSOLE && exists) { + CONF.allow_debug && F.consoledebug(append ? 'EXT:' : 'REW:', p); + } else + F.consoledebug(append ? 'EXT:' : 'COP:', p); + + if (append) { + Fs.appendFile(filename, '\n' + Fs.readFileSync(file.filename).toString('utf8'), next); + } else + copyFile(file.filename, filename, next); + + if (CONSOLE && exists) + CONF.allow_debug && F.consoledebug('REW:', p); + else + F.consoledebug('COP:', p); + + }, callback); +} + +function copyFile(oldname, newname, callback) { + var writer = Fs.createWriteStream(newname); + writer.on('finish', callback); + Fs.createReadStream(oldname).pipe(writer); +} diff --git a/changes.txt b/changes.txt index 2a4b3d73e..68a16e3a6 100755 --- a/changes.txt +++ b/changes.txt @@ -1,3 +1,712 @@ +======= 3.4.13 + +- fixed wrong counting of week number in the `Date.format()` + +======= 3.4.12 + +- (critical) fixed WebSocket implementation for Safari +15 +- (critical) fixed extracting packages/bundles + +======= 3.4.11 + +- fixed calling `F.snapshotstats()` #785 +- improved RegExp for validating URL addresses by [yetingli](https://github.com/yetingli) + +======= 3.4.10 + +- fixed CSS variables + +======= 3.4.9 + +- added HTML escaping for meta tags +- added `insecure` flag into the `U.request()` method +- added `RESTBuilder.insecure()` method +- fixed security issue when parsing query arguments (reported by ) +- fixed security in `U.get()` and `U.set()` (reported by Agustin Gianni) + +======= 3.4.8 + +- fixed measuring dimension for `.gif` images +- fixed potential remote code execution in `U.set()` founded by [Snyk](https://snyk.io/vuln) + +======= 3.4.7 + +- fixed: command injection in `Image.pipe()` and `Image.stream()` +- fixed `DELETE` method for the schemas (now it works like `PATCH` method) +- fixed: `controller.transfer()` + +======= 3.4.6 + +- added: a support for Total.js v4 UIDs + +- updated: file stats +- updated: calculating of `usage` + +- fixed: applying of `default_root` in static files +- fixed: routing evaluation +- fixed: parsing of longer WebSocket messages +- fixed: mail error handling +- fixed: `versions` with `default_root` + +======= 3.4.5 + +- fixed: a problem with persistent images + +======= 3.4.4 + +- added: schema options `$.successful(function(response) {})` +- added: `options.reconnectserver {Boolean}` to `WEBSOCKETCLIENT` +- added: `req.snapshot(callback(err, request_body))` +- added: a new command `CMD('reload_preferences')` +- added: a new FILESTORAGE mechanism based on `UID` +- added: `sql` extension to `U.getContentType()` +- added: `F.stats.performance.usage` which contains percentual usage of the thread + +- updated: `SchemaOptions` method `$.response([index/operation_name])`, e.g. `$.response('workflow.NAME')` +- updated: snapshot `startscript.js.json` contains tabs instead of spaces +- updated: `DatabaseBuilder.rule(rule, [param])`, supports string declaration of filter function +- updated: `URL` validation + +- fixed: cleaning of NoSQL embedded databases +- fixed: `String.parseCSV()`, now supports multiline strings +- fixed: a bug when closing of websocket +- fixed: `DatabaseBuilder.search()` method +- fixed: `Error` in `CLONE()` method +- fixed: `schema.inherit()` by adding `schema.middleware()` and `schema.verify()` +- fixed: parsing messages in WebSocket +- fixed: a problem in some commands pre-render in the view compiler +- fixed: parsing of query strings + +======= 3.4.3 + +- added: `HASH(value, [type])` for creating hash like in jComponent +- added: `SchemaOptions.repo` as alias to `SchemaInstance.model.$$repository` +- added: a new type `CONVERT syntax` to `schema.define()` (more in docs) +- added: `SchemaEntity.verify(name, function($), [cache])` for async verification of values +- added: `TEMP` variable as a new global variable (it's cleaned every 7 minutes) +- added: `CONF.allow_persistent_images: true` which allows to reuse resized images in temp directory +- added: `req.filecache(callback)` as alias for `F.exists()` +- added: own QueryParser +- added: `RESTBuilderInstance.convert('name:String,age:Number')` method +- added: `RESTBuilder.upgrade(fn(restbuilder))` for upgrading of `RESTBuilder` +- added: `RESTBuilder` parses Total.js Errors in responses as Error +- added: `String.prototype.env()` replaces all values in the form `[key]` for `CONF.key` +- added: WebSocket supports a new type - raw `buffer` +- added: `Number.fixed(decimals)` + +- updated: `websocket.send2(message, comparer, replacer, [params])` by adding `params` argument for comparer function +- updated: `Websocket.encodedecode` can enable/disable easily encoding of messages +- updated: bundling skips all bundles with `-bk.bundle` in filename +- updated: bundle filenames are displayed in console +- updated: `UPDATE()` method by adding `noarchive` argument +- updated: `TEST()` method supports `[subdomain]` keyword and `METHOD url` in URL address +- updated: `MODIFY([filename], fn)` by adding `filename` argument +- updated: background of schedulers by @fgnm +- updated: `U.download()` by adding `param` argument +- updated: `U.request()` by adding `param` argument +- updated: `schema.cl(name, [value])` method by adding `value` argument for replacing of existing code-list +- updated: Tangular version to `v4.0.0` + +- improved: `filename` in modificators (now filenames contain relative paths) +- improved: performance of `U.request()` (around +10%) +- improved: performance of `U.download()` (around +10%) +- improved: performance of `RESTBuilder` +- improved: CSS minifier by compressing single hex color from e.g. `#000000` to `#000` + +- fixed: localization in `totaljs` executable script +- fixed: phone validation +- fixed: `DOWNLOAD()` +- fixed: `Number.VAT()` by Tomas Novak +- fixed: debugging mode in Node.js v14 +- fixed: `allow_compile_html` in static files +- fixed: `ROUTE()` method, there was a problem with spaces `GET /* ` +- fixed: `ACTION()` with json output +- fixed: controller in `$ACTION()` with used `get` and `query` actions +- fixed: `PATCH` method in `$ACTION()` +- fixed: `schema.allow()` in `PATCH` method +- fixed: image resizing in debug-mode + +======= 3.4.1 + +- added: `SchemaOptions.parent` returns a parent model +- added: Tangular template engine (experimental) +- added: `String.makeid()` for creating of unique identifier from string +- added: a new property called `message.ua` to `FLOWSTREAM()` + +- updated: `HttpFile.fs()` by adding `id` argument for updating of existing file +- updated: default value for `allow_ssc_validation` to `true` + +- fixed: `String.parseDate(format)` with defined format +- fixed: inheriting of controllers between schemas +- fixed: `MailMessage.attachments()` +- fixed: calling of `F.snapshotstats` in cache recycle +- fixed: `controller.success()` +- fixed: removing of unused files when a bundle is extracting +- fixed: a processor function in `F.backup()` + +- improved: `Date.format()` +- improved: Total.js translate (supports ErrorBuilder and DBMS) + +======= 3.4.0 + +- added: `date.setTimeZone(timezone)` +- added: `NOSQL('~absolute_path.nosql')' loads external NoSQL embedded database +- added: `TABLE('~absolute_path.nosql')' loads external Table +- added: `(generate)` subtype into the `config` files +- added: `String.isBase64()` +- added: new schema type `Base64` +- added: SchemaEntity supports `schema.addWorkflowExtension(name, fn($, [data]))` +- added: SchemaEntity supports `schema.addTransformExtension(name, fn($, [data]))` +- added: SchemaEntity supports `schema.addOperationExtension(name, fn($, [data]))` +- added: SchemaEntity supports `schema.addHookExtension(name, fn($, [data]))` +- added: SchemaEntity supports `schema.setSaveExtension(fn($, [data]))` +- added: SchemaEntity supports `schema.setReadExtension(fn($, [data]))` +- added: SchemaEntity supports `schema.setQueryExtension(fn($, [data]))` +- added: SchemaEntity supports `schema.setRemoveExtension(fn($, [data]))` +- added: SchemaEntity supports `schema.setInsertExtension(fn($, [data]))` +- added: SchemaEntity supports `schema.setUpdateExtension(fn($, [data]))` +- added: SchemaEntity supports `schema.setPatchExtension(fn($, [data]))` +- added: SchemaOptions supports `$.extend([data])` for evaluating of all extensions for the current operation +- added: `WebSocket.keys` property (it contains all keys with connections) +- added: `threads` directory for server-less functionality +- added: a global variable called `THREAD` with a name of current thread +- added: `require('total.js').http(..., { thread: 'thread_name' })` evaluates only specified thread +- added: `require('total.js').cluster.http(..., { thread: 'thread_name' })` evaluates only specified thread in cluster +- added: framework creates a file with app stats in the form `your_init_script_name.js.json` +- added: a new config key `allow_stats_snapshot` +- added: view engine `@{import()}` supports auto-merging JS or CSS files: `@{import('default.js + ui.js')}` +- added: `exports.options` delegate to component in `FLOWSTREAM` +- added: `DatabaseBuilder.autofill()` from DBMS +- added: `HttpFile.extension` property +- added: `HttpFile.size` property alias to `HttpFile.length` +- added: auto-session cleaner of unused sessions +- added: `allow_sessions_unused` config key for cleaning of unused sessions +- added: missing `PATH.schemas`, `PATH.operations` and `PATH.tasks` +- added: a new method `PATH.updates` +- added: easy updating of applications via `UPDATE(versions, [callback], [pause_server_message])` +- added: NOSQL counter `.reset([type], [id], [date], [callback])` method- +- added: `session.listlive(callback)` returns all live items in session +- added: `controller.ua` returns parsed User-Agent +- added: `$.ua` returns parsed User-Agent in Schemas, Operations, TaskBuilder, `MIDDLEWARE()` and `AUTH()` +- added: support for `.mjs` extensions +- added: a simple support for DDOS protection `allow_reqlimit : Number` (max. concurent requests by IP just-in-time) +- added: unit-testing supports colors, added by @dacrhu +- added: `String.encryptUID()` as alias for `U.encryptUID()` +- added: `String.decryptUID()` as alias for `U.decryptUID()` + +- updated: `WEBSOCKET()` supports `+`, `-` and `🔒` as authorization flags +- updated: `LOAD()` supports `service` type +- updated: cluster watches `restart` or `restart_NAME_of_THREAD` files for restarting of existing threads +- updated: cluster supports `auto` mode +- updated: cluster supports watcher in `debug` mode +- updated: `*.filefs()`, `*.filenosql()`, `*.imagefs()`, `*.imagenosql()` by adding `checkmeta` argument +- updated: `$.done([user_instance])` method in `AUTH()`, added a new argument called `user_instance` (optional) +- updated: GZIP is enabled only for JSON bodies which have more than 4096 bytes +- updated: `.env` parser supports parsing of `.env-debug` or `.env-release` files according to the mode +- updated: list of user-agents in `String.parseUA()` + +- fixed: `ON('error404')` when the route doens't exist +- fixed: `filter` in Schema `workflows`, `transformations` and `operations` +- fixed: `NOSQL()` joins with absolute paths +- fixed: `TABLE()` joins with absolute paths +- fixed: `(random)` subtype in `config` files +- fixed: `(response)` phrase in `ROUTE()` for multiple `OPERATIONS` +- fixed: a response in `ROUTE()` with mulitple operations if the result contained some error +- fixed: a security bug with a path traversal vulnerability +- fixed: `debug` watcher for `themes` +- fixed: `generators` in schemas with a new declaration +- fixed: a problem with handling files in 404 action +- fixed: `startup` directory in bundles +- fixed: `schema.inherit()` didn't copy `required` fields. +- fixed: `SUCCESS()` serialization with `SUCCESS()` argument +- fixed: a critial bug with `UID()` generator +- fixed: clearing of DNS cache + +- improved: `LOGMAIL()` mail format +- improved: starting logs in console output (added IPv4 local address) +- improved: performance with JSON serialization in `controller.success()` and `controller.done()` + +======= 3.3.2 + +- fixed: default time zone (`utc` is default time zone) + +======= 3.3.1 + +- added: `RESTBuilder.callback()` which performs `.exec()` automatically +- added: `FLOWSTREAM()` + +- fixed: `AUDIT()` method +- fixed: error handling in `controller.invalid()` +- fixed: `req.authorize()` +- fixed: CSS auto-vendor-prefixes, fixed `opacity` with `!important` +- fixed: `CONVERT()` a problem with arrays + +======= 3.3.0 + +- added: `NEWTASK(name, declaration)` for creating preddefined `TaskBuilder` +- added: `TASK(name, taskname, callback, [controller/SchemaOptions/OperationOptions/ErrorBuilder])` for executing preddefined `TaskBuilder` +- added: a new config key `directory_tasks` for `TaskBuilder` +- added: a global alias `MODIFY()` for `F.modify()` +- added: a global alias `VIEWCOMPILE()` for `F.view_compile()` +- added: `mail.type = 'html'` can be `html` (default) or `plain` +- added: `$.headers` into the `SchemaOptions`, `OperationOptions` and `TaskBuilder` +- added: `String.parseCSV([delimiter])` returns `Object Array` +- added: `String.parseUA([structured])` a simple user-agent parser +- added: `req.useragent([structured])` returns parsed User-Agent +- added: a new config key `default_crypto` it can rewrite Total.js crypto mechanism (default: `undefined`) +- added: a new config key `default_crypto_iv` it's an initialization vector (default: generated from `secret`) or it can contain a custom `hex` value +- added: a new config key `allow_workers_silent` can enable/disable silent workers (default: `false`) +- added: a new config sub-type called `random`, example: `secret_key (random) : 10` and `10` means a length of value +- added: a new command `clear_dnscache` for clearing DNS cache +- added: commands `INSTALL('command', 'command_name', function)` for registering commands and `CMD(name, [a], [b], [c], [d])` for executing commands +- added: `ENCRYPTREQ(req, val, [key], [strict])` to encrypt value according to the request meta data +- added: `DECRYPTREQ(req, val, [key])` to decrypt value according to the request meta data +- added: `controller.nocache()` +- added: `controller.nocontent()` +- added: `REPO` as a global variable +- added: `FUNC` as a global variable +- added: `MAIN` as a global variable +- added: `DEF` as a global variable for defining of behaviour for some operations (alternative to `F`) +- added: `PREF.set(name, [value])` (read+write) or `PREF.propname` (only read) for reading/writing a persistent preferences +- added: `F.onPrefSave = function(obj)` to write preferences +- added: `F.onPrefLoad = function(next(obj))` to read preferences +- added: `RESTBuilder.url(url)` which returns a new instance of `RESTBuilder` for chaining +- added: `restbuilder.keepalive()` enables a keepalive for `RESTBuilder` instance +- added: `SESSION()` management, more in docs +- added: `controller.sessionid` with ID of `SESSION()` +- added: `AUTH()` supports a new auth declaration with `$` as `AuthOptions` like `SchemaOptions` or `OperationOptions` +- added: `AuthOptions` to prototypes +- added: `ErrorBuilder.length` property (alias for `instance.items.length) +- added: Schemas `prepare` supports `req` argument +- added: `DEF.currencies.eur = function(val) {}` registers a currency formatter +- added: `DEF.helpers` registers a new view engine helper (`F.helpers` is alias for for this object) +- added: `DEF.validators` is alias for for `F.validators` +- added: usage of currency formatter `Number.currency(currency)` +- added: new schema type `Number2` with default value is `null`, not zero `0` +- added: `@{json2(model, elementID, key1, key2, key3)}` can serialize data with keys defined into the ` + + + \ No newline at end of file diff --git a/flow.js b/flow.js new file mode 100644 index 000000000..fa41980ed --- /dev/null +++ b/flow.js @@ -0,0 +1,491 @@ +// Copyright 2012-2020 (c) Peter Širka +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +/** + * @module FrameworkFlowStream + * @version 3.4.0 + */ + +if (!global.framework_utils) + global.framework_utils = require('./utils'); + +const D = '__'; + +function Message() {} + +Message.prototype = { + + get user() { + return this.controller ? this.controller.user : null; + }, + + get session() { + return this.controller ? this.controller.session : null; + }, + + get sessionid() { + return this.controller && this.controller ? this.controller.req.sessionid : null; + }, + + get language() { + return (this.controller ? this.controller.language : '') || ''; + }, + + get ip() { + return this.controller ? this.controller.ip : null; + }, + + get id() { + return this.controller ? this.controller.id : null; + }, + + get req() { + return this.controller ? this.controller.req : null; + }, + + get res() { + return this.controller ? this.controller.res : null; + }, + + get params() { + return this.controller ? this.controller.params : null; + }, + + get files() { + return this.controller ? this.controller.files : null; + }, + + get body() { + return this.controller ? this.controller.body : null; + }, + + get query() { + return this.controller ? this.controller.query : null; + }, + + get headers() { + return this.controller && this.controller.req ? this.controller.req.headers : null; + }, + + get ua() { + return this.controller && this.controller.req ? this.controller.req.ua : null; + } +}; + +var MP = Message.prototype; + +MP.emit = function(name, a, b, c, d, e, f, g) { + + var self = this; + + if (!self.$events) + return self; + + var evt = self.$events[name]; + if (evt) { + var clean = false; + for (var i = 0, length = evt.length; i < length; i++) { + if (evt[i].$once) + clean = true; + evt[i].call(self, a, b, c, d, e, f, g); + } + if (clean) { + evt = evt.remove(n => n.$once); + self.$events[name] = evt.length ? evt : undefined; + } + } + return self; +}; + +MP.on = function(name, fn) { + var self = this; + if (!self.$events) + self.$events = {}; + if (self.$events[name]) + self.$events[name].push(fn); + else + self.$events[name] = [fn]; + return self; +}; + +MP.once = function(name, fn) { + fn.$once = true; + return this.on(name, fn); +}; + +MP.removeListener = function(name, fn) { + var self = this; + if (self.$events) { + var evt = self.$events[name]; + if (evt) { + evt = evt.remove(n => n === fn); + self.$events[name] = evt.length ? evt : undefined; + } + } + return self; +}; + +MP.removeAllListeners = function(name) { + if (this.$events) { + if (name === true) + this.$events = {}; + else if (name) + this.$events[name] = undefined; + else + this.$events = {}; + } + return this; +}; + +MP.clone = function() { + var self = this; + var obj = new Message(); + obj.$events = self.$events; + obj.duration = self.duration; + obj.repo = self.repo; + obj.main = self.main; + obj.count = self.count; + obj.data = self.data; + obj.used = self.used; + obj.processed = 0; + return obj; +}; + +MP.send = function(outputindex) { + + var self = this; + var outputs; + var count = 0; + + if (outputindex == null) { + if (self.schema.connections) { + outputs = Object.keys(self.schema.connections); + for (var i = 0; i < outputs.length; i++) + count += self.send(outputs[i]); + } + return count; + } + + var meta = self.main.meta; + var now = Date.now(); + + outputs = self.schema.connections ? (self.schema.connections[outputindex] || EMPTYARRAY) : EMPTYARRAY; + + if (self.processed === 0) { + self.processed = 1; + self.schema.stats.pending--; + self.schema.stats.output++; + self.schema.stats.duration = now - self.duration2; + } + + if (!self.main.$can(false, self.toid, outputindex)) + return count; + + for (var i = 0; i < outputs.length; i++) { + var output = outputs[i]; + + if (output.disabled || output.paused) + continue; + + var schema = meta.flow[output.id]; + if (schema && schema.component && self.main.$can(true, output.id, output.index)) { + var next = meta.components[schema.component]; + if (next && next.message) { + var inputindex = output.index; + var message = self.clone(); + message.used++; + message.from = self.to; + message.fromid = self.toid; + message.fromindex = outputindex; + message.fromcomponent = self.schema.component; + message.fromschema = self.toschema; + message.to = next; + message.toid = output.id; + message.toindex = inputindex; + message.tocomponent = schema.component; + message.toschema = message.schema = schema; + message.cache = schema.cache; + message.options = schema.options; + message.duration2 = now; + schema.stats.input++; + schema.stats.pending++; + self.$events.message && self.emit('message', message); + self.main.$events.message && self.main.emit('message', message); + setImmediate(sendmessage, next, message); + count++; + } + } + } + + return count; +}; + +MP.replace = function(data) { + this.data = data; + return this; +}; + +MP.destroy = function() { + + var self = this; + + if (self.processed === 0) { + self.processed = 1; + self.schema.stats.pending--; + self.schema.stats.output++; + self.schema.stats.duration = Date.now() - self.duration2; + } + + self.$events.end && self.emit('end', self); + self.main.$events.end && self.main.emit('end', self); + + self.repo = null; + self.main = null; + self.from = null; + self.to = null; + self.fromschema = null; + self.toschema = null; + self.data = null; + self.options = null; + self.duration = null; + self.duration2 = null; + self.$events = null; +}; + +function Flow(name) { + var t = this; + t.name = name; + t.meta = {}; + t.meta.components = {}; + t.meta.messages = 0; + t.meta.flow = {}; + t.meta.cache = {}; + t.$events = {}; + new framework_utils.EventEmitter2(t); +} + +var FP = Flow.prototype; + +FP.register = function(name, declaration) { + var self = this; + + if (typeof(declaration) === 'string') + declaration = new Function('instance', declaration); + + var cache; + var prev = self.meta.components[name]; + if (prev) { + cache = prev.cache; + prev.connected = false; + prev.disabled = true; + prev.destroy = null; + prev.disconnect && prev.disconnect(); + } + + var curr = { id: name, main: self, connected: true, disabled: false, cache: cache || {} }; + declaration(curr); + self.meta.components[name] = curr; + self.$events.register && self.emit('register', name, curr); + curr.install && !prev && curr.install(); + curr.connect && curr.connect(); + curr.destroy = function() { + self.unregister(name); + }; + return self; +}; + +FP.destroy = function() { + var self = this; + self.unregister(); + setTimeout(function() { + self.emit('destroy'); + self.meta = null; + self.$events = null; + }, 500); + delete F.flows[self.name]; +}; + +FP.unregister = function(name) { + + var self = this; + + if (name == null) { + var keys = Object.keys(self.meta.components); + for (var i = 0; i < keys.length; i++) + self.unregister(self.meta.components[keys[i]]); + return self; + } + + var curr = self.meta.components[name]; + if (curr) { + self.$events.unregister && self.emit('unregister', name, curr); + curr.connected = false; + curr.disabled = true; + curr.destroy = null; + curr.cache = null; + curr.disconnect && curr.disconnect(); + curr.uninstall && curr.uninstall(); + delete self.meta.components[name]; + } + return self; +}; + +FP.use = function(schema, callback) { + var self = this; + + if (typeof(schema) === 'string') + schema = schema.parseJSON(true); + + // schema.COMPONENT_ID.component = 'condition'; + // schema.COMPONENT_ID.options = {}; + // schema.COMPONENT_ID.connections = { '0': [{ id: 'COMPONENT_ID', index: '2' }] } + + var err = new ErrorBuilder(); + + if (schema) { + + var keys = Object.keys(schema); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + if (key === 'paused') + continue; + + var instance = schema[key]; + if (!instance.component) + continue; + + var component = self.meta.components[instance.component]; + schema[key].stats = { pending: 0, input: 0, output: 0, duration: 0 }; + schema[key].cache = {}; + + if (!component) + err.push(key, '"' + instance.component + '" component not found.'); + + component.options && component.options.call(schema[key], schema[key].options); + } + + self.meta.flow = schema; + } else + err.push('schema', 'Flow schema is invalid.'); + + self.$events.schema && self.emit('schema', schema); + callback && callback(err.length ? err : null); + return self; +}; + +function sendmessage(instance, message, event) { + + if (event) { + message.$events.message && message.emit('message', message); + message.main.$events.message && message.main.emit('message', message); + } + + instance.message(message); +} + +FP.$can = function(isinput, id, index) { + var self = this; + if (!self.meta.flow.paused) + return true; + var key = (isinput ? 'input' : 'output') + D + id + D + index; + if (!self.meta.flow.paused[key]) + return true; +}; + +// path = ID__INPUTINDEX +FP.trigger = function(path, data, controller, events) { + path = path.split(D); + var self = this; + var inputindex = path.length === 1 ? 0 : path[1]; + var schema = self.meta.flow[path[0]]; + if (schema && schema.component) { + var instance = self.meta.components[schema.component]; + if (instance && instance.message && self.$can(true, path[0], path[1])) { + + var message = new Message(); + + message.$events = events || {}; + message.duration = message.duration2 = Date.now(); + message.controller = controller; + + message.used = 1; + message.repo = {}; + message.main = self; + message.data = data; + message.count = self.meta.messages++; + + message.from = null; + message.fromid = null; + message.fromindex = null; + message.fromcomponent = null; + message.fromschema = null; + + message.to = instance; + message.toid = path[0]; + message.toindex = inputindex; + message.tocomponent = instance.id; + message.toschema = message.schema = schema; + message.cache = instance.cache; + + message.options = schema.options; + message.processed = 0; + + schema.stats.input++; + schema.stats.pending++; + setImmediate(sendmessage, instance, message, true); + return message; + } + } +}; + +FP.trigger2 = function(path, data, controller) { + var self = this; + var keys = Object.keys(self.meta.flow); + var events = {}; + var obj; + + path = path.split(D); + + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var flow = self.meta.flow[key]; + if (flow.component === path[0]) + obj = self.trigger(key + D + (path.length === 1 ? 0 : path[1]), data, controller, events); + } + + return obj; +}; + +FP.clear = function() { + var self = this; + self.meta.flow = {}; + return self; +}; + +FP.make = function(fn) { + var self = this; + fn.call(self, self); + return self; +}; + +exports.make = function(name) { + return new Flow(name); +}; \ No newline at end of file diff --git a/graphdb.js b/graphdb.js new file mode 100644 index 000000000..1917c48f3 --- /dev/null +++ b/graphdb.js @@ -0,0 +1,2582 @@ +// Copyright 2012-2018 (c) Peter Širka +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +/** + * @module FrameworkGraphDB + * @version 1.0.0 + */ + +const Fs = require('fs'); +const Zlib = require('zlib'); + +const ZLIBOPTIONS = { level: Zlib.constants.Z_FULL_FLUSH, memLevel: Zlib.constants.Z_BEST_COMPRESSION, strategy: Zlib.constants.Z_DEFAULT_STRATEGY }; +const VERSION = 1; +const DOCUMENTSIZE = 1000; +const PAGESIZE = 20; +const PAGELIMIT = 50; +const DATAOFFSET = 17; +const EMPTYBUFFER = U.createBufferSize(1); +const HEADERSIZE = 7000; +const DELAY = 100; +const REGTUNESCAPE = /%7C|%0D|%0A/g; +const REGTESCAPETEST = /\||\n|\r/; +const REGTESCAPE = /\||\n|\r/g; +const BOOLEAN = { '1': 1, 'true': 1, 'on': 1 }; +const DatabaseBuilder = framework_nosql.DatabaseBuilder; + +// STATES +const STATE_UNCOMPRESSED = 1; +const STATE_COMPRESSED = 2; +const STATE_REMOVED = 255; + +// META +const META_PAGE_ADD = 100; +const META_CLASSESRELATIONS = 101; +const META_PAGE_ADD3 = 102; +const META_RELATIONPAGEINDEX = 103; + +// OPERATIONS +const NEXT_READY = 1; +const NEXT_INSERT = 2; +const NEXT_RELATION = 3; +const NEXT_UPDATE = 4; +const NEXT_FIND = 5; +const NEXT_REMOVE = 6; +const NEXT_RESIZE = 7; +const NEXT_CONTINUE = 100; + +// TYPES +const TYPE_CLASS = 1; +const TYPE_RELATION = 2; +const TYPE_RELATION_DOCUMENT = 3; + +var IMPORTATOPERATIONS = 0; + +function GraphDB(name) { + + F.path.verify('databases'); + + var self = this; + self.name = name; + self.filename = F.path.databases(name + '.gdb'); + self.filenameBackup = self.filename.replace(/\.gdb$/, '.gdp-backup'); + self.ready = false; + + self.$classes = {}; + self.$relations = {}; + self.$events = {}; + + self.header = {}; + + self.pending = {}; + self.pending.insert = []; + self.pending.find = []; + self.pending.update = []; + self.pending.remove = []; + self.pending.relation = []; + self.pending.meta = []; + + self.states = {}; + self.states.resize = false; + self.states.insert = false; + self.states.read = false; + self.states.remove = false; + self.states.update = false; + + F.path.verify('databases'); + // t.open(); + + self.cb_error = function(err) { + err && console.log(err); + }; + + self.cb_next = function(value) { + self.next(value); + }; + + F.grapdbinstance = true; + self.open(); +} + +var GP = GraphDB.prototype; + +// ==== DB:HEADER (7000b) +// name (30b) = from: 0 +// version (1b) = from: 30 +// pages (4b) = from: 31 +// pagesize (2b) = from: 35 +// pagelimit (2b) = from: 37 +// documents (4b) = from: 39 +// documentsize (2b) = from: 43 +// classindex (1b) = from: 45 +// relationindex (1b) = from: 46 +// relationnodeindex = from: 47 +// classes + relations = from: 51 + +// ==== DB:PAGE (20b) +// type (1b) = from: 0 +// index (1b) = from: 1 +// documents (2b) = from: 2 +// freeslots (1b) = from: 4 +// parentindex (4b) = from: 5 + +// ==== DB:DOCUMENT (SIZE) +// type (1b) = from: 0 +// index (1b) = from: 1 +// state (1b) = from: 2 +// pageindex (4b) = from: 3 +// relationindex (4b) = from: 7 (it's for relations between two documents in TYPE_RELATION page) +// parentindex (4b) = from: 11 +// size/count (2b) = from: 15 +// data = from: 17 + +// Creates new page +function addPage(self, type, index, parentindex, callback) { + + // @type + // 1: classes + // 2: relations + // 3: relations private + + // @index + // index of value + + // Add a new page + self.header.pages++; + + var indexer = self.header.pages; + var buffer = []; + var page = U.createBufferSize(self.header.pagesize); + + // console.log('CREATING PAGE:', TYPES[type], indexer, type, index); + + page.writeUInt8(type, 0); // type (1:class, 2:relation, 3:private) + page.writeUInt8(index, 1); // index + page.writeUInt16LE(0, 2); // documents + page.writeUInt8(0, 4); // freeslots + page.writeUInt32LE(parentindex, 5); // parentindex + + buffer.push(page); + + for (var i = 0; i < self.header.pagelimit; i++) { + var doc = U.createBufferSize(self.header.documentsize); + doc.writeUInt8(type, 0); + doc.writeUInt8(index, 1); + doc.writeUInt8(STATE_REMOVED, 2); + doc.writeUInt32LE(self.header.pages, 3); + doc.writeUInt32LE(0, 7); // continuerindex + doc.writeUInt32LE(0, 11); // parentindex + doc.writeUInt16LE(0, 15); // size/count + buffer.push(doc); + } + + buffer = Buffer.concat(buffer); + + var offset = offsetPage(self, indexer); + + Fs.write(self.fd, buffer, 0, buffer.length, offset, function(err) { + err && self.error(err, 'createPage.write'); + !err && updMeta(self, type === TYPE_RELATION_DOCUMENT ? META_PAGE_ADD3 : META_PAGE_ADD); + callback && callback(err, indexer); + }); + + return indexer; +} + +function addNodeFree(self, meta, callback) { + + if (!meta.type.findfreeslots) { + addNode(self, meta, callback); + return; + } + + findDocumentFree(self, meta.type.pageindex, function(err, documentindex, pageindex) { + + if (!documentindex) { + meta.type.findfreeslots = false; + addNode(self, meta, callback); + return; + } + + var buffer = U.createBufferSize(self.header.documentsize); + buffer.writeUInt8(meta.typeid, 0); // type + buffer.writeUInt8(meta.type.index, 1); // index + buffer.writeUInt32LE(pageindex, 3); // pageindex + buffer.writeUInt8(meta.state || STATE_UNCOMPRESSED, 2); // state + buffer.writeUInt32LE(meta.relationindex || 0, 7); // relationindex + buffer.writeUInt32LE(meta.parentindex || 0, 11); // parentindex + buffer.writeUInt16LE(meta.size, 15); + meta.data && meta.data.copy(buffer, DATAOFFSET); + + Fs.write(self.fd, buffer, 0, buffer.length, offsetDocument(self, documentindex), function() { + meta.type.locked = false; + callback(null, documentindex, pageindex); + }); + }); +} + +function addNode(self, meta, callback) { + + // meta.typeid (1 CLASS, 2 RELATION) + // meta.type (link to type class/relation) + // meta.state + // meta.parentindex + // meta.relationindex + // meta.size + // meta.buffer + + var buf = U.createBufferSize(self.header.pagesize); + var offset = offsetPage(self, meta.type.pageindex); + + meta.type.locked = true; + + Fs.read(self.fd, buf, 0, buf.length, offset, function(err) { + + if (err) + throw err; + + if (buf[0] !== meta.typeid) + throw new Error('Not a class page'); + + if (!meta.type.private && buf[1] !== meta.type.index) + throw new Error('Not same class type'); + + // type : buf[0] + // index : buf[1] + // documents : buf.readUInt16LE(2) + // freeslots : buf[4] + // parentindex : readUInt32LE(5) + + var buffer = U.createBufferSize(self.header.documentsize); + buffer.writeUInt8(buf[0], 0); // type + buffer.writeUInt8(meta.type.index, 1); // index + buffer.writeUInt32LE(meta.type.pageindex, 3); // pageindex + buffer.writeUInt8(meta.state || STATE_UNCOMPRESSED, 2); // state + buffer.writeUInt32LE(meta.relationindex || 0, 7); // relationindex + buffer.writeUInt32LE(meta.parentindex || 0, 11); // parentindex + buffer.writeUInt16LE(meta.size, 15); + meta.data && meta.data.copy(buffer, DATAOFFSET); + + var documents = buf.readUInt16LE(2); + var documentsbuf = U.createBufferSize(2); + + documents++; + documentsbuf.writeUInt16LE(documents); + + Fs.write(self.fd, documentsbuf, 0, documentsbuf.length, offset + 2, function(err) { + + err && console.log('addNode.write.meta', err); + Fs.write(self.fd, buffer, 0, buffer.length, offset + self.header.pagesize + ((documents - 1) * self.header.documentsize), function(err) { + + err && console.log('addNode.write.data', err); + + // type (1b) = from: 0 + // index (1b) = from: 1 + // state (1b) = from: 2 + // pageindex (4b) = from: 3 + // continuerindex (4b) = from: 7 + // parentindex (4b) = from: 11 + // size/count (2b) = from: 15 + // data = from: 17 + + // We must create a new page + if (documents + 1 > self.header.pagelimit) { + addPage(self, meta.typeid, meta.type.index, meta.type.pageindex, function(err, index) { + + var documentindex = getDocumentIndex(self, meta.type.pageindex, documents); + meta.type.documentindex = documentindex; + meta.type.pageindex = index; + meta.type.locked = false; + + // Problem with classes + // meta.type.index = 0; + + if (meta.type.private) + self.header.relationpageindex = index; + + updMeta(self, meta.type.private ? META_RELATIONPAGEINDEX : META_CLASSESRELATIONS); + callback(null, documentindex, index); + }); + } else { + var documentindex = getDocumentIndex(self, meta.type.pageindex, documents); + meta.type.locked = false; + meta.type.documentindex = documentindex; + callback(null, documentindex, meta.type.pageindex); + } + }); + }); + }); +} + +function addDocument(self, cls, value, callback) { + + // meta.typeid (1 CLASS, 2 RELATION) + // meta.type (link to type class/relation) + // meta.state + // meta.parentindex + // meta.relationindex + // meta.size + // meta.data + + var meta = {}; + meta.type = cls; + meta.typeid = TYPE_CLASS; + meta.state = 1; + meta.parentindex = 0; + meta.relationindex = 0; + meta.data = U.createBuffer(stringifyData(cls.schema, value)); + meta.size = meta.data.length; + + var limit = self.header.documentsize - DATAOFFSET; + + if (meta.data.length > limit) { + Zlib.deflate(meta.data, ZLIBOPTIONS, function(err, buf) { + if (err || buf.length > limit) + callback(new Error('GraphDB: Data too long'), 0); + else { + meta.state = STATE_COMPRESSED; + meta.data = buf; + meta.size = buf.length; + addNodeFree(self, meta, callback); + } + }); + } else + addNodeFree(self, meta, callback); +} + +function addRelation(self, relation, indexA, indexB, callback) { + + // Workflow: + // Has "A" relation nodes? + // Has "B" relation nodes? + // Create "A" relation with "B" + // Create "B" relation with "A" + // Register relation to global relations + + var tasks = []; + var relA = null; + var relB = null; + + var tmprelation = { index: relation.index, pageindex: 0, documentindex: 0, locked: false, private: true }; + + tasks.push(function(next) { + self.read(indexA, function(err, doc, relid) { + if (doc) { + relA = relid; + next(); + } else { + tasks = null; + next = null; + callback(new Error('GraphDB: Node (A) "{0}" not exists.'.format(indexA))); + } + }); + }); + + tasks.push(function(next) { + self.read(indexB, function(err, doc, relid) { + if (doc) { + relB = relid; + next(); + } else { + tasks = null; + next = null; + callback(new Error('GraphDB: Node (B) "{0}" not exists.'.format(indexB))); + } + }); + }); + + tasks.push(function(next) { + + if (relA == 0) { + next(); + return; + } + + checkRelation(self, relation, relA, indexB, function(err, is) { + if (is) { + tasks = null; + next = null; + callback(new Error('GraphDB: Same relation already exists between nodes (A) "{0}" and (B) "{1}".'.format(indexA, indexB))); + } else + next(); + }); + }); + + // Obtaining indexA a relation document + tasks.push(function(next) { + + if (F.isKilled) + return; + + IMPORTATOPERATIONS++; + + if (relA) + next(); + else { + addRelationDocument(self, relation, indexA, function(err, index) { + relA = index; + next(); + }, true); + } + }); + + // Obtaining indexB a relation document + tasks.push(function(next) { + + if (F.isKilled) + return; + + if (relB) + next(); + else { + addRelationDocument(self, relation, indexB, function(err, index) { + relB = index; + next(); + }, true); + } + }); + + // Push "indexB" relation to "indexA" + tasks.push(function(next) { + tmprelation.documentindex = relA; + tmprelation.pageindex = self.header.relationpageindex; + pushRelationDocument(self, relA, tmprelation, indexB, true, function(err, index) { + // Updated relation, document was full + if (relA !== index) { + relA = index; + updDocumentRelation(self, indexA, relA, next); + } else + next(); + }, true); + }); + + tasks.push(function(next) { + tmprelation.documentindex = relB; + tmprelation.pageindex = self.header.relationpageindex; + pushRelationDocument(self, relB, tmprelation, indexA, false, function(err, index) { + // Updated relation, document was full + if (relB !== index) { + relB = index; + updDocumentRelation(self, indexB, relB, next); + } else + next(); + }, true); + }); + + tasks.push(function(next) { + // console.log('PUSH COMMON', relation.documentindex, indexA); + pushRelationDocument(self, relation.documentindex, relation, indexA, true, next); + }); + + tasks.async(function() { + IMPORTATOPERATIONS--; + // console.log('REL ====', relA, relB); + callback(null, true); + }); +} + +function remRelation(self, relation, indexA, indexB, callback) { + + var tasks = []; + var relA = null; + var relB = null; + + tasks.push(function(next) { + self.read(indexA, function(err, doc, relid) { + if (doc) { + relA = relid; + next(); + } else { + tasks = null; + next = null; + callback(new Error('GraphDB: Node (A) "{0}" not exists.'.format(indexA))); + } + }); + }); + + tasks.push(function(next) { + self.read(indexB, function(err, doc, relid) { + if (doc) { + relB = relid; + next(); + } else { + tasks = null; + next = null; + callback(new Error('GraphDB: Node (B) "{0}" not exists.'.format(indexB))); + } + }); + }); + + tasks.async(function() { + + if (F.isKilled) + return; + + IMPORTATOPERATIONS++; + remRelationLink(self, relA, indexB, function(err, countA) { + remRelationLink(self, relB, indexA, function(err, countB) { + remRelationLink(self, relation.documentindex, indexA, function(err, countC) { + IMPORTATOPERATIONS--; + callback(null, (countA + countB + countC) > 1); + }); + }); + }); + }); +} + +function remRelationLink(self, index, documentindex, callback, nochild, counter) { + + var buf = U.createBufferSize(self.header.documentsize); + var offset = offsetDocument(self, index); + + !counter && (counter = 0); + + Fs.read(self.fd, buf, 0, buf.length, offset, function() { + + // type (1b) = from: 0 + // index (1b) = from: 1 + // state (1b) = from: 2 + // pageindex (4b) = from: 3 + // relationindex (4b) = from: 7 (it's for relations between two documents in TYPE_RELATION page) + // parentindex (4b) = from: 11 + // size/count (2b) = from: 15 + // data = from: 17 + + if ((buf[0] !== TYPE_RELATION && buf[0] !== TYPE_RELATION_DOCUMENT) || (buf[2] === STATE_REMOVED)) { + callback(null, counter); + return; + } + + var relid = buf.readUInt32LE(7); + var count = buf.readUInt16LE(15); + var arr = []; + var is = false; + + for (var i = 0; i < count; i++) { + var off = DATAOFFSET + (i * 6); + var obj = {}; + obj.INDEX = buf[off]; + obj.INIT = buf[off + 1]; + obj.ID = buf.readUInt32LE(off + 2); + if (obj.ID === documentindex && obj.INIT === 1) + is = true; + else + arr.push(obj); + } + + if (is) { + count = arr.length; + for (var i = 0; i < count; i++) { + var off = DATAOFFSET + (i * 6); + var obj = arr[i]; + buf.writeUInt8(obj.INDEX, off); + buf.writeUInt8(obj.INIT, off + 1); + buf.writeUInt32LE(obj.ID, off + 2); + } + buf.writeUInt16LE(count, 15); + buf.fill(EMPTYBUFFER, DATAOFFSET + ((count + 1) * 6)); + Fs.write(self.fd, buf, 0, buf.length, offset, function() { + counter++; + if (relid && !nochild) + setImmediate(remRelationLink, self, relid, documentindex, callback, null, counter); + else + callback(null, counter); + }); + } else if (relid && !nochild) + setImmediate(remRelationLink, self, relid, documentindex, callback, null, counter); + else + callback(null, counter); + }); +} + +// Traverses all RELATIONS documents and remove specific "documentindex" +function remRelationAll(self, index, documentindex, callback, counter) { + + var buf = U.createBufferSize(self.header.pagelimit * self.header.documentsize); + var offset = offsetDocument(self, index); + + !counter && (counter = 0); + + Fs.read(self.fd, buf, 0, buf.length, offset, function(err, size) { + + if (err || !size) { + callback(null, counter); + return; + } + + // type (1b) = from: 0 + // index (1b) = from: 1 + // state (1b) = from: 2 + // pageindex (4b) = from: 3 + // relationindex (4b) = from: 7 (it's for relations between two documents in TYPE_RELATION page) + // parentindex (4b) = from: 11 + // size/count (2b) = from: 15 + // data = from: 17 + + var removed = []; + + while (true) { + + if (!buf.length) + break; + + index++; + + var data = buf.slice(0, self.header.documentsize); + + if ((data[0] !== TYPE_RELATION && data[0] !== TYPE_RELATION_DOCUMENT) || (data[2] === STATE_REMOVED)) { + buf = buf.slice(self.header.documentsize); + continue; + } + + var count = data.readUInt16LE(15); + var arr = []; + var is = false; + + for (var i = 0; i < count; i++) { + var off = DATAOFFSET + (i * 6); + var obj = {}; + obj.INDEX = data[off]; + obj.INIT = data[off + 1]; + obj.ID = data.readUInt32LE(off + 2); + if (obj.ID === documentindex) + is = true; + else + arr.push(obj); + } + + if (is) { + + var newcount = arr.length; + + for (var i = 0; i < newcount; i++) { + var off = DATAOFFSET + (i * 6); + var obj = arr[i]; + data.writeUInt8(obj.INDEX, off); + data.writeUInt8(obj.INIT, off + 1); + data.writeUInt32LE(obj.ID, off + 2); + } + + data.writeUInt16LE(newcount, 15); + data.fill(EMPTYBUFFER, DATAOFFSET + ((newcount + 1) * 6)); + + removed.push({ index: index - 1, buf: data }); + } + + buf = buf.slice(self.header.documentsize); + } + + if (!removed.length) { + setImmediate(remRelationAll, self, index, documentindex, callback, counter); + return; + } + + counter += removed.length; + removed.wait(function(item, next) { + Fs.write(self.fd, item.buf, 0, item.buf.length, offsetDocument(self, item.index), next); + }, function() { + setImmediate(remRelationAll, self, index, documentindex, callback, counter); + }); + + }); +} + +function addRelationDocument(self, relation, index, callback, between) { + + // meta.typeid (1 CLASS, 2 RELATION, 3 PRIVATE RELATION) + // meta.type (link to type class/relation) + // meta.state + // meta.parentindex + // meta.relationindex + // meta.size + // meta.data + + var meta = {}; + meta.typeid = between ? TYPE_RELATION_DOCUMENT : TYPE_RELATION; + meta.type = between ? { index: 0, pageindex: self.header.relationpageindex, documentindex: index, locked: false, private: true } : relation; + meta.state = 1; + meta.parentindex = 0; + meta.relationindex = 0; + meta.size = 0; + + // Creates a new node + addNode(self, meta, function(err, relationindex) { + + // Updates exiting document by updating relation index + updDocumentRelation(self, index, relationindex, function(err) { + // Returns a new relation index + callback(err, relationindex); + }); + }); +} + +function findDocumentFree(self, pageindex, callback, ready) { + + var offset = offsetPage(self, pageindex); + var buf = U.createBufferSize(self.header.pagesize); + + Fs.read(self.fd, buf, 0, buf.length, offset, function() { + + // ==== DB:PAGE (20b) + // type (1b) = from: 0 + // index (1b) = from: 1 + // documents (2b) = from: 2 + // freeslots (1b) = from: 4 + // parentindex (4b) = from: 5 + + var relid = buf.readUInt32LE(5); + if (!relid) { + if (!ready) { + callback(null, 0); + return; + } + } + + // First page is the last page saved in meta therefore is needed to perform recursive with "ready" + if (!ready) { + findDocumentFree(self, relid, callback, true); + return; + } + + var documents = buf.readUInt16LE(2); + if (documents >= self.header.pagelimit) { + // Finds in parent if exists + if (relid) + findDocumentFree(self, relid, callback, true); + else + callback(null, 0); + return; + } + + // Finds a free document slot + var index = getDocumentIndex(self, pageindex) - 1; + var buffer = U.createBufferSize(self.header.pagelimit * self.header.documentsize); + + Fs.read(self.fd, buffer, 0, buffer.length, offset + self.header.pagesize, function() { + while (true) { + index++; + var data = buffer.slice(0, self.header.documentsize); + if (!data.length) + break; + + if (data[2] === STATE_REMOVED) { + + if (F.isKilled) + return; + + updPageMeta(self, pageindex, function(err, buf) { + buf.writeUInt16LE(documents + 1, 2); + setImmediate(callback, null, index, pageindex); + }); + buffer = buffer.slice(self.header.documentsize); + return; + } + } + + if (relid) + findDocumentFree(self, relid, callback, true); + else + callback(null, 0); + + }); + }); +} + +// Finds a free space for new relation in "pushRelationDocument" +function findRelationDocument(self, relid, callback) { + + if (!relid) { + callback(null, 0); + return; + } + + var offset = offsetDocument(self, relid); + var buf = U.createBufferSize(self.header.documentsize); + + Fs.read(self.fd, buf, 0, buf.length, offset, function(err, size) { + + if (err || !size) { + callback(err, 0); + return; + } + + var count = buf.readUInt16LE(15); + if (count + 1 > self.header.relationlimit) { + // Checks if the relation index has next relation + + if (relid === buf.readUInt32LE(7)) + return; + + relid = buf.readUInt32LE(7); + if (relid) + setImmediate(findRelationDocument, self, relid, callback); + else + callback(null, 0); + } else { + // Free space for this relation + callback(null, relid); + } + }); +} + +// Pushs "documentindex" to "index" document (document with all relations) +function pushRelationDocument(self, index, relation, documentindex, initializator, callback, between, recovered) { + + var offset = offsetDocument(self, index); + var buf = U.createBufferSize(self.header.documentsize); + + Fs.read(self.fd, buf, 0, buf.length, offset, function() { + + // type (1b) = from: 0 + // index (1b) = from: 1 + // state (1b) = from: 2 + // pageindex (4b) = from: 3 + // relationindex (4b) = from: 7 (it's for relations between two documents in TYPE_RELATION page) + // parentindex (4b) = from: 11 + // size/count (2b) = from: 15 + // data = from: 17 + + var count = buf.readUInt16LE(15); + if (count + 1 > self.header.relationlimit) { + findRelationDocument(self, buf.readUInt32LE(7), function(err, newindex) { + + // Is some relation document exist? + if (newindex && !recovered) { + pushRelationDocument(self, newindex, relation, documentindex, initializator, callback, between, true); + return; + } + + // meta.typeid (1 CLASS, 2 RELATION) + // meta.type (link to type class/relation) + // meta.state + // meta.parentindex + // meta.relationindex + // meta.size + // meta.buffer + + var meta = {}; + meta.typeid = relation.private ? TYPE_RELATION_DOCUMENT : TYPE_RELATION; + meta.type = relation; + meta.state = STATE_UNCOMPRESSED; + meta.parentindex = 0; + meta.relationindex = index; + meta.size = 0; + + addNode(self, meta, function(err, docindex, pageindex) { + relation.pageindex = pageindex; + relation.documentindex = docindex; + updDocumentRelation(self, relation.documentindex, index, function() { + updDocumentParent(self, index, relation.documentindex, function() { + pushRelationDocument(self, relation.documentindex, relation, documentindex, initializator, callback, between); + }); + }); + }); + }); + + } else { + + var buffer = U.createBufferSize(6); + buffer.writeUInt8(relation.index, 0); + buffer.writeUInt8(initializator ? 1 : 0, 1); + buffer.writeUInt32LE(documentindex, 2); + buffer.copy(buf, DATAOFFSET + (count * 6)); + buf.writeUInt16LE(count + 1, 15); + + if (buf[2] === STATE_REMOVED) { + // We must update counts of documents in the page meta + var pageindex = Math.ceil(index / self.header.pagelimit); + updPageMeta(self, pageindex, function(err, buf) { + + // type (1b) = from: 0 + // index (1b) = from: 1 + // documents (2b) = from: 2 + // freeslots (1b) = from: 4 + // parentindex (4b) = from: 5 + + buf.writeUInt16LE(buf.readUInt16LE(2) + 1, 2); + setImmediate(function() { + Fs.write(self.fd, buf, 0, buf.length, offset, function(err) { + err && self.error(err, 'pushRelationDocument.read.write'); + callback(null, index); + }); + }); + }); + + buf.writeUInt8(STATE_UNCOMPRESSED, 2); + + } else { + // DONE + Fs.write(self.fd, buf, 0, buf.length, offset, function(err) { + err && self.error(err, 'pushRelationDocument.read.write'); + callback(null, index); + }); + } + } + + }); +} + +function updDocumentRelation(self, index, relationindex, callback) { + + if (index === relationindex) + throw new Error('FET'); + + var offset = offsetDocument(self, index); + var buf = U.createBufferSize(4); + buf.writeUInt32LE(relationindex); + Fs.write(self.fd, buf, 0, buf.length, offset + 7, callback); +} + +function updDocumentParent(self, index, parentindex, callback) { + var offset = offsetDocument(self, index); + var buf = U.createBufferSize(4); + buf.writeUInt32LE(parentindex); + Fs.write(self.fd, buf, 0, buf.length, offset + 11, callback); +} + +function updPageMeta(self, index, fn) { + var offset = offsetPage(self, index); + var buf = U.createBufferSize(self.header.pagesize); + Fs.read(self.fd, buf, 0, buf.length, offset, function() { + fn(null, buf); + Fs.write(self.fd, buf, 0, buf.length, offset, self.cb_error); + }); +} + +function remDocument(self) { + if (!self.ready || self.states.remove || !self.pending.remove.length || F.isKilled) + return; + self.states.remove = true; + var doc = self.pending.remove.shift(); + IMPORTATOPERATIONS++; + remRelationAll(self, doc.id, doc.id, function() { + remDocumentAll(self, doc.id, function(err, count) { + IMPORTATOPERATIONS--; + self.states.remove = false; + doc.callback && doc.callback(err, count); + setImmediate(self.cb_next, NEXT_REMOVE); + }); + }); +} + +function remDocumentAll(self, index, callback, count) { + + var offset = offsetDocument(self, index); + var buf = U.createBufferSize(17); + + // type (1b) = from: 0 + // index (1b) = from: 1 + // state (1b) = from: 2 + // pageindex (4b) = from: 3 + // relationindex (4b) = from: 7 (it's for relations between two documents in TYPE_RELATION page) + // parentindex (4b) = from: 11 + // size/count (2b) = from: 15 + // data = from: 17 + + if (!count) + count = 0; + + Fs.read(self.fd, buf, 0, buf.length, offset, function() { + + var relid = buf.readUInt32LE(7); + + if (buf[2] === STATE_REMOVED) { + if (relid) + remDocumentAll(self, relid, callback, count); + else + callback(null, count); + return; + } + + buf.writeUInt8(STATE_REMOVED, 2); + buf.writeUInt16LE(0, 15); + + if (buf[0] === TYPE_CLASS) + self.$classes[buf[1]].findfreeslots = true; + + var pageindex = buf.readUInt32LE(3); + + Fs.write(self.fd, buf, 0, buf.length, offset, function() { + + // Updates "documents" in the current page + updPageMeta(self, pageindex, function(err, buf) { + + // type (1b) = from: 0 + // index (1b) = from: 1 + // documents (2b) = from: 2 + // freeslots (1b) = from: 4 + // parentindex (4b) = from: 5 + + var documents = buf.readUInt16LE(2); + buf.writeUInt16LE(documents > 0 ? documents - 1 : documents, 2); + count++; + + setImmediate(function() { + if (relid) + remDocumentAll(self, relid, callback, count); + else + callback(null, count); + }); + }); + }); + }); +} + +function offsetPage(self, index) { + return HEADERSIZE + ((index - 1) * (self.header.pagesize + (self.header.pagelimit * self.header.documentsize))); +} + +function offsetDocument(self, index) { + var page = Math.ceil(index / self.header.pagelimit); + var offset = page * self.header.pagesize; + return HEADERSIZE + offset + ((index - 1) * self.header.documentsize); +} + +function getIndexPage(self, offset) { + return ((offset - HEADERSIZE) / (self.header.pagesize + (self.header.pagelimit * self.header.documentsize))); +} + +function getDocumentIndex(self, pageindex, count) { + return ((pageindex - 1) * self.header.pagelimit) + (count || 1); +} + +function checkRelation(self, relation, indexA, indexB, callback) { + + self.read(indexA, function(err, docs, relid) { + + if (docs) { + for (var i = 0; i < docs.length; i++) { + var doc = docs[i]; + if (doc.ID === indexB && (relation.both || doc.INIT)) { + callback(null, true); + return; + } + } + } + + if (relid) + setImmediate(checkRelation, self, relation, relid, indexB, callback); + else + callback(null, false); + }); +} + +function updMeta(self, type) { + var buf; + switch (type) { + + case META_PAGE_ADD: + buf = U.createBufferSize(4); + buf.writeUInt32LE(self.header.pages); + Fs.write(self.fd, buf, 0, buf.length, 31, self.cb_error); + break; + + case META_PAGE_ADD3: + buf = U.createBufferSize(4); + buf.writeUInt32LE(self.header.pages, 0); + Fs.write(self.fd, buf, 0, buf.length, 31, function() { + buf.writeUInt32LE(self.header.relationpageindex, 0); + Fs.write(self.fd, buf, 0, buf.length, 47, self.cb_error); + }); + break; + + case META_RELATIONPAGEINDEX: + buf = U.createBufferSize(4); + buf.writeUInt32LE(self.header.relationpageindex, 0); + Fs.write(self.fd, buf, 0, buf.length, 47, self.cb_error); + break; + + case META_CLASSESRELATIONS: + + var obj = {}; + obj.c = []; // classes + obj.r = []; // relations + + for (var i = 0; i < self.header.classindex; i++) { + var item = self.$classes[i + 1]; + obj.c.push({ n: item.name, i: item.index, p: item.pageindex, r: item.schema.raw, d: item.documentindex }); + } + + for (var i = 0; i < self.header.relationindex; i++) { + var item = self.$relations[i + 1]; + obj.r.push({ n: item.name, i: item.index, p: item.pageindex, b: item.both ? 1 :0, d: item.documentindex }); + } + + buf = U.createBufferSize(HEADERSIZE - 45); + buf.writeUInt8(self.header.classindex, 0); + buf.writeUInt8(self.header.relationindex, 1); + buf.writeUInt32LE(self.header.relationpageindex, 2); + buf.write(JSON.stringify(obj), 6); + Fs.write(self.fd, buf, 0, buf.length, 45, self.cb_error); + break; + } +} + +function insDocument(self) { + + if (!self.ready || self.states.insert || !self.pending.insert.length || F.isKilled) + return; + + var doc = self.pending.insert.shift(); + if (doc) { + + var cls = self.$classes[doc.name]; + if (cls == null) { + doc.callback(new Error('GraphDB: Class "{0}" not found.'.format(doc.name))); + return; + } + + if (cls.locked || !cls.ready) { + self.pending.insert.push(doc); + setTimeout(self.cb_next, DELAY, NEXT_INSERT); + return; + } + + self.states.insert = true; + + addDocument(self, cls, doc.value, function(err, id) { + // setTimeout(insDocument, 100, self); + self.states.insert = false; + setImmediate(insDocument, self); + doc.callback(err, id); + }); + } +} + +function updDocument(self) { + + if (!self.ready || self.states.update || !self.pending.update.length || F.isKilled) + return; + + var upd = self.pending.update.shift(); + if (upd) { + self.states.update = true; + + var offset = offsetDocument(self, upd.id); + var buf = U.createBufferSize(self.header.documentsize); + + Fs.read(self.fd, buf, 0, buf.length, offset, function(err, size) { + + if (err) { + self.states.update = false; + upd.callback(err); + setImmediate(self.cb_next, NEXT_UPDATE); + return; + } + + if (!size) { + upd.callback(null, 0); + self.states.update = false; + setImmediate(self.cb_next, NEXT_UPDATE); + return; + } + + var save = function(err) { + self.states.update = false; + !err && Fs.write(self.fd, buf, 0, buf.length, offset, self.cb_error); + upd.callback(err, err ? 0 : 1); + setImmediate(self.cb_next, NEXT_UPDATE); + }; + + var data = buf.slice(DATAOFFSET, buf.readUInt16LE(15) + DATAOFFSET); + var limit = self.header.documentsize - DATAOFFSET; + var schema = self.$classes[buf[1]].schema; + var doc; + + if (buf[2] === STATE_COMPRESSED) { + Zlib.inflate(data, ZLIBOPTIONS, function(err, buffer) { + doc = parseData(schema, buffer.toString('utf8').split('|')); + buffer = U.createBuffer(stringifyData(schema, upd.fn(doc, upd.value))); + if (buffer.length > limit) { + Zlib.deflate(buffer, ZLIBOPTIONS, function(err, buffer) { + if (buffer.length <= limit) { + buf.writeUInt16LE(buffer.length, 15); + buf.writeUInt8(STATE_COMPRESSED, 2); + buffer.copy(buf, DATAOFFSET); + save(); + } else + save(new Error('GraphDB: Data too long')); + }); + } else { + buf.writeUInt16LE(buffer.length, 15); + buf.writeUInt8(STATE_UNCOMPRESSED, 2); + buffer.copy(buf, DATAOFFSET); + save(); + } + }); + } else { + doc = parseData(schema, data.toString('utf8').split('|')); + var o = stringifyData(schema, upd.fn(doc, upd.value)); + var buffer = U.createBuffer(o); + if (buffer.length > limit) { + Zlib.deflate(buffer, ZLIBOPTIONS, function(err, buffer) { + if (buffer.length <= limit) { + buf.writeUInt16LE(buffer.length, 15); + buf.writeUInt8(STATE_COMPRESSED, 2); + buffer.copy(buf, DATAOFFSET); + save(); + } else + save(new Error('GraphDB: Data too long')); + }); + } else { + buf.writeUInt16LE(buffer.length, 15); + buf.writeUInt8(STATE_UNCOMPRESSED, 2); + buffer.copy(buf, DATAOFFSET); + save(); + } + } + }); + } +} + +function insRelation(self) { + + if (!self.ready || self.states.relation) + return; + + var doc = self.pending.relation.shift(); + if (doc) { + + var rel = self.$relations[doc.name]; + if (rel == null) { + doc.callback(new Error('GraphDB: Relation "{0}" not found.'.format(doc.name))); + return; + } + + if (rel.locked || !rel.ready) { + self.pending.relation.push(doc); + setTimeout(insRelation, DELAY, self); + return; + } + + self.states.relation = true; + + if (doc.connect) { + addRelation(self, rel, doc.indexA, doc.indexB, function(err, id) { + self.states.relation = false; + doc.callback(err, id); + setImmediate(insRelation, self); + }); + } else { + remRelation(self, rel, doc.indexA, doc.indexB, function(err, id) { + self.states.relation = false; + doc.callback(err, id); + setImmediate(insRelation, self); + }); + } + } +} + +GP.create = function(filename, documentsize, callback) { + var self = this; + Fs.unlink(filename, function() { + var buf = U.createBufferSize(HEADERSIZE); + buf.write('Total.js GraphDB embedded', 0); + buf.writeUInt8(VERSION, 30); // version + buf.writeUInt32LE(0, 31); // pages + buf.writeUInt16LE(PAGESIZE, 35); // pagesize + buf.writeUInt16LE(PAGELIMIT, 37); // pagelimit + buf.writeUInt32LE(0, 39); // documents + buf.writeUInt16LE(documentsize, 43); // documentsize + buf.writeUInt8(0, 45); // classindex + buf.writeUInt8(0, 46); // relationindex + buf.writeUInt8(0, 47); // relationpageindex + buf.write('{"c":[],"r":[]}', 51); // classes and relations + Fs.open(filename, 'w', function(err, fd) { + Fs.write(fd, buf, 0, buf.length, 0, function(err) { + err && self.error(err, 'create'); + Fs.close(fd, function() { + callback && callback(); + }); + }); + }); + }); + return self; +}; + +GP.open = function() { + var self = this; + Fs.stat(self.filename, function(err, stat) { + if (err) { + // file not found + self.create(self.filename, DOCUMENTSIZE, () => self.open()); + } else { + self.header.size = stat.size; + Fs.open(self.filename, 'r+', function(err, fd) { + self.fd = fd; + err && self.error(err, 'open'); + var buf = U.createBufferSize(HEADERSIZE); + Fs.read(self.fd, buf, 0, buf.length, 0, function() { + + self.header.pages = buf.readUInt32LE(31); + self.header.pagesize = buf.readUInt16LE(35); + self.header.pagelimit = buf.readUInt16LE(37); + self.header.documents = buf.readUInt32LE(39); + self.header.documentsize = buf.readUInt16LE(43); + + var size = F.config['graphdb.' + self.name] || DOCUMENTSIZE; + if (size > self.header.documentsize) { + setTimeout(function() { + self.next(NEXT_RESIZE); + }, DELAY); + } + + self.header.relationlimit = ((self.header.documentsize - DATAOFFSET) / 6) >> 0; + self.header.classindex = buf[45]; + self.header.relationindex = buf[46]; + self.header.relationpageindex = buf.readUInt32LE(47); + + var data = buf.slice(51, buf.indexOf(EMPTYBUFFER, 51)).toString('utf8'); + var meta = data.parseJSON(true); + + for (var i = 0; i < meta.c.length; i++) { + var item = meta.c[i]; + self.class(item.n, item.r, item); + } + + for (var i = 0; i < meta.r.length; i++) { + var item = meta.r[i]; + self.relation(item.n, item.b === 1, item); + } + + !self.header.relationpageindex && addPage(self, TYPE_RELATION_DOCUMENT, 0, 0, function(err, index) { + self.header.relationpageindex = index; + }); + + self.ready = true; + self.next(NEXT_READY); + }); + }); + } + }); + return self; +}; + +GP.next = function(type) { + + var self = this; + var tmp; + + switch (type) { + case NEXT_READY: + for (var i = 0; i < self.pending.meta.length; i++) { + tmp = self.pending.meta[i]; + if (tmp.type === TYPE_CLASS) + self.class(tmp.name, tmp.data); + else + self.relation(tmp.name, tmp.data); + } + self.emit('ready'); + break; + + case NEXT_RESIZE: + + clearTimeout(self.$resizedelay); + self.$resizedelay = setTimeout(function() { + if (!self.states.resize) { + self.ready = false; + self.states.resize = true; + var size = (F.config['graphdb.' + self.name] || DOCUMENTSIZE); + var meta = { documentsize: size > self.header.documentsize ? size : self.header.documentsize }; + var keys = Object.keys(self.$classes); + + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var cls = self.$classes[key]; + if (cls.$resize) { + !meta.classes && (meta.classes = {}); + meta.classes[cls.index] = cls.$resize; + cls.$resize = null; + } + } + self.resize(meta, function() { + self.states.resize = false; + self.ready = true; + setImmediate(self.cb_next, NEXT_CONTINUE); + }); + } + }, DELAY); + + break; + + case NEXT_INSERT: + insDocument(self); + break; + case NEXT_RELATION: + insRelation(self); + break; + case NEXT_UPDATE: + updDocument(self); + break; + case NEXT_REMOVE: + remDocument(self); + break; + case NEXT_FIND: + if (self.pending.find.length) { + tmp = self.pending.find.shift(); + $find(self, tmp.name, tmp.builder, tmp.reverse); + } + break; + } +}; + +GP.class = function(name, meta, data) { + + var self = this; + + if (!self.ready && !data) { + self.pending.meta.push({ name: name, data: meta, type: 1 }); + return self; + } + + var item = self.$classes[name]; + var save = false; + + if (item == null) { + + item = {}; + item.locked = false; + + if (data) { + item.ready = true; + item.name = name; + item.index = data.i; + item.pageindex = data.p; + item.documentindex = data.d; + item.findfreeslots = true; + } else { + self.header.classindex++; + item.name = name; + item.index = self.header.classindex; + item.ready = false; + item.pageindex = addPage(self, TYPE_CLASS, item.index, 0, function() { + item.ready = true; + }); + item.documentindex = getDocumentIndex(self, item.pageindex); + save = true; + } + + item.schema = parseSchema(meta); + self.$classes[item.name] = self.$classes[item.index] = item; + + } else { + var newschema = parseSchema(meta); + var raw = item.schema.raw; + if (raw !== newschema.raw) { + item.$resize = newschema; + self.next(NEXT_RESIZE); + } + } + + save && updMeta(self, META_CLASSESRELATIONS); + return self; +}; + +GP.relation = function(name, both, data) { + + var self = this; + + if (!self.ready && !data) { + self.pending.meta.push({ name: name, data: both, type: 2 }); + return self; + } + + var self = this; + var item = self.$relations[name]; + var save = false; + + if (item == null) { + + item = {}; + item.ready = true; + item.locked = false; + + if (data) { + item.name = name; + item.index = data.i; + item.pageindex = data.p; + item.documentindex = data.d; + item.both = both; + } else { + self.header.relationindex++; + item.name = name; + item.index = self.header.relationindex; + item.ready = false; + item.both = both; + item.pageindex = addPage(self, TYPE_RELATION, item.index, 0, function() { + item.ready = true; + }); + item.documentindex = getDocumentIndex(self, item.pageindex); + save = true; + } + + self.$relations[item.name] = self.$relations[item.index] = item; + + } else { + // compare + } + + save && updMeta(self, META_CLASSESRELATIONS); + return self; +}; + +GP.emit = function(name, a, b, c, d, e, f, g) { + var evt = this.$events[name]; + if (evt) { + var clean = false; + for (var i = 0, length = evt.length; i < length; i++) { + if (evt[i].$once) + clean = true; + evt[i].call(this, a, b, c, d, e, f, g); + } + if (clean) { + evt = evt.remove(n => n.$once); + if (evt.length) + this.$events[name] = evt; + else + this.$events[name] = undefined; + } + } + return this; +}; + +GP.on = function(name, fn) { + if (this.$ready && (name === 'ready' || name === 'load')) { + fn(); + return this; + } + if (!fn.$once) + this.$free = false; + if (this.$events[name]) + this.$events[name].push(fn); + else + this.$events[name] = [fn]; + return this; +}; + +GP.once = function(name, fn) { + fn.$once = true; + return this.on(name, fn); +}; + +GP.removeListener = function(name, fn) { + var evt = this.$events[name]; + if (evt) { + evt = evt.remove(n => n === fn); + if (evt.length) + this.$events[name] = evt; + else + this.$events[name] = undefined; + } + return this; +}; + +GP.removeAllListeners = function(name) { + if (name === true) + this.$events = EMPTYOBJECT; + else if (name) + this.$events[name] = undefined; + else + this.$events[name] = {}; + return this; +}; + +GP.resize = function(meta, callback) { + + // meta.documentsize + // meta.classes + + var self = this; + var filename = self.filename + '-tmp'; + + self.create(filename, meta.documentsize, function(err) { + + if (err) + throw err; + + Fs.open(filename, 'r+', function(err, fd) { + + var offset = HEADERSIZE; + var newoffset = HEADERSIZE; + var size = self.header.pagesize + (self.header.pagelimit * self.header.documentsize); + var newsize = self.header.pagesize + (self.header.pagelimit * meta.documentsize); + var pageindex = 0; + var totaldocuments = 0; + + var finish = function() { + + var buf = U.createBufferSize(HEADERSIZE); + Fs.read(fd, buf, 0, buf.length, 0, function() { + + // ==== DB:HEADER (7000b) + // name (30b) = from: 0 + // version (1b) = from: 30 + // pages (4b) = from: 31 + // pagesize (2b) = from: 35 + // pagelimit (2b) = from: 37 + // documents (4b) = from: 39 + // documentsize (2b) = from: 43 + // classindex (1b) = from: 45 + // relationindex (1b) = from: 46 + // relationnodeindex = from: 47 + // classes + relations = from: 51 + + // buf. + + buf.writeUInt32LE(pageindex > 0 ? (pageindex - 1) : 0, 31); + buf.writeUInt32LE(totaldocuments, 39); + buf.writeUInt16LE(meta.documentsize, 43); + + var obj = {}; + obj.c = []; // classes + obj.r = []; // relations + + for (var i = 0; i < self.header.classindex; i++) { + var item = self.$classes[i + 1]; + var schema = meta.classes[i + 1]; + obj.c.push({ n: item.name, i: item.index, p: item.pageindex, r: schema ? schema.raw : item.schema.raw, d: item.documentindex }); + } + + for (var i = 0; i < self.header.relationindex; i++) { + var item = self.$relations[i + 1]; + obj.r.push({ n: item.name, i: item.index, p: item.pageindex, b: item.both ? 1 :0, d: item.documentindex }); + } + + buf.writeUInt8(self.header.classindex, 45); + buf.writeUInt8(self.header.relationindex, 46); + buf.writeUInt32LE(self.header.relationpageindex, 47); + buf.write(JSON.stringify(obj), 51); + + Fs.write(fd, buf, 0, buf.length, 0, function() { + // console.log(pageindex, meta.documentsize, totaldocuments); + Fs.close(fd, function() { + Fs.close(self.fd, function() { + Fs.copyFile(self.filename, self.filename.replace(/\.gdb$/, NOW.format('_yyyyMMddHHmm') + '.gdp'), function() { + Fs.rename(self.filename + '-tmp', self.filename, function() { + callback(null); + }); + }); + }); + }); + }); + }); + }; + + var readvalue = function(docbuf, callback) { + var data = docbuf.slice(DATAOFFSET, docbuf.readUInt16LE(15) + DATAOFFSET); + if (docbuf[2] === STATE_COMPRESSED) + Zlib.inflate(data, ZLIBOPTIONS, (err, data) => callback(data ? data.toString('utf8') : '')); + else + callback(data.toString('utf8')); + }; + + var writevalue = function(value, callback) { + var maxsize = meta.documentsize - DATAOFFSET; + var data = U.createBuffer(value); + if (data.length > maxsize) { + Zlib.deflate(data, ZLIBOPTIONS, (err, data) => callback((!data || data.length > maxsize) ? EMPTYBUFFER : data)); + } else + callback(data); + }; + + var process = function() { + + pageindex++; + + // ==== DB:PAGE (20b) + // type (1b) = from: 0 + // index (1b) = from: 1 + // documents (2b) = from: 2 + // freeslots (1b) = from: 4 + // parentindex (4b) = from: 5 + + // ==== DB:DOCUMENT (SIZE) + // type (1b) = from: 0 + // index (1b) = from: 1 + // state (1b) = from: 2 + // pageindex (4b) = from: 3 + // relationindex (4b) = from: 7 (it's for relations between two documents in TYPE_RELATION page) + // parentindex (4b) = from: 11 + // size/count (2b) = from: 15 + // data = from: 17 + + var buf = U.createBufferSize(size); + + Fs.read(self.fd, buf, 0, buf.length, offset, function(err, size) { + + if (!size) { + finish(); + return; + } + + var newbuf = U.createBufferSize(newsize); + + // Copies page info + newbuf.fill(buf, 0, self.header.pagesize); + buf = buf.slice(self.header.pagesize); + + var index = self.header.pagesize; + var documents = 0; + + (self.header.pagelimit).async(function(i, next) { + + // Unexpected problem + if (!buf.length) { + next(); + return; + } + + var docbuf = buf.slice(0, self.header.documentsize); + var typeid = docbuf[0]; + var indexid = docbuf[1]; + + if (docbuf[2] !== STATE_REMOVED) { + totaldocuments++; + documents++; + } + + if (docbuf[2] !== STATE_REMOVED && meta.classes && typeid === TYPE_CLASS && meta.classes[indexid]) { + readvalue(docbuf, function(value) { + + // parseData + // stringifyData + value = stringifyData(meta.classes[indexid], parseData(self.$classes[indexid].schema, value.split('|'))); + + writevalue(value, function(value) { + + if (value === EMPTYBUFFER) { + // BIG PROBLEM + docbuf.writeUInt16LE(0, 15); + docbuf.writeUInt8(STATE_REMOVED, 2); + documents--; + } else { + docbuf.writeUInt16LE(value.length, 15); + docbuf.fill(value, DATAOFFSET, DATAOFFSET + value.length); + } + + newbuf.fill(docbuf, index, index + self.header.documentsize); + index += meta.documentsize; + buf = buf.slice(self.header.documentsize); + next(); + }); + + }); + } else { + newbuf.fill(docbuf, index, index + self.header.documentsize); + index += meta.documentsize; + buf = buf.slice(self.header.documentsize); + next(); + } + + }, function() { + + // Update count of documents + if (newbuf.readUInt16LE(2) !== documents) + newbuf.writeUInt16LE(documents, 2); + + Fs.write(fd, newbuf, 0, newbuf.length, newoffset, function() { + offset += size; + newoffset += newsize; + setImmediate(process); + }); + }); + + }); + }; + + process(); + }); + }); + return self; +}; + + +function $update(doc, value) { + return value; +} + +function $modify(doc, value) { + var keys = Object.keys(value); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + + switch (key[0]) { + case '+': + case '-': + case '*': + case '/': + var tmp = key.substring(1); + if (typeof(doc[tmp]) === 'number') { + if (key[0] === '+') + doc[tmp] += value[key]; + else if (key[0] === '-') + doc[tmp] -= value[key]; + else if (key[0] === '*') + doc[tmp] *= value[key]; + else if (key[0] === '/') + doc[tmp] = doc[tmp] / value[key]; + } + break; + default: + if (doc[key] != undefined) + doc[key] = value[key]; + break; + } + } + return doc; +} + +GP.remove = function(id, callback) { + var self = this; + var rem = { id: id, callback: callback || NOOP }; + self.pending.remove.push(rem); + self.next(NEXT_REMOVE); + return self; +}; + +GP.update = function(id, value, callback) { + var self = this; + var upd = { id: id, value: value, fn: typeof(value) === 'function' ? value : $update, callback: callback || NOOP }; + self.pending.update.push(upd); + self.next(NEXT_UPDATE); + return self; +}; + +GP.modify = function(id, value, callback) { + var self = this; + var upd = { id: id, value: value, fn: $modify, callback: callback || NOOP }; + self.pending.update.push(upd); + self.next(NEXT_UPDATE); + return self; +}; + +GP.insert = function(name, value, callback) { + var self = this; + self.pending.insert.push({ name: name, value: value, callback: callback || NOOP }); + self.next(NEXT_INSERT); + return self; +}; + +GP.cursor = function(type, name, callback) { + + var self = this; + var index; + var tmp; + + switch (type) { + case TYPE_CLASS: + tmp = self.$classes[name]; + index = tmp.pageindex; + break; + case TYPE_RELATION: + tmp = self.$relations[name]; + index = tmp.pageindex; + break; + } + + var offset = offsetPage(self, index); + var buf = U.createBufferSize(PAGESIZE); + + Fs.read(self.fd, buf, 0, buf.length, offset, function(err) { + + if (err) { + callback(err); + return; + } + + if (buf[0] !== TYPE_CLASS) { + callback(new Error('Invalid page type')); + return; + } + + if (buf[1] !== tmp.index) { + callback(new Error('Invalid type index')); + return; + } + + var data = {}; + data.count = buf.readUInt16LE(2); + data.parent = buf.readUInt32LE(5); + data.offset = offset; + data.type = buf[0]; + data.index = buf[1]; + data.freeslots = buf[4]; + + data.next = function(callback) { + + if (data.parent == 0) { + callback(new Error('This is the last page'), data); + return; + } + + offset = offsetPage(self, data.parent); + Fs.read(self.fd, buf, 0, buf.length, offset, function(err) { + data.count = buf.readUInt16LE(2); + data.parent = buf.readUInt32LE(5); + data.offset = offset; + data.type = buf[0]; + data.index = buf[1]; + data.freeslots = buf[4]; + data.INDEX = getIndexPage(self, offset) + 1; + callback(err, data); + }); + }; + + data.documents = function(callback) { + + if (!data.count) { + callback(null, EMPTYARRAY); + return; + } + + var index = getIndexPage(self, data.offset) * self.header.pagelimit; + var buffer = U.createBufferSize(self.header.pagelimit * self.header.documentsize); + var offset = data.offset + self.header.pagesize; + var decompress = []; + + index += self.header.pagelimit + 1; + + Fs.read(self.fd, buffer, 0, buffer.length, offset, function(err) { + + if (err) { + callback(err, EMPTYARRAY); + return; + } + + var arr = []; + while (true) { + + if (!buffer.length) + break; + + index--; + var data = buffer.slice(buffer.length - self.header.documentsize); + // index++; + // var data = buffer.slice(0, self.header.documentsize); + if (!data.length) + break; + + // type (1b) = from: 0 + // index (1b) = from: 1 + // state (1b) = from: 2 + // pageindex (4b) = from: 3 + // continuerindex (4b) = from: 7 + // parentindex (4b) = from: 11 + // size/count (2b) = from: 15 + // data = from: 17 + + if (data[2] !== STATE_REMOVED) { + var raw = data.slice(DATAOFFSET, data.readUInt16LE(15) + DATAOFFSET); + if (type === TYPE_CLASS) { + // Document is compressed + if (data[2] === STATE_COMPRESSED) { + var obj = {}; + obj.CLASS = tmp.name; + obj.ID = index; + obj.BUFFER = raw; + decompress.push({ CLASS: tmp, ID: index, BUFFER: raw, index: arr.push(null) }); + } else { + var obj = parseData(tmp.schema, raw.toString('utf8').split('|')); + obj.CLASS = tmp.name; + obj.ID = index; + arr.push(obj); + } + } + } + + buffer = buffer.slice(0, buffer.length - self.header.documentsize); + // buffer = buffer.slice(self.header.documentsize); + } + + if (decompress.length) { + decompress.wait(function(item, next) { + Zlib.inflate(item.BUFFER, ZLIBOPTIONS, function(err, data) { + var obj = parseData(item.CLASS.schema, data.toString('utf8').split('|')); + obj.CLASS = item.CLASS.name; + obj.ID = item.ID; + arr[item.index] = obj; + setImmediate(next); + }); + }, () => callback(null, arr)); + } else + callback(null, arr); + }); + }; + + callback(null, data); + }); +}; + +GP.read = function(index, callback) { + var self = this; + var buf = U.createBufferSize(self.header.documentsize); + Fs.read(self.fd, buf, 0, buf.length, offsetDocument(self, index), function(err) { + + if (err) { + callback(err); + return; + } + + if (buf[2] === STATE_REMOVED) { + callback(null, buf[0] === TYPE_CLASS ? null : EMPTYARRAY); + return; + } + + var tmp; + + switch(buf[0]) { + case TYPE_CLASS: + tmp = self.$classes[buf[1]]; + if (tmp) { + var data = buf.slice(DATAOFFSET, buf.readUInt16LE(15) + DATAOFFSET); + if (buf[2] === STATE_COMPRESSED) { + Zlib.inflate(data, ZLIBOPTIONS, function(err, data) { + data = parseData(tmp.schema, data.toString('utf8').split('|')); + data.ID = index; + data.CLASS = tmp.name; + callback(null, data, buf.readUInt32LE(7), buf.readUInt32LE(11)); + }); + } else { + data = parseData(tmp.schema, data.toString('utf8').split('|')); + data.ID = index; + data.CLASS = tmp.name; + callback(null, data, buf.readUInt32LE(7), buf.readUInt32LE(11)); + } + } else + callback(new Error('GraphDB: invalid document'), null); + break; + case TYPE_RELATION: + tmp = self.$relations[buf[1]]; + if (tmp) { + + var count = buf.readUInt16LE(15); + var arr = []; + for (var i = 0; i < count; i++) { + var off = DATAOFFSET + (i * 6); + arr.push({ RELATION: tmp.name, ID: buf.readUInt32LE(off + 2), INIT: buf[1], INDEX: i }); + } + + callback(null, arr, buf.readUInt32LE(7), buf.readUInt32LE(11), 'RELATION'); + + } else + callback(new Error('GraphDB: invalid document'), null); + break; + + case TYPE_RELATION_DOCUMENT: + + var count = buf.readUInt16LE(15); + var arr = []; + + for (var i = 0; i < count; i++) { + var off = DATAOFFSET + (i * 6); + tmp = self.$relations[buf[off]]; + arr.push({ RELATION: tmp.name, ID: buf.readUInt32LE(off + 2), INIT: buf[off + 1], INDEX: i }); + } + + callback(null, arr, buf.readUInt32LE(7), buf.readUInt32LE(11), 'PRIVATE'); + break; + + default: + callback(null, null); + break; + } + }); +}; + +GP.connect = function(name, indexA, indexB, callback) { + var self = this; + self.pending.relation.push({ name: name, indexA: indexA, indexB: indexB, callback: callback, connect: true }); + self.next(NEXT_RELATION); + return self; +}; + +GP.disconnect = function(name, indexA, indexB, callback) { + var self = this; + self.pending.relation.push({ name: name, indexA: indexA, indexB: indexB, callback: callback }); + self.next(NEXT_RELATION); + return self; +}; + +GP.find = function(cls) { + var self = this; + var builder = new DatabaseBuilder(self); + self.pending.find.push({ name: cls, builder: builder }); + setImmediate(self.cb_next, NEXT_FIND); + return builder; +}; + +GP.find2 = function(cls) { + var self = this; + var builder = new DatabaseBuilder(self); + self.pending.find.push({ name: cls, builder: builder, reverse: true }); + setImmediate(self.cb_next, NEXT_FIND); + return builder; +}; + +GP.scalar = function(cls, type, field) { + var self = this; + var builder = new DatabaseBuilder(self); + builder.scalar(type, field); + self.pending.find.push({ name: cls, builder: builder }); + setImmediate(self.cb_next, NEXT_FIND); + return builder; +}; + +GP.count = function(cls) { + return this.scalar(cls, 'count', 'ID'); +}; + +function GraphDBFilter(db) { + var t = this; + t.db = db; + t.levels = null; +} + +GraphDBFilter.prototype.level = function(num) { + var self = this; + if (self.levels == null) + self.levels = {}; + return self.levels[num] = new DatabaseBuilder(self.db); +}; + +GraphDBFilter.prototype.prepare = function() { + + var self = this; + + if (!self.levels) + return self; + + var arr = Object.keys(self.levels); + + for (var i = 0; i < arr.length; i++) { + var key = arr[i]; + var builder = self.levels[key]; + var filter = {}; + filter.builder = builder; + filter.scalarcount = 0; + filter.filter = builder.makefilter(); + filter.compare = builder.compile(); + filter.index = 0; + filter.count = 0; + filter.counter = 0; + filter.first = builder.$options.first && !builder.$options.sort; + self.levels[key] = filter; + } + + return self; +}; + +GP.graph = function(id, options, callback, filter) { + + if (typeof(options) === 'function') { + callback = options; + options = EMPTYOBJECT; + } else if (!options) + options = EMPTYOBJECT; + + var self = this; + + if (!filter) + filter = new GraphDBFilter(self); + + + self.read(id, function(err, doc, linkid) { + + if (err || !doc) { + callback(err, null, 0); + return; + } + + // options.depth (Int) + // options.relation (String or String Array) + // options.class (String or String Array) + + var relations = null; + var classes = null; + + if (options.relation) { + + var rel; + relations = {}; + + if (options.relation instanceof Array) { + for (var i = 0; i < options.relation.length; i++) { + rel = self.$relations[options.relation[i]]; + if (rel) + relations[rel.name] = rel.both ? 1 : 0; + } + } else { + rel = self.$relations[options.relation]; + if (rel) + relations[rel.name] = rel.both ? 1 : 0; + } + } + + if (options.class) { + + var clstmp; + classes = {}; + + if (options.class instanceof Array) { + for (var i = 0; i < options.class.length; i++) { + clstmp = self.$classes[options.class[i]]; + if (clstmp) + classes[clstmp.name] = 1; + } + } else { + clstmp = self.$classes[options.class]; + if (clstmp) + classes[clstmp.name] = clstmp.index + 1; + } + } + + filter.prepare(); + + var pending = []; + var tmp = {}; + var count = 1; + var sort = false; + + tmp[id] = 1; + + doc.INDEX = 0; + doc.LEVEL = 0; + doc.NODES = []; + + var reader = function(parent, id, depth) { + + if ((options.depth && depth >= options.depth) || (tmp[id])) { + process(); + return; + } + + tmp[id] = 1; + + self.read(id, function(err, links, linkid) { + + if (linkid && !tmp[linkid]) { + pending.push({ id: linkid, parent: parent, depth: depth }); + sort = true; + } + + // because of seeking on HDD + links.quicksort('ID'); + + var fil; + + links.wait(function(item, next) { + + var key = item.ID + '-' + item.RELATION; + + if (tmp[key] || (relations && relations[item.RELATION] == null) || (!options.all && !item.INIT && !relations) || (relations && relations[item.RELATION] === item.TYPE)) + return next(); + + tmp[key] = 1; + + self.read(item.ID, function(err, doc, linkid) { + + if (doc && (!classes || classes[doc.CLASS])) { + + count++; + + doc.INDEX = item.INDEX; + doc.LEVEL = depth + 1; + doc.NODES = []; + + var rel = self.$relations[item.RELATION]; + + if (rel) { + // doc.RELATION = rel.relation; + doc.RELATION = rel.name; + } + + fil = filter.levels ? filter.levels[depth + 1] : null; + + if (fil) { + !fil.response && (fil.response = parent.NODES); + if (!framework_nosql.compare(fil, doc)) + linkid = null; + } else + parent.NODES.push(doc); + + if (linkid && !tmp[linkid]) { + pending.push({ id: linkid, parent: doc, depth: depth + 1 }); + sort = true; + } + } + + next(); + }); + + }, process); + }); + }; + + var process = function() { + + if (pending.length) { + + // because of seeking on HDD + if (sort && pending.length > 1) { + pending.quicksort('id'); + sort = false; + } + + var item = pending.shift(); + reader(item.parent, item.id, item.depth); + + } else { + + if (filter.levels) { + var keys = Object.keys(filter.levels); + for (var i = 0; i < keys.length; i++) { + var f = filter.levels[keys[i]]; + framework_nosql.callback(f); + } + } + + callback(null, doc, count); + } + }; + + linkid && pending.push({ id: linkid, parent: doc, depth: 0 }); + process(); + + }, options.type); + + return filter; +}; + +function $find(self, cls, builder, reverse) { + + var filter = {}; + + filter.builder = builder; + filter.scalarcount = 0; + filter.filter = builder.makefilter(); + filter.compare = builder.compile(); + filter.index = 0; + filter.count = 0; + filter.counter = 0; + filter.first = builder.$options.first && !builder.$options.sort; + + var tmp = self.$classes[cls]; + if (!tmp) { + framework_nosql.callback(filter, 'GraphDB: Class "{0}" is not registered.'.format(cls)); + setImmediate(self.cb_next, NEXT_FIND); + return; + } + + var read = function(err, data) { + + if (err || (!data.count && !data.parent)) { + framework_nosql.callback(filter); + return; + } + + data.documents(function(err, docs) { + for (var i = 0; i < docs.length; i++) { + var doc = docs[i]; + filter.index++; + if ((doc && framework_nosql.compare(filter, doc) === false) || (reverse && filter.done)) { + framework_nosql.callback(filter); + data.next = null; + data.documents = null; + data = null; + setImmediate(self.cb_next, NEXT_FIND); + return; + } + } + data.next(read); + }); + }; + + self.cursor(1, tmp.name, read); +} + +function parseSchema(schema) { + + var obj = {}; + var arr = schema.split('|').trim(); + + obj.meta = {}; + obj.keys = []; + obj.raw = schema; + + for (var i = 0; i < arr.length; i++) { + var arg = arr[i].split(':'); + var type = 0; + switch ((arg[1] || '').toLowerCase().trim()) { + case 'number': + type = 2; + break; + case 'boolean': + case 'bool': + type = 3; + break; + case 'date': + type = 4; + break; + case 'object': + type = 5; + break; + case 'string': + default: + type = 1; + break; + } + var name = arg[0].trim(); + obj.meta[name] = { type: type, pos: i }; + obj.keys.push(name); + } + + return obj; +} + +function stringifyData(schema, doc) { + + var output = ''; + var esc = false; + var size = 0; + + for (var i = 0; i < schema.keys.length; i++) { + var key = schema.keys[i]; + var meta = schema.meta[key]; + var val = doc[key]; + + switch (meta.type) { + case 1: // String + val = val ? val : ''; + size += 4; + break; + case 2: // Number + val = (val || 0); + size += 2; + break; + case 3: // Boolean + val = (val == true ? '1' : '0'); + break; + case 4: // Date + // val = val ? val.toISOString() : ''; + val = val ? val.getTime() : ''; + !val && (size += 13); + break; + case 5: // Object + val = val ? JSON.stringify(val) : ''; + size += 4; + break; + } + + if (!esc && (meta.type === 1 || meta.type === 5)) { + val += ''; + if (REGTESCAPETEST.test(val)) { + esc = true; + val = val.replace(REGTESCAPE, regtescape); + } + } + + output += '|' + val; + } + + return (esc ? '*' : '+') + output; +} + +function parseData(schema, lines, cache) { + + var obj = {}; + var esc = lines === '*'; + var val; + + for (var i = 0; i < schema.keys.length; i++) { + var key = schema.keys[i]; + + if (cache && cache !== EMPTYOBJECT && cache[key] != null) { + obj[key] = cache[key]; + continue; + } + + var meta = schema.meta[key]; + if (meta == null) + continue; + + var pos = meta.pos + 1; + + switch (meta.type) { + case 1: // String + obj[key] = lines[pos]; + if (esc && obj[key]) + obj[key] = obj[key].replace(REGTUNESCAPE, regtescapereverse); + break; + case 2: // Number + val = +lines[pos]; + obj[key] = val < 0 || val > 0 ? val : 0; + break; + case 3: // Boolean + val = lines[pos]; + obj[key] = BOOLEAN[val] == 1; + break; + case 4: // Date + val = lines[pos]; + obj[key] = val ? new Date(val[10] === 'T' ? val : +val) : null; + break; + case 5: // Object + val = lines[pos]; + if (esc && val) + val = val.replace(REGTUNESCAPE, regtescapereverse); + obj[key] = val ? val.parseJSON(true) : null; + break; + } + } + + return obj; +} + +function regtescapereverse(c) { + switch (c) { + case '%0A': + return '\n'; + case '%0D': + return '\r'; + case '%7C': + return '|'; + } + return c; +} + +function regtescape(c) { + switch (c) { + case '\n': + return '%0A'; + case '\r': + return '%0D'; + case '|': + return '%7C'; + } + return c; +} + +exports.load = function(name, size) { + return new GraphDB(name, size); +}; + +exports.getImportantOperations = function() { + return IMPORTATOPERATIONS; +}; \ No newline at end of file diff --git a/helpers/debug.js b/helpers/debug.js index 98181945e..1161565cc 100644 --- a/helpers/debug.js +++ b/helpers/debug.js @@ -12,5 +12,6 @@ const options = {}; // options.sleep = 3000; // options.inspector = 9229; // options.watch = ['private']; +// options.livereload = true; require('total.js/debug')(options); \ No newline at end of file diff --git a/helpers/index.js b/helpers/index.js new file mode 100644 index 000000000..0fdae1466 --- /dev/null +++ b/helpers/index.js @@ -0,0 +1,21 @@ +// =================================================== +// Total.js start script +// https://www.totaljs.com +// =================================================== + +const total = 'total.js'; +const options = {}; + +// options.ip = '127.0.0.1'; +// options.port = parseInt(process.argv[2]); +// options.unixsocket = require('path').join(require('os').tmpdir(), 'app_name'); +// options.config = { name: 'Total.js' }; +// options.sleep = 3000; +// options.inspector = 9229; +// options.watch = ['private']; +// options.livereload = true; + +if (process.argv.indexOf('--release', 1) !== -1 || process.argv.indexOf('release', 1) !== -1) + require(total).http('release', options); +else + require(total + '/debug')(options); \ No newline at end of file diff --git a/image.js b/image.js index 45c40fe73..c77af5e65 100755 --- a/image.js +++ b/image.js @@ -1,4 +1,4 @@ -// Copyright 2012-2018 (c) Peter Širka +// Copyright 2012-2020 (c) Peter Širka // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the @@ -21,7 +21,7 @@ /** * @module FrameworkImage - * @version 2.9.2 + * @version 3.3.0 */ 'use strict'; @@ -34,7 +34,11 @@ const Fs = require('fs'); const REGEXP_SVG = /(width="\d+")+|(height="\d+")+/g; const REGEXP_PATH = /\//g; const REGEXP_ESCAPE = /'/g; +const SPAWN_OPT = { shell: true }; const D = require('os').platform().substring(0, 3).toLowerCase() === 'win' ? '"' : '\''; +const CMD_CONVERT = { gm: 'gm', im: 'convert', magick: 'magick' }; +const CMD_CONVERT2 = { gm: 'gm convert', im: 'convert', magick: 'magick' }; +const SUPPORTEDIMAGES = { jpg: 1, png: 1, gif: 1, apng: 1, jpeg: 1, heif: 1, heic: 1, webp: 1, ico: 1 }; var CACHE = {}; var middlewares = {}; @@ -51,7 +55,7 @@ function u32(buf, o) { } exports.measureGIF = function(buffer) { - return { width: buffer[6], height: buffer[8] }; + return { width: buffer.readInt16LE(6), height: buffer.readInt16LE(8) }; }; // MIT @@ -63,25 +67,17 @@ exports.measureJPG = function(buffer) { var o = 0; var jpeg = 0xff == buffer[0] && 0xd8 == buffer[1]; - if (!jpeg) - return; - - o += 2; - - while (o < len) { + if (jpeg) { + o += 2; + while (o < len) { + while (0xff != buffer[o]) o++; + while (0xff == buffer[o]) o++; + if (sof[buffer[o]]) + return { width: u16(buffer, o + 6), height: u16(buffer, o + 4) }; + else + o += u16(buffer, ++o); - while (0xff != buffer[o]) o++; - while (0xff == buffer[o]) o++; - - if (!sof[buffer[o]]) { - o += u16(buffer, ++o); - continue; } - - var w = u16(buffer, o + 6); - var h = u16(buffer, o + 4); - - return { width: w, height: h }; } return null; @@ -142,25 +138,27 @@ exports.measure = function(type, buffer) { } }; -function Image(filename, useImageMagick, width, height) { +function Image(filename, cmd, width, height) { var type = typeof(filename); this.width = width; this.height = height; this.builder = []; this.filename = type === 'string' ? filename : null; this.currentStream = type === 'object' ? filename : null; - this.isIM = useImageMagick == null ? F.config['default-image-converter'] === 'im' : useImageMagick; this.outputType = type === 'string' ? framework_utils.getExtension(filename) : 'jpg'; this.islimit = false; + this.cmdarg = cmd || CONF.default_image_converter; } -Image.prototype.clear = function() { +var ImageProto = Image.prototype; + +ImageProto.clear = function() { var self = this; self.builder = []; return self; }; -Image.prototype.measure = function(callback) { +ImageProto.measure = function(callback) { var self = this; var index = self.filename.lastIndexOf('.'); @@ -175,6 +173,7 @@ Image.prototype.measure = function(callback) { return; } + F.stats.performance.open++; var extension = self.filename.substring(index).toLowerCase(); var stream = require('fs').createReadStream(self.filename, { start: 0, end: extension === '.jpg' ? 40000 : 24 }); @@ -199,7 +198,7 @@ Image.prototype.measure = function(callback) { return self; }; -Image.prototype.$$measure = function() { +ImageProto.$$measure = function() { var self = this; return function(callback) { self.measure(callback); @@ -213,7 +212,7 @@ Image.prototype.$$measure = function() { * @param {Function(stream)} writer A custom stream writer, optional. * @return {Image} */ -Image.prototype.save = function(filename, callback, writer) { +ImageProto.save = function(filename, callback, writer) { var self = this; @@ -250,6 +249,7 @@ Image.prototype.save = function(filename, callback, writer) { if (!middleware) return callback(null, true); + F.stats.performance.open++; var reader = Fs.createReadStream(filename); var writer = Fs.createWriteStream(filename + '_'); @@ -269,14 +269,14 @@ Image.prototype.save = function(filename, callback, writer) { return self; }; -Image.prototype.$$save = function(filename, writer) { +ImageProto.$$save = function(filename, writer) { var self = this; return function(callback) { self.save(filename, callback, writer); }; }; -Image.prototype.pipe = function(stream, type, options) { +ImageProto.pipe = function(stream, type, options) { var self = this; @@ -286,10 +286,12 @@ Image.prototype.pipe = function(stream, type, options) { } !self.builder.length && self.minify(); - !type && (type = self.outputType); - var cmd = spawn(self.isIM ? 'convert' : 'gm', self.arg(self.filename ? wrap(self.filename) : '-', (type ? type + ':' : '') + '-')); + if (!type || !SUPPORTEDIMAGES[type]) + type = self.outputType; + F.stats.performance.open++; + var cmd = spawn(CMD_CONVERT[self.cmdarg], self.arg(self.filename ? wrap(self.filename) : '-', (type ? type + ':' : '') + '-'), SPAWN_OPT); cmd.stderr.on('data', stream.emit.bind(stream, 'error')); cmd.stdout.on('data', stream.emit.bind(stream, 'data')); cmd.stdout.on('end', stream.emit.bind(stream, 'end')); @@ -317,17 +319,17 @@ Image.prototype.pipe = function(stream, type, options) { * @param {Function(stream)} writer A custom stream writer. * @return {ReadStream} */ -Image.prototype.stream = function(type, writer) { +ImageProto.stream = function(type, writer) { var self = this; !self.builder.length && self.minify(); - if (!type) + if (!type || !SUPPORTEDIMAGES[type]) type = self.outputType; - var cmd = spawn(self.isIM ? 'convert' : 'gm', self.arg(self.filename ? wrap(self.filename) : '-', (type ? type + ':' : '') + '-')); - + F.stats.performance.open++; + var cmd = spawn(CMD_CONVERT[self.cmdarg], self.arg(self.filename ? wrap(self.filename) : '-', (type ? type + ':' : '') + '-'), SPAWN_OPT); if (self.currentStream) { if (self.currentStream instanceof Buffer) cmd.stdin.end(self.currentStream); @@ -340,15 +342,17 @@ Image.prototype.stream = function(type, writer) { return middleware ? cmd.stdout.pipe(middleware()) : cmd.stdout; }; -Image.prototype.cmd = function(filenameFrom, filenameTo) { +ImageProto.cmd = function(filenameFrom, filenameTo) { var self = this; var cmd = ''; if (!self.islimit) { - var tmp = F.config['default-image-consumption']; - self.limit('memory', (1500 / 100) * tmp); - self.limit('map', (3000 / 100) * tmp); + var tmp = CONF.default_image_consumption; + if (tmp) { + self.limit('memory', (1500 / 100) * tmp); + self.limit('map', (3000 / 100) * tmp); + } } self.builder.sort(sort); @@ -357,25 +361,29 @@ Image.prototype.cmd = function(filenameFrom, filenameTo) { for (var i = 0; i < length; i++) cmd += (cmd ? ' ' : '') + self.builder[i].cmd; - return (self.isIM ? 'convert' : 'gm -convert') + wrap(filenameFrom, true) + ' ' + cmd + wrap(filenameTo, true); + return CMD_CONVERT2[self.cmdarg] + wrap(filenameFrom, true) + ' ' + cmd + wrap(filenameTo, true); }; function sort(a, b) { return a.priority > b.priority ? 1 : -1; } -Image.prototype.arg = function(first, last) { +ImageProto.arg = function(first, last) { var self = this; var arr = []; - !self.isIM && arr.push('-convert'); + if (self.cmdarg === 'gm') + arr.push('convert'); + first && arr.push(first); if (!self.islimit) { - var tmp = F.config['default-image-consumption']; - self.limit('memory', (1500 / 100) * tmp); - self.limit('map', (3000 / 100) * tmp); + var tmp = CONF.default_image_consumption; + if (tmp) { + self.limit('memory', (1500 / 100) * tmp); + self.limit('map', (3000 / 100) * tmp); + } } self.builder.sort(sort); @@ -397,10 +405,10 @@ Image.prototype.arg = function(first, last) { return arr; }; -Image.prototype.identify = function(callback) { +ImageProto.identify = function(callback) { var self = this; - - exec((self.isIM ? 'identify' : 'gm identify') + wrap(self.filename, true), function(err, stdout) { + F.stats.performance.open++; + exec((self.cmdarg === 'gm' ? 'gm ' : '') + 'identify' + wrap(self.filename, true), function(err, stdout) { if (err) { callback(err, null); @@ -416,14 +424,14 @@ Image.prototype.identify = function(callback) { return self; }; -Image.prototype.$$identify = function() { +ImageProto.$$identify = function() { var self = this; return function(callback) { self.identify(callback); }; }; -Image.prototype.push = function(key, value, priority, encode) { +ImageProto.push = function(key, value, priority, encode) { var self = this; var cmd = key; @@ -446,7 +454,7 @@ Image.prototype.push = function(key, value, priority, encode) { return self; }; -Image.prototype.output = function(type) { +ImageProto.output = function(type) { var self = this; if (type[0] === '.') type = type.substring(1); @@ -454,7 +462,7 @@ Image.prototype.output = function(type) { return self; }; -Image.prototype.resize = function(w, h, options) { +ImageProto.resize = function(w, h, options) { options = options || ''; var self = this; @@ -470,7 +478,7 @@ Image.prototype.resize = function(w, h, options) { return self.push('-resize', size + options, 1, true); }; -Image.prototype.thumbnail = function(w, h, options) { +ImageProto.thumbnail = function(w, h, options) { options = options || ''; var self = this; @@ -486,7 +494,7 @@ Image.prototype.thumbnail = function(w, h, options) { return self.push('-thumbnail', size + options, 1, true); }; -Image.prototype.geometry = function(w, h, options) { +ImageProto.geometry = function(w, h, options) { options = options || ''; var self = this; @@ -503,20 +511,20 @@ Image.prototype.geometry = function(w, h, options) { }; -Image.prototype.filter = function(type) { +ImageProto.filter = function(type) { return this.push('-filter', type, 1, true); }; -Image.prototype.trim = function() { +ImageProto.trim = function() { return this.push('-trim +repage', 1); }; -Image.prototype.limit = function(type, value) { +ImageProto.limit = function(type, value) { this.islimit = true; return this.push('-limit', type + ' ' + value, 1); }; -Image.prototype.extent = function(w, h) { +ImageProto.extent = function(w, h, x, y) { var self = this; var size = ''; @@ -528,6 +536,12 @@ Image.prototype.extent = function(w, h) { else if (!w && h) size = 'x' + h; + if (x || y) { + !x && (x = 0); + !y && (y = 0); + size += (x >= 0 ? '+' : '') + x + (y >= 0 ? '+' : '') + y; + } + return self.push('-extent', size, 4, true); }; @@ -539,7 +553,7 @@ Image.prototype.extent = function(w, h) { * @param {String} filter Optional, resize filter (default: Box) * @return {Image} */ -Image.prototype.miniature = function(w, h, color, filter) { +ImageProto.miniature = function(w, h, color, filter) { return this.filter(filter || 'Hamming').thumbnail(w, h).background(color ? color : 'white').align('center').extent(w, h); }; @@ -550,7 +564,7 @@ Image.prototype.miniature = function(w, h, color, filter) { * @param {String} color Optional, background color. * @return {Image} */ -Image.prototype.resizeCenter = function(w, h, color) { +ImageProto.resizeCenter = ImageProto.resize_center = function(w, h, color) { return this.resize(w, h, '^').background(color ? color : 'white').align('center').crop(w, h); }; @@ -562,11 +576,11 @@ Image.prototype.resizeCenter = function(w, h, color) { * @param {String} color Optional, background color. * @return {Image} */ -Image.prototype.resizeAlign = function(w, h, align, color) { +ImageProto.resizeAlign = ImageProto.resize_align = function(w, h, align, color) { return this.resize(w, h, '^').background(color ? color : 'white').align(align || 'center').crop(w, h); }; -Image.prototype.scale = function(w, h, options) { +ImageProto.scale = function(w, h, options) { options = options || ''; var self = this; @@ -582,15 +596,15 @@ Image.prototype.scale = function(w, h, options) { return self.push('-scale', size + options, 1, true); }; -Image.prototype.crop = function(w, h, x, y) { +ImageProto.crop = function(w, h, x, y) { return this.push('-crop', w + 'x' + h + '+' + (x || 0) + '+' + (y || 0), 4, true); }; -Image.prototype.quality = function(percentage) { +ImageProto.quality = function(percentage) { return this.push('-quality', percentage || 80, 5, true); }; -Image.prototype.align = function(type) { +ImageProto.align = function(type) { var output; @@ -645,68 +659,72 @@ Image.prototype.align = function(type) { return this; }; -Image.prototype.gravity = function(type) { +ImageProto.gravity = function(type) { return this.align(type); }; -Image.prototype.blur = function(radius) { +ImageProto.blur = function(radius) { return this.push('-blur', radius, 10, true); }; -Image.prototype.normalize = function() { +ImageProto.normalize = function() { return this.push('-normalize', null, 10); }; -Image.prototype.rotate = function(deg) { +ImageProto.rotate = function(deg) { return this.push('-rotate', deg || 0, 8, true); }; -Image.prototype.flip = function() { +ImageProto.flip = function() { return this.push('-flip', null, 10); }; -Image.prototype.flop = function() { +ImageProto.flop = function() { return this.push('-flop', null, 10); }; -Image.prototype.minify = function() { +ImageProto.define = function(value) { + return this.push('-define', value, 10, true); +}; + +ImageProto.minify = function() { return this.push('+profile', '*', null, 10, true); }; -Image.prototype.grayscale = function() { +ImageProto.grayscale = function() { return this.push('-colorspace', 'Gray', 10, true); }; -Image.prototype.bitdepth = function(value) { +ImageProto.bitdepth = function(value) { return this.push('-depth', value, 10, true); }; -Image.prototype.colors = function(value) { +ImageProto.colors = function(value) { return this.push('-colors', value, 10, true); }; -Image.prototype.background = function(color) { +ImageProto.background = function(color) { return this.push('-background', color, 2, true).push('-extent 0x0', null, 2); }; -Image.prototype.fill = function(color) { +ImageProto.fill = function(color) { return this.push('-fill', color, 2, true); }; -Image.prototype.sepia = function() { +ImageProto.sepia = function() { return this.push('-modulate', '115,0,100', 4).push('-colorize', '7,21,50', 5); }; -Image.prototype.watermark = function(filename, x, y, w, h) { +ImageProto.watermark = function(filename, x, y, w, h) { return this.push('-draw', 'image over {1},{2} {3},{4} {5}{0}{5}'.format(filename, x || 0, y || 0, w || 0, h || 0, D), 6, true); }; -Image.prototype.make = function(fn) { +ImageProto.make = function(fn) { fn.call(this, this); return this; }; -Image.prototype.command = function(key, value, priority, esc) { +ImageProto.command = function(key, value, priority, esc) { if (priority === true) { priority = 0; @@ -723,12 +741,12 @@ function wrap(command, empty) { exports.Image = Image; exports.Picture = Image; -exports.init = function(filename, imageMagick, width, height) { - return new Image(filename, imageMagick, width, height); +exports.init = function(filename, cmd, width, height) { + return new Image(filename, cmd, width, height); }; -exports.load = function(filename, imageMagick, width, height) { - return new Image(filename, imageMagick, width, height); +exports.load = function(filename, cmd, width, height) { + return new Image(filename, cmd, width, height); }; exports.middleware = function(type, fn) { @@ -737,10 +755,6 @@ exports.middleware = function(type, fn) { middlewares[type] = fn; }; -exports.restart = function() { - middlewares = {}; -}; - // Clears cache with commands exports.clear = function() { CACHE = {}; diff --git a/index.js b/index.js index acf312fe2..46586e267 100755 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -// Copyright 2012-2018 (c) Peter Širka +// Copyright 2012-2021 (c) Peter Širka // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the @@ -21,7 +21,7 @@ /** * @module Framework - * @version 2.9.4 + * @version 3.4.13 */ 'use strict'; @@ -45,12 +45,15 @@ const CT_TEXT = 'text/plain'; const CT_HTML = 'text/html'; const CT_JSON = 'application/json'; const COMPRESSION = { 'text/plain': true, 'text/javascript': true, 'text/css': true, 'text/jsx': true, 'application/javascript': true, 'application/x-javascript': true, 'application/json': true, 'text/xml': true, 'image/svg+xml': true, 'text/x-markdown': true, 'text/html': true }; +const COMPRESSIONSPECIAL = { js: 1, css: 1, mjs: 1 }; +const RESPONSENOCACHE = { zip: 1, rar: 1 }; const REG_TEMPORARY = /\//g; const REG_MOBILE = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|Tablet/i; const REG_ROBOT = /search|agent|bot|crawler|spider/i; -const REG_VERSIONS = /(href|src)="[a-zA-Z0-9/:\-.]+\.(jpg|js|css|png|gif|svg|html|ico|json|less|sass|scss|swf|txt|webp|woff|woff2|xls|xlsx|xml|xsl|xslt|zip|rar|csv|doc|docx|eps|gzip|jpe|jpeg|manifest|mov|mp3|flac|mp4|ogg|package|pdf)"/gi; +const REG_VERSIONS = /(href|src)="[a-zA-Z0-9/:\-._]+\.(jpg|js|css|png|apng|gif|svg|html|ico|json|less|sass|scss|swf|txt|webp|heif|heic|jpeg|woff|woff2|xls|xlsx|xml|xsl|xslt|zip|rar|csv|doc|docx|eps|gzip|jpe|jpeg|manifest|mov|mp3|flac|mp4|ogg|package|pdf)"/gi; const REG_COMPILECSS = /url\(.*?\)/g; const REG_ROUTESTATIC = /^(\/\/|https:|http:)+/; +const REG_NEWIMPL = /^(async\s)?function(\s)?([a-zA-Z$][a-zA-Z0-9$]+)?(\s)?\([a-zA-Z0-9$]+\)|^function anonymous\(\$/; const REG_RANGE = /bytes=/; const REG_EMPTY = /\s/g; const REG_ACCEPTCLEANER = /\s|\./g; @@ -59,15 +62,17 @@ const REG_WEBSOCKET_ERROR = /ECONNRESET|EHOSTUNREACH|EPIPE|is closed/i; const REG_WINDOWSPATH = /\\/g; const REG_SCRIPTCONTENT = /<|>|;/; const REG_HTTPHTTPS = /^(\/)?(http|https):\/\//i; -const REG_NOCOMPRESS = /[.|-]+min\.(css|js)$/i; +const REG_NOCOMPRESS = /[.|-]+min(@[a-z0-9]*)?\.(css|js)$/i; +const REG_WWW = /^www\./i; const REG_TEXTAPPLICATION = /text|application/; const REG_ENCODINGCLEANER = /[;\s]charset=utf-8/g; const REG_SKIPERROR = /epipe|invalid\sdistance/i; +const REG_OLDCONF = /-/g; const REG_UTF8 = /[^\x20-\x7E]+/; -const FLAGS_PROXY = ['post', 'json']; +const REG_ENCODEDSPACE = /\+/g; const FLAGS_INSTALL = ['get']; const FLAGS_DOWNLOAD = ['get', 'dnscache']; -const QUERYPARSEROPTIONS = { maxKeys: 69 }; +const QUERYPARSEROPTIONS = { maxKeys: 33 }; const EMPTYARRAY = []; const EMPTYOBJECT = {}; const EMPTYREQUEST = { uri: {} }; @@ -80,15 +85,27 @@ const REPOSITORY_META_AUTHOR = '$author'; const REPOSITORY_META_IMAGE = '$image'; const REPOSITORY_PLACE = '$place'; const REPOSITORY_SITEMAP = '$sitemap'; +const REPOSITORY_COMPONENTS = '$components'; const ATTR_END = '"'; const ETAG = '858'; const CONCAT = [null, null]; -const CLUSTER_CACHE_SET = { TYPE: 'cache-set' }; -const CLUSTER_CACHE_REMOVE = { TYPE: 'cache-remove' }; -const CLUSTER_CACHE_REMOVEALL = { TYPE: 'cache-remove-all' }; -const CLUSTER_CACHE_CLEAR = { TYPE: 'cache-clear' }; +const CLUSTER_CACHE_SET = { TYPE: 'cache', method: 'set' }; +const CLUSTER_CACHE_REMOVE = { TYPE: 'cache', method: 'remove' }; +const CLUSTER_CACHE_REMOVEALL = { TYPE: 'cache', method: 'removeAll' }; +const CLUSTER_CACHE_CLEAR = { TYPE: 'cache', method: 'clear' }; +const CLUSTER_SNAPSHOT = { TYPE: 'snapshot' }; const GZIPFILE = { memLevel: 9 }; const GZIPSTREAM = { memLevel: 1 }; +const MODELERROR = {}; +const IMAGES = { jpg: 1, png: 1, gif: 1, apng: 1, jpeg: 1, heif: 1, heic: 1, webp: 1 }; +const KEYSLOCALIZE = { html: 1, htm: 1 }; +const PROXYOPTIONS = { end: true }; +const PROXYKEEPALIVE = new http.Agent({ keepAlive: true, timeout: 60000 }); +const JSFILES = { js: 1, mjs: 1 }; +var PREFFILE = 'preferences.json'; + +var PATHMODULES = require.resolve('./index'); +PATHMODULES = PATHMODULES.substring(0, PATHMODULES.length - 8); Object.freeze(EMPTYOBJECT); Object.freeze(EMPTYARRAY); @@ -96,6 +113,32 @@ Object.freeze(EMPTYREQUEST); global.EMPTYOBJECT = EMPTYOBJECT; global.EMPTYARRAY = EMPTYARRAY; +global.NOW = new Date(); +global.THREAD = ''; +global.isWORKER = false; +global.REQUIRE = function(path) { + return require(F.directory + '/' + path); +}; + +function flowwrapper(name) { + if (!name) + name = 'default'; + if (F.flows[name]) + return F.flows[name]; + var flow = new framework_flow.make(name); + return F.flows[name] = flow; +} + +global.FLOWSTREAM = function(name) { + global.framework_flow = require('./flow'); + global.FLOW = flowwrapper; + return flowwrapper(name); +}; + +var DEF = global.DEF = {}; + +DEF.currencies = {}; + var PROTORES, PROTOREQ; var RANGE = { start: 0, end: 0 }; @@ -105,7 +148,6 @@ var SUCCESSHELPER = { success: true }; // Cached headers for repeated usage HEADERS.responseCode = {}; HEADERS.responseCode[HEADER_TYPE] = CT_TEXT; -HEADERS.responseCode['X-Powered-By'] = 'Total.js'; HEADERS.redirect = {}; HEADERS.redirect[HEADER_TYPE] = CT_HTML + '; charset=utf-8'; HEADERS.redirect[HEADER_LENGTH] = '0'; @@ -115,13 +157,6 @@ HEADERS.sse['Pragma'] = 'no-cache'; HEADERS.sse['Expires'] = '-1'; HEADERS.sse[HEADER_TYPE] = 'text/event-stream'; HEADERS.sse['X-Powered-By'] = 'Total.js'; -HEADERS.mmr = {}; -HEADERS.mmr[HEADER_CACHE] = 'private, no-cache, no-store, max-age=0'; -HEADERS.mmr['Pragma'] = 'no-cache'; -HEADERS.mmr['Expires'] = '-1'; -HEADERS.mmr['X-Powered-By'] = 'Total.js'; -HEADERS.proxy = {}; -HEADERS.proxy['X-Proxy'] = 'total.js'; HEADERS.file_lastmodified = {}; HEADERS.file_lastmodified['Access-Control-Allow-Origin'] = '*'; HEADERS.file_lastmodified[HEADER_CACHE] = 'public, max-age=11111111'; @@ -248,9 +283,8 @@ HEADERS.binary['X-Powered-By'] = 'Total.js'; HEADERS.authorization = { user: '', password: '', empty: true }; HEADERS.fsStreamRead = { flags: 'r', mode: '0666', autoClose: true }; HEADERS.fsStreamReadRange = { flags: 'r', mode: '0666', autoClose: true, start: 0, end: 0 }; -HEADERS.workers = { cwd: '' }; -HEADERS.mmrpipe = { end: false }; HEADERS.responseLocalize = {}; +HEADERS.responseLocalize['Access-Control-Allow-Origin'] = '*'; HEADERS.responseNotModified = {}; HEADERS.responseNotModified[HEADER_CACHE] = 'public, max-age=11111111'; HEADERS.responseNotModified['X-Powered-By'] = 'Total.js'; @@ -258,17 +292,17 @@ HEADERS.response503 = {}; HEADERS.response503[HEADER_CACHE] = 'private, no-cache, no-store, max-age=0'; HEADERS.response503[HEADER_TYPE] = CT_HTML; HEADERS.response503['X-Powered-By'] = 'Total.js'; -HEADERS.notModifiedEtag = {}; -HEADERS.notModifiedEtag['X-Powered-By'] = 'Total.js'; -HEADERS.notModifiedLastModifiedDate = {}; -HEADERS.notModifiedLastModifiedDate['X-Powered-By'] = 'Total.js'; +HEADERS.response503ddos = {}; +HEADERS.response503ddos[HEADER_CACHE] = 'private, no-cache, no-store, max-age=0'; +HEADERS.response503ddos[HEADER_TYPE] = CT_TEXT; +HEADERS.response503ddos['X-Powered-By'] = 'Total.js'; Object.freeze(HEADERS.authorization); -var IMAGEMAGICK = false; var _controller = ''; var _owner = ''; var _flags; +var _prefix; // GO ONLINE MODE !global.framework_internal && (global.framework_internal = require('./internal')); @@ -276,42 +310,155 @@ var _flags; !global.framework_utils && (global.framework_utils = require('./utils')); !global.framework_mail && (global.framework_mail = require('./mail')); !global.framework_image && (global.framework_image = require('./image')); -!global.framework_nosql && (global.framework_nosql = require('./nosql')); +!global.framework_session && (global.framework_session = require('./session')); + +require('./tangular'); + +function sessionwrapper(name) { + if (!name) + name = 'default'; + if (F.sessions[name]) + return F.sessions[name]; + var session = new framework_session.Session(name); + session.load(); + if (F.sessionscount) + F.sessionscount++; + else + F.sessionscount = 1; + return F.sessions[name] = session; +} + +global.SESSION = function(name) { + global.framework_session = require('./session'); + global.SESSION = sessionwrapper; + return sessionwrapper(name); +}; + +var TMPENV = framework_utils.copy(process.env); +TMPENV.istotaljsworker = true; + +HEADERS.workers = { cwd: '', silent: false, env: TMPENV }; +HEADERS.workers2 = { cwd: '', silent: true, env: TMPENV }; global.Builders = framework_builders; var U = global.Utils = global.utils = global.U = global.framework_utils; global.Mail = framework_mail; global.WTF = (message, name, uri) => F.problem(message, name, uri); -global.NOBIN = (name) => F.nosql(name).binary; -global.NOCOUNTER = (name) => F.nosql(name).counter; -global.NOMEM = global.NOSQLMEMORY = (name, view) => global.framework_nosql.inmemory(name, view); -global.CONFIG = (name) => F.config[name]; -global.UPTODATE = (type, url, options, interval, callback) => F.uptodate(type, url, options, interval, callback); -global.INSTALL = (type, name, declaration, options, callback) => F.install(type, name, declaration, options, callback); -global.UNINSTALL = (type, name, options) => F.uninstall(type, name, options); -global.RESOURCE = (name, key) => F.resource(name, key); -global.TRANSLATE = (name, key) => F.translate(name, key); -global.TRANSLATOR = (name, text) => F.translator(name, text); -global.TRACE = (message, name, uri, ip) => F.trace(message, name, uri, ip); -global.$$$ = global.GETSCHEMA = (group, name, fn, timeout) => framework_builders.getschema(group, name, fn, timeout); +global.NOBIN = global.NOSQLBINARY = (name) => F.nosql(name).binary; +global.NOSQLSTORAGE = (name) => F.nosql(name).storage; +global.NOCOUNTER = global.NOSQLCOUNTER = (name) => F.nosql(name).counter; + +function nomemwrapper(name) { + return global.framework_nosql.inmemory(name); +} + +global.NOMEM = global.NOSQLMEMORY = function(name) { + if (!global.framework_nosql) + global.framework_nosql = require('./nosql'); + global.NOMEM = global.NOSQLMEMORY = global.framework_nosql.inmemory; + return nomemwrapper(name); +}; + +global.CONFIG = function(name, val) { + return arguments.length === 1 ? CONF[name] : (CONF[name] = val); +}; + +var prefid; + +global.PREF = {}; +global.PREF.set = function(name, value) { + + if (value === undefined) + return F.pref[name]; + + if (value === null) { + delete F.pref[name]; + } else + F.pref[name] = global.PREF[name] = value; + + prefid && clearTimeout(prefid); + prefid = setTimeout(F.onPrefSave, 1000, F.pref); +}; + +global.CACHE = function(name, value, expire, persistent) { + return arguments.length === 1 ? F.cache.get2(name) : F.cache.set(name, value, expire, persistent); +}; + global.CREATE = (group, name) => framework_builders.getschema(group, name).default(); -global.SCRIPT = (body, value, callback, param) => F.script(body, value, callback, param); global.SINGLETON = (name, def) => SINGLETONS[name] || (SINGLETONS[name] = (new Function('return ' + (def || '{}')))()); -global.FUNCTION = (name) => F.functions[name]; -global.ROUTING = (name) => F.routing(name); -global.SCHEDULE = (date, each, fn, param) => F.schedule(date, each, fn, param); +global.FUNCTION = (name) => F.functions[name] || NOOP; global.FINISHED = framework_internal.onFinished; global.DESTROY = framework_internal.destroyStream; -global.UID = () => UIDGENERATOR.date + (++UIDGENERATOR.index).padLeft(4, '0') + UIDGENERATOR.instance + (UIDGENERATOR.index % 2 ? 1 : 0); -global.ROUTE = (a, b, c, d, e) => F.route(a, b, c, d, e); -global.GROUP = (a, b) => F.group(a, b); -global.WEBSOCKET = (a, b, c, d) => F.websocket(a, b, c, d); -global.FILE = (a, b, c) => F.file(a, b, c); -global.REDIRECT = (a, b, c, d) => F.redirect(a, b, c, d); + +function filestoragewrapper(name) { + var key = 'storage_' + name; + return F.databases[key] ? F.databases[key] : (F.databases[key] = new framework_nosql.DatabaseBinary({ name: name }, F.path.databases('fs-' + name + '/'), '.file')); +} + +global.FILESTORAGE = function(name) { + if (!global.framework_nosql) + global.framework_nosql = require('./nosql'); + global.FILESTORAGE = filestoragewrapper; + return filestoragewrapper(name); +}; + +global.UID16 = function(type) { + var index; + if (type) { + if (UIDGENERATOR.types[type]) + index = UIDGENERATOR.types[type] = UIDGENERATOR.types[type] + 1; + else { + UIDGENERATOR.multiple = true; + index = UIDGENERATOR.types[type] = 1; + } + } else + index = UIDGENERATOR.index++; + return UIDGENERATOR.date16 + index.padLeft(3, '0') + UIDGENERATOR.instance + UIDGENERATOR.date16.length + (index % 2 ? 1 : 0) + 'c'; // "c" version +}; + +global.UID = function(type) { + var index; + if (type) { + if (UIDGENERATOR.types[type]) + index = UIDGENERATOR.types[type] = UIDGENERATOR.types[type] + 1; + else { + UIDGENERATOR.multiple = true; + index = UIDGENERATOR.types[type] = 1; + } + } else + index = UIDGENERATOR.index++; + return UIDGENERATOR.date + index.padLeft(3, '0') + UIDGENERATOR.instance + UIDGENERATOR.date.length + (index % 2 ? 1 : 0) + 'b'; // "b" version +}; + +global.UIDF = function(type) { + + var index; + + if (type) { + if (UIDGENERATOR.typesnumber[type]) + index = UIDGENERATOR.typesnumber[type] = UIDGENERATOR.typesnumber[type] + 1; + else { + UIDGENERATOR.multiplenumber = true; + index = UIDGENERATOR.typesnumber[type] = 1; + } + } else + index = UIDGENERATOR.indexnumber++; + + var div = index > 1000 ? 10000 : 1000; + return (UIDGENERATOR.datenumber + (index / div)); +}; + +global.ERROR = function(name) { + return name == null ? F.errorcallback : function(err) { + err && F.error(err, name); + }; +}; + global.AUTH = function(fn) { - F.onAuthorize = fn; + F.onAuthorize = framework_builders.AuthOptions.wrap(fn); }; + global.WEBSOCKETCLIENT = function(callback) { var ws = require('./websocketclient').create(); callback && callback.call(ws, ws); @@ -319,118 +466,356 @@ global.WEBSOCKETCLIENT = function(callback) { }; global.$CREATE = function(schema) { - schema = parseSchema(schema); - var o = framework_builders.getschema(schema[0], schema[1]); + var o = framework_builders.getschema(schema); return o ? o.default() : null; }; global.$MAKE = function(schema, model, filter, callback, novalidate, argument) { - schema = parseSchema(schema); - var o = framework_builders.getschema(schema[0], schema[1]); - return o ? o.make(model, filter, callback, argument, novalidate) : undefined; + + var o = framework_builders.getschema(schema); + var w = null; + + if (typeof(filter) === 'function') { + var tmp = callback; + callback = filter; + filter = tmp; + } + + if (filter instanceof Array) { + w = {}; + for (var i = 0; i < filter.length; i++) + w[filter[i]] = i + 1; + filter = null; + } else if (filter instanceof Object) { + if (!(filter instanceof RegExp)) { + filter = null; + w = filter; + } + } + + return o ? o.make(model, filter, callback, argument, novalidate, w) : undefined; }; global.$QUERY = function(schema, options, callback, controller) { - schema = parseSchema(schema); - var o = framework_builders.getschema(schema[0], schema[1]); - o && o.query(options, callback, controller); + var o = framework_builders.getschema(schema); + if (o) + o.query(options, callback, controller); + else + callback && callback(new Error('Schema "{0}" not found.'.format(getSchemaName(schema)))); return !!o; }; -global.$GET = function(schema, options, callback, controller) { - schema = parseSchema(schema); - var o = framework_builders.getschema(schema[0], schema[1]); - o && o.get(options, callback, controller); +global.$GET = global.$READ = function(schema, options, callback, controller) { + var o = framework_builders.getschema(schema); + if (o) + o.get(options, callback, controller); + else + callback && callback(new Error('Schema "{0}" not found.'.format(getSchemaName(schema)))); return !!o; }; global.$WORKFLOW = function(schema, name, options, callback, controller) { - schema = parseSchema(schema); - var o = framework_builders.getschema(schema[0], schema[1]); - o && o.workflow2(name, options, callback, controller); + var o = framework_builders.getschema(schema); + if (o) + o.workflow2(name, options, callback, controller); + else + callback && callback(new Error('Schema "{0}" not found.'.format(getSchemaName(schema)))); return !!o; }; global.$TRANSFORM = function(schema, name, options, callback, controller) { - schema = parseSchema(schema); - var o = framework_builders.getschema(schema[0], schema[1]); - o && o.transform2(name, options, callback, controller); + var o = framework_builders.getschema(schema); + if (o) + o.transform2(name, options, callback, controller); + else + callback && callback(new Error('Schema "{0}" not found.'.format(getSchemaName(schema)))); return !!o; }; -global.$ASYNC = function(schema, callback, index, controller) { +global.$REMOVE = function(schema, options, callback, controller) { + var o = framework_builders.getschema(schema); - if (index && typeof(index) === 'object') { - controller = index; - index = undefined; + if (typeof(options) === 'function') { + controller = callback; + callback = options; + options = EMPTYOBJECT; } - schema = parseSchema(schema); - var o = framework_builders.getschema(schema[0], schema[1]).default(); - controller && (o.$$controller = controller); - return o.$async(callback, index); -}; - -global.$OPERATION = function(schema, name, options, callback, controller) { - schema = parseSchema(schema); - var o = framework_builders.getschema(schema[0], schema[1]); - o && o.operation2(name, options, callback, controller); + if (o) + o.remove(options, callback, controller); + else + callback && callback(new Error('Schema "{0}" not found.'.format(getSchemaName(schema)))); return !!o; }; -global.DB = global.DATABASE = function() { - return typeof(F.database) === 'object' ? F.database : F.database.apply(framework, arguments); +global.$SAVE = function(schema, model, options, callback, controller, novalidate) { + return performschema('$save', schema, model, options, callback, controller, novalidate); }; -global.ON = function() { - return F.on.apply(F, arguments); +global.$INSERT = function(schema, model, options, callback, controller, novalidate) { + return performschema('$insert', schema, model, options, callback, controller, novalidate); }; -global.OFF = function() { - return arguments.length > 1 ? F.removeListener.apply(F, arguments) : F.removeAllListeners.apply(F, arguments); +global.$UPDATE = function(schema, model, options, callback, controller, novalidate) { + return performschema('$update', schema, model, options, callback, controller, novalidate); }; -global.EMIT = function() { - return F.emit.apply(F, arguments); +global.$PATCH = function(schema, model, options, callback, controller, novalidate) { + return performschema('$patch', schema, model, options, callback, controller, novalidate); }; -global.LOG = function() { - return F.log.apply(F, arguments); -}; +// GET Users/Neviem --> @query @workflow +global.$ACTION = function(schema, model, callback, controller) { -global.LOGGER = function() { - return F.logger.apply(F, arguments); -}; + if (typeof(model) === 'function') { + controller = callback; + callback = model; + model = null; + } + + var meta = F.temporary.other[schema]; + var tmp, index; + + if (!meta) { + + index = schema.indexOf('-->'); + + var op = (schema.substring(index + 3).trim().trim() + ' ').split(/\s@/).trim(); + tmp = schema.substring(0, index).split(/\s|\t/).trim(); + + if (tmp.length !== 2) { + callback('Invalid "{0}" type.'.format(schema)); + return; + } + + meta = {}; + meta.method = tmp[0].toUpperCase(); + meta.schema = tmp[1]; + + if (meta.schema[0] === '*') + meta.schema = meta.schema.substring(1); + + meta.op = []; + meta.opcallbackindex = -1; + + var name = meta.schema.split('/'); + var o = GETSCHEMA(name[0], name[1]); + if (!o) { + callback(new ErrorBuilder().push('', 'Schema "{0}" not found'.format(meta.schema))); + return; + } + + for (var i = 0; i < op.length; i++) { + + tmp = {}; + + var item = op[i]; + if (item[0] === '@') + item = item.substring(1); + + index = item.indexOf('('); + + if (index !== -1) { + meta.opcallbackindex = i; + tmp.response = true; + item = item.substring(0, index).trim(); + } + + tmp.name = item; + tmp.name2 = '$' + tmp.name; + + if (o.meta[item] === undefined) { + if (o.meta['workflow#' + item] !== undefined) + tmp.type = '$workflow'; + else if (o.meta['transform#' + item] !== undefined) + tmp.type = '$transform'; + else if (o.meta['operation#' + item] !== undefined) + tmp.type = '$operation'; + else if (o.meta['hook#' + item] !== undefined) + tmp.type = '$hook'; + else { + callback(new ErrorBuilder().push('', 'Schema "{0}" doesn\'t contain "{1}" operation.'.format(meta.schema, item))); + return; + } + } + + if (tmp.type) + tmp.type2 = tmp.type.substring(1); -global.MAKE = global.TRANSFORM = function(transform, fn) { + meta.op.push(tmp); + } - if (typeof(transform) === 'function') { - var tmp = fn; - fn = transform; - transform = tmp; + meta.multiple = meta.op.length > 1; + meta.schema = o; + meta.validate = meta.method !== 'GET'; + F.temporary.other[schema] = meta; } - var obj; + if (meta.validate) { + + var req = controller ? controller.req : null; + if (meta.method === 'PATCH' || meta.method === 'DELETE') { + if (!req) + req = {}; + req.$patch = true; + } - if (typeof(fn) === 'function') { - obj = {}; - fn.call(obj, obj); + var data = {}; + data.meta = meta; + data.callback = callback; + data.controller = controller; + meta.schema.make(model, null, performsschemaaction_async, data, null, null, req); } else - obj = fn; + performsschemaaction(meta, null, callback, controller); + +}; + +function performsschemaaction_async(err, response, data) { + if (err) + data.callback(err); + else + performsschemaaction(data.meta, response, data.callback, data.controller); +} + +function performsschemaaction(meta, model, callback, controller) { + + if (meta.multiple) { + + if (!model) + model = meta.schema.default(); + + model.$$controller = controller; + var async = model.$async(callback, meta.opcallbackindex === - 1 ? null : meta.opcallbackindex); + + for (var i = 0; i < meta.op.length; i++) { + var op = meta.op[i]; + if (op.type) + async[op.type](op.name); + else + async[op.name2](); + } + + } else { + + var op = meta.op[0]; + + if (model) { + model.$$controller = controller; + if (op.type) + model[op.type](op.name, EMPTYOBJECT, callback); + else + model[op.name2](EMPTYOBJECT, callback); + } else { + if (op.type) + meta.schema[op.type2 + '2'](op.name, EMPTYOBJECT, callback, controller); + else + meta.schema[op.name](EMPTYOBJECT, callback, controller); + } + } +} + +// type, schema, model, options, callback, controller +function performschema(type, schema, model, options, callback, controller, novalidate) { + + if (typeof(options) === 'function') { + novalidate = controller; + controller = callback; + callback = options; + options = null; + } + + var o = framework_builders.getschema(schema); + + if (!o) { + callback && callback(new Error('Schema "{0}" not found.'.format(getSchemaName(schema)))); + return false; + } + + var workflow = {}; + workflow[type.substring(1)] = 1; + + var req = controller ? controller.req : null; + var keys; + + if (type === '$patch') { + keys = Object.keys(model); + if (req) + req.$patch = true; + else + req = { $patch: true }; + } + + o.make(model, null, function(err, model) { + if (err) { + callback && callback(err); + } else { + model.$$keys = keys; + model.$$controller = controller; + model[type](options, callback); + if (req && req.$patch && req.method && (req.method !== 'PATCH' & req.method !== 'DELETE')) + delete req.$patch; + } + }, null, novalidate, workflow, req); + + return !!o; +} + +global.$ASYNC = function(schema, callback, index, controller) { + + if (index && typeof(index) === 'object') { + controller = index; + index = undefined; + } + + var o = framework_builders.getschema(schema).default(); + + if (!o) { + callback && callback(new Error('Schema "{0}" not found.'.format(getSchemaName(schema)))); + return EMPTYOBJECT; + } + + controller && (o.$$controller = controller); + return o.$async(callback, index); +}; + +global.$OPERATION = function(schema, name, options, callback, controller) { + var o = framework_builders.getschema(schema); + if (o) + o.operation2(name, options, callback, controller); + else + callback && callback(new Error('Schema "{0}" not found.'.format(getSchemaName(schema)))); + return !!o; +}; - return transform ? TransformBuilder.transform.apply(obj, arguments) : obj; +global.DB = global.DATABASE = function(a, b, c, d) { + return typeof(F.database) === 'object' ? F.database : F.database(a, b, c, d); }; -global.NEWTRANSFORM = function() { - return TransformBuilder.addTransform.apply(this, arguments); +global.OFF = function() { + return arguments.length > 1 ? F.removeListener.apply(F, arguments) : F.removeAllListeners.apply(F, arguments); }; -global.NEWSCHEMA = function(group, name) { +global.NEWSCHEMA = function(group, name, make) { + + if (typeof(name) === 'function') { + make = name; + name = undefined; + } + if (!name) { - name = group; - group = 'default'; + var arr = group.split('/'); + if (arr.length === 2) { + name = arr[1]; + group = arr[0]; + } else { + name = group; + group = 'default'; + } } - return framework_builders.newschema(group, name); + + var schema = framework_builders.newschema(group, name); + make && make.call(schema, schema); + return schema; }; global.CLEANUP = function(stream, callback) { @@ -454,7 +839,7 @@ global.SUCCESS = function(success, value) { var err; if (success instanceof Error) { - err = success; + err = success.toString(); success = false; } else if (success instanceof framework_builders.ErrorBuilder) { if (success.hasError()) { @@ -466,7 +851,7 @@ global.SUCCESS = function(success, value) { success = true; SUCCESSHELPER.success = !!success; - SUCCESSHELPER.value = value == null ? undefined : value; + SUCCESSHELPER.value = value === SUCCESSHELPER ? value.value : value == null ? undefined : (value && value.$$schema ? value.$clean() : value); SUCCESSHELPER.error = err ? err : undefined; return SUCCESSHELPER; }; @@ -482,7 +867,11 @@ global.TRY = function(fn, err) { }; global.OBSOLETE = function(name, message) { - console.log(F.datetime.format('yyyy-MM-dd HH:mm:ss') + ' :: OBSOLETE / IMPORTANT ---> "' + name + '"', message); + + if (F.config.nowarnings) + return; + + console.log(NOW.format('yyyy-MM-dd HH:mm:ss') + ' :: OBSOLETE / IMPORTANT ---> "' + name + '"', message); if (global.F) F.stats.other.obsolete++; }; @@ -498,10 +887,48 @@ var directory = U.$normalize(require.main ? Path.dirname(require.main.filename) // F.service() changes the values below: var DATE_EXPIRES = new Date().add('y', 1).toUTCString(); -const WEBSOCKET_COMPRESS = U.createBuffer([0x00, 0x00, 0xFF, 0xFF]); +const _randomstring = 'abcdefghijklmnoprstuwxy'.split(''); + +function random2string() { + return _randomstring[(Math.random() * _randomstring.length) >> 0] + _randomstring[(Math.random() * _randomstring.length) >> 0]; +} + +const WEBSOCKET_COMPRESS = Buffer.from([0x00, 0x00, 0xFF, 0xFF]); const WEBSOCKET_COMPRESS_OPTIONS = { windowBits: Zlib.Z_DEFAULT_WINDOWBITS }; -const UIDGENERATOR = { date: new Date().format('yyMMddHHmm'), instance: 'abcdefghijklmnoprstuwxy'.split('').random().join('').substring(0, 3), index: 1 }; -const EMPTYBUFFER = U.createBufferSize(0); +const UIDGENERATOR = { types: {}, typesnumber: {} }; + +function UIDGENERATOR_REFRESH() { + + var ticks = NOW.getTime(); + var dt = Math.round(((ticks - 1580511600000) / 1000 / 60)); + + UIDGENERATOR.date = dt + ''; + UIDGENERATOR.date16 = dt.toString(16); + + var seconds = ((NOW.getSeconds() / 60) + '').substring(2, 4); + UIDGENERATOR.datenumber = +((((ticks - 1580511600000) / 1000 / 60) >> 0) + seconds); // 1580511600000 means 1.1.2020 + UIDGENERATOR.indexnumber = 1; + UIDGENERATOR.index = 1; + UIDGENERATOR.instance = random2string(); + + var keys; + + if (UIDGENERATOR.multiple) { + keys = Object.keys(UIDGENERATOR.types); + for (var i = 0; i < keys.length; i++) + UIDGENERATOR.types[keys[i]] = 0; + } + + if (UIDGENERATOR.multiplenumber) { + keys = Object.keys(UIDGENERATOR.typesnumber); + for (var i = 0; i < keys.length; i++) + UIDGENERATOR.typesnumber[keys[i]] = 0; + } +} + +UIDGENERATOR_REFRESH(); + +const EMPTYBUFFER = Buffer.alloc(0); global.EMPTYBUFFER = EMPTYBUFFER; const controller_error_status = function(controller, status, problem) { @@ -518,6 +945,7 @@ const controller_error_status = function(controller, status, problem) { controller.req.$total_route = F.lookup(controller.req, '#' + status, EMPTYARRAY, 0); controller.req.$total_exception = problem; controller.req.$total_execute(status, true); + return controller; }; @@ -525,135 +953,199 @@ var PERF = {}; function Framework() { - this.$id = null; // F.id ==> property - this.version = 2940; - this.version_header = '2.9.4'; - this.version_node = process.version.toString().replace('v', '').replace(/\./g, '').parseFloat(); + var self = this; - this.config = { + self.$id = null; // F.id ==> property + self.version = 3413; + self.version_header = '3.4.13'; + self.version_node = process.version.toString(); + self.syshash = (__dirname + '-' + Os.hostname() + '-' + Os.platform() + '-' + Os.arch() + '-' + Os.release() + '-' + Os.tmpdir() + JSON.stringify(process.versions)).md5(); + self.pref = global.PREF; + global.CONF = self.config = { debug: true, trace: true, - 'trace-console': true, + trace_console: true, + //nowarnings: process.argv.indexOf('restart') !== -1, + nowarnings: true, name: 'Total.js', version: '1.0.0', author: '', - secret: Os.hostname() + '-' + Os.platform() + '-' + Os.arch(), - - 'default-xpoweredby': 'Total.js', - 'etag-version': '', - 'directory-controllers': '/controllers/', - 'directory-components': '/components/', - 'directory-views': '/views/', - 'directory-definitions': '/definitions/', - 'directory-temp': '/tmp/', - 'directory-models': '/models/', - 'directory-resources': '/resources/', - 'directory-public': '/public/', - 'directory-public-virtual': '/app/', - 'directory-modules': '/modules/', - 'directory-source': '/source/', - 'directory-logs': '/logs/', - 'directory-tests': '/tests/', - 'directory-databases': '/databases/', - 'directory-workers': '/workers/', - 'directory-packages': '/packages/', - 'directory-private': '/private/', - 'directory-isomorphic': '/isomorphic/', - 'directory-configs': '/configs/', - 'directory-services': '/services/', - 'directory-themes': '/themes/', + secret: self.syshash, + secret_uid: self.syshash.substring(10), + + 'security.txt': 'Contact: mailto:support@totaljs.com\nContact: https://www.totaljs.com/contact/', + etag_version: '', + directory_src: '/.src/', + directory_bundles: '/bundles/', + directory_controllers: '/controllers/', + directory_components: '/components/', + directory_views: '/views/', + directory_definitions: '/definitions/', + directory_temp: '/tmp/', + directory_models: '/models/', + directory_schemas: '/schemas/', + directory_operations: '/operations/', + directory_resources: '/resources/', + directory_public: '/public/', + directory_public_virtual: '/app/', + directory_modules: '/modules/', + directory_source: '/source/', + directory_logs: '/logs/', + directory_tests: '/tests/', + directory_databases: '/databases/', + directory_workers: '/workers/', + directory_packages: '/packages/', + directory_private: '/private/', + directory_isomorphic: '/isomorphic/', + directory_configs: '/configs/', + directory_services: '/services/', + directory_themes: '/themes/', + directory_tasks: '/tasks/', + directory_updates: '/updates/', // all HTTP static request are routed to directory-public - 'static-url': '', - 'static-url-script': '/js/', - 'static-url-style': '/css/', - 'static-url-image': '/img/', - 'static-url-video': '/video/', - 'static-url-font': '/fonts/', - 'static-url-download': '/download/', - 'static-url-components': '/components.', - 'static-accepts': { 'flac': true, 'jpg': true, 'jpeg': true, 'png': true, 'gif': true, 'ico': true, 'js': true, 'css': true, 'txt': true, 'xml': true, 'woff': true, 'woff2': true, 'otf': true, 'ttf': true, 'eot': true, 'svg': true, 'zip': true, 'rar': true, 'pdf': true, 'docx': true, 'xlsx': true, 'doc': true, 'xls': true, 'html': true, 'htm': true, 'appcache': true, 'manifest': true, 'map': true, 'ogv': true, 'ogg': true, 'mp4': true, 'mp3': true, 'webp': true, 'webm': true, 'swf': true, 'package': true, 'json': true, 'md': true, 'm4v': true, 'jsx': true }, + static_url: '', + static_url_script: '/js/', + static_url_style: '/css/', + static_url_image: '/img/', + static_url_video: '/video/', + static_url_font: '/fonts/', + static_url_download: '/download/', + static_url_components: '/components.', + static_accepts: { flac: true, jpg: true, jpeg: true, png: true, gif: true, ico: true, js: true, mjs: true, css: true, txt: true, xml: true, woff: true, woff2: true, otf: true, ttf: true, eot: true, svg: true, zip: true, rar: true, pdf: true, docx: true, xlsx: true, doc: true, xls: true, html: true, htm: true, appcache: true, manifest: true, map: true, ogv: true, ogg: true, mp4: true, mp3: true, webp: true, webm: true, swf: true, package: true, json: true, md: true, m4v: true, jsx: true, heif: true, heic: true, ics: true }, // 'static-accepts-custom': [], - - 'default-layout': 'layout', - 'default-theme': '', + default_crypto_iv: Buffer.from(self.syshash).slice(0, 16), + default_xpoweredby: 'Total.js', + default_layout: 'layout', + default_theme: '', + default_proxy: '', + default_request_maxkeys: 33, + default_request_maxkey: 25, // default maximum request size / length // default 10 kB - 'default-request-length': 10, - 'default-websocket-request-length': 2, - 'default-websocket-encodedecode': true, - 'default-maximum-file-descriptors': 0, - 'default-timezone': '', - 'default-root': '', - 'default-response-maxage': '11111111', - 'default-errorbuilder-status': 200, + default_request_maxlength: 10, + default_websocket_maxlength: 2, + default_websocket_encodedecode: true, + default_maxopenfiles: 100, + default_timezone: 'utc', + default_root: '', + default_response_maxage: '11111111', + default_errorbuilder_status: 200, + + // Default originators + default_cors: null, // Seconds (2 minutes) - 'default-cors-maxage': 120, + default_cors_maxage: 120, // in milliseconds - 'default-request-timeout': 3000, - 'default-dependency-timeout': 1500, + default_request_timeout: 3000, + default_dependency_timeout: 1500, + default_restbuilder_timeout: 10000, // otherwise is used ImageMagick (Heroku supports ImageMagick) - // gm = graphicsmagick or im = imagemagick - 'default-image-converter': 'gm', - 'default-image-quality': 93, - 'default-image-consumption': 30, - - 'allow-static-files': true, - 'allow-gzip': true, - 'allow-websocket': true, - 'allow-websocket-compression': true, - 'allow-compile': true, - 'allow-compile-script': true, - 'allow-compile-style': true, - 'allow-compile-html': true, - 'allow-performance': false, - 'allow-custom-titles': false, - 'allow-cache-snapshot': false, - 'allow-debug': false, - 'allow-head': false, - 'allow-filter-errors': true, - 'disable-strict-server-certificate-validation': true, - 'disable-clear-temporary-directory': false, + // gm = graphicsmagick or im = imagemagick or magick (new version of ImageMagick) + default_image_converter: 'gm', // command-line name + default_image_quality: 93, + default_image_consumption: 0, // disabled because e.g. GM v1.3.32 throws some error about the memory + + allow_static_files: true, + allow_gzip: true, + allow_websocket: true, + allow_websocket_compression: true, + allow_compile: true, + allow_compile_script: true, + allow_compile_style: true, + allow_compile_html: true, + allow_localize: true, + allow_stats_snapshot: true, + allow_performance: false, + allow_custom_titles: false, + allow_cache_snapshot: false, + allow_cache_cluster: false, + allow_debug: false, + allow_head: false, + allow_filter_errors: true, + allow_clear_temp: true, + allow_ssc_validation: true, + allow_workers_silent: false, + allow_sessions_unused: '-20 minutes', + allow_reqlimit: 0, + allow_persistent_images: false, + + nosql_worker: false, + nosql_inmemory: null, // String Array + nosql_cleaner: 1440, + nosql_logger: true, + logger: false, // Used in F.service() // All values are in minutes - 'default-interval-clear-resources': 20, - 'default-interval-clear-cache': 10, - 'default-interval-precompile-views': 61, - 'default-interval-websocket-ping': 3, - 'default-interval-clear-dnscache': 120, - 'default-interval-uptodate': 5 + default_interval_clear_resources: 20, + default_interval_clear_cache: 10, + default_interval_clear_dnscache: 30, + default_interval_precompile_views: 61, + default_interval_websocket_ping: 3, + default_interval_uptodate: 5, + + set ['mail-smtp'] (val) { + CONF['mail_smtp'] = val; + return null; + }, + + set ['mail-smtp-options'] (val) { + CONF['mail_smtp_options'] = val; + return null; + }, + + set ['mail-address-reply'] (val) { + CONF['mail_address_reply'] = val; + return null; + }, + + set ['mail-address-from'] (val) { + CONF['mail_address_from'] = val; + return null; + }, + + set ['mail-address-copy'] (val) { + CONF['mail_address_copy'] = val; + return null; + } }; - this.global = {}; - this.resources = {}; - this.connections = {}; - this.functions = {}; - this.themes = {}; - this.versions = null; - this.workflows = {}; - this.uptodates = null; - this.schedules = []; - - this.isDebug = true; - this.isTest = false; - this.isLoaded = false; - this.isWorker = true; - this.isCluster = process.env.PASSENGER_APP_ENV ? false : require('cluster').isWorker; - - this.routes = { + global.REPO = global.G = self.global = {}; + global.MAIN = {}; + global.TEMP = {}; + + self.$bundling = true; + self.resources = {}; + self.connections = {}; + global.FUNC = self.functions = {}; + self.themes = {}; + self.versions = null; + self.workflows = {}; + self.uptodates = null; + self.schedules = {}; + + self.isDebug = true; + self.isTest = false; + self.isLoaded = false; + self.isWorker = true; + self.isCluster = process.env.PASSENGER_APP_ENV ? false : require('cluster').isWorker; + + self.routes = { sitemap: null, web: [], system: {}, files: [], + filesfallback: null, cors: [], + corsall: false, websockets: [], middleware: {}, redirects: {}, @@ -664,49 +1156,67 @@ function Framework() { mapping: {}, packages: {}, blocks: {}, - resources: {}, - mmr: {} + proxies: [], + resources: {} }; - this.owners = []; - this.modificators = null; - this.helpers = {}; - this.modules = {}; - this.models = {}; - this.sources = {}; - this.controllers = {}; - this.dependencies = {}; - this.isomorphic = {}; - this.components = { has: false, css: false, js: false, views: {}, instances: {}, version: null, links: '', groups: {} }; - this.convertors = []; - this.tests = []; - this.errors = []; - this.problems = []; - this.changes = []; - this.server = null; - this.port = 0; - this.ip = ''; - - this.validators = { + self.owners = []; + self.modificators = null; + self.modificators2 = null; + DEF.helpers = self.helpers = {}; + self.modules = {}; + self.models = {}; + self.sources = {}; + self.controllers = {}; + self.dependencies = {}; + self.isomorphic = {}; + self.components = { has: false, css: false, js: false, views: {}, instances: {}, version: null, links: '', groups: {}, files: {} }; + self.convertors = []; + self.convertors2 = null; + self.tests = []; + self.errors = []; + self.timeouts = []; + self.problems = []; + self.changes = []; + self.server = null; + self.port = 0; + self.ip = ''; + + DEF.validators = self.validators = { email: new RegExp('^[a-zA-Z0-9-_.+]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'), - url: /^(https?:\/\/(?:www\.|(?!www))[^\s.#!:?+=&@!$'~*,;/()[\]]+\.[^\s#!?+=&@!$'~*,;()[\]\\]{2,}\/?|www\.[^\s#!:.?+=&@!$'~*,;/()[\]]+\.[^\s#!?+=&@!$'~*,;()[\]\\]{2,}\/?)/i, - phone: /^[+]?[(]?[0-9]{3}[)]?[-\s.]?[0-9]{3}[-\s.]?[0-9]{4,6}$/im, - zip: /^\d{5}(?:[-\s]\d{4})?$/, - uid: /^\d{14,}[a-z]{3}[01]{1}$/ + url: /^http(s)?:\/\/[^,{}\\]*$/i, + phone: /^[+]?[(]?[0-9]{3}[)]?[-\s.]?[0-9]{3}[-\s.]?[0-9]{4,8}$/im, + zip: /^[0-9a-z\-\s]{3,20}$/i, + uid: /^\d{14,}[a-z]{3}[01]{1}|^\d{9,14}[a-z]{2}[01]{1}a|^\d{4,18}[a-z]{2}\d{1}[01]{1}b|^[0-9a-f]{4,18}[a-z]{2}\d{1}[01]{1}c|^[0-9a-z]{4,18}[a-z]{2}\d{1}[01]{1}d$/ }; - this.workers = {}; - this.databases = {}; - this.directory = HEADERS.workers.cwd = directory; - this.isLE = Os.endianness ? Os.endianness() === 'LE' : true; - this.isHTTPS = false; - this.datetime = new Date(); + self.workers = {}; + self.sessions = {}; + self.flows = {}; + self.databases = {}; + self.databasescleaner = {}; + self.directory = HEADERS.workers2.cwd = HEADERS.workers.cwd = directory; + self.isLE = Os.endianness ? Os.endianness() === 'LE' : true; + self.isHTTPS = false; + + // Fix for workers crash (port in use) when debugging main process with --inspect or --debug + // See: https://github.com/nodejs/node/issues/14325 and https://github.com/nodejs/node/issues/9435 + for (var i = 0; i < process.execArgv.length; i++) { + // Setting inspect/debug port to random unused + if ((/inspect|debug/).test(process.execArgv[i])) { + process.execArgv[i] = '--inspect=0'; + break; + } + } + + HEADERS.workers.execArgv = process.execArgv; // It's hidden - // this.waits = {}; + // self.waits = {}; - this.temporary = { + self.temporary = { path: {}, + shortcache: {}, notfound: {}, processing: {}, range: {}, @@ -714,18 +1224,35 @@ function Framework() { versions: {}, dependencies: {}, // temporary for module dependencies other: {}, + keys: {}, // for crypto keys internal: {}, // controllers/modules names for the routing owners: {}, - ready: {} + ready: {}, + ddos: {}, + service: { redirect: 0, request: 0, file: 0, usage: 0 } }; - this.stats = { + self.stats = { + + error: 0, + + performance: { + request: 0, + message: 0, + external: 0, + file: 0, + open: 0, + dbrm: 0, + dbwm: 0, + online: 0, + usage: 0, + mail: 0 + }, other: { websocketPing: 0, websocketCleaner: 0, obsolete: 0, - restart: 0, mail: 0 }, @@ -741,16 +1268,17 @@ function Framework() { head: 0, post: 0, put: 0, - path: 0, + patch: 0, upload: 0, schema: 0, - mmr: 0, + operation: 0, blocked: 0, 'delete': 0, mobile: 0, desktop: 0 }, response: { + ddos: 0, view: 0, json: 0, websocket: 0, @@ -767,44 +1295,49 @@ function Framework() { empty: 0, redirect: 0, forward: 0, + proxy: 0, notModified: 0, sse: 0, - mmr: 0, errorBuilder: 0, error400: 0, error401: 0, error403: 0, error404: 0, error408: 0, + error409: 0, error431: 0, error500: 0, - error501: 0 + error501: 0, + error503: 0 } }; // intialize cache - this.cache = new FrameworkCache(); - this.path = new FrameworkPath(); - - this._request_check_redirect = false; - this._request_check_referer = false; - this._request_check_POST = false; - this._request_check_robot = false; - this._length_middleware = 0; - this._length_request_middleware = 0; - this._length_files = 0; - this._length_wait = 0; - this._length_themes = 0; - this._length_cors = 0; - this._length_subdomain_web = 0; - this._length_subdomain_websocket = 0; - this._length_convertors = 0; - - this.isVirtualDirectory = false; - this.isTheme = false; - this.isWindows = Os.platform().substring(0, 3).toLowerCase() === 'win'; - - this.$events = {}; + self.cache = new FrameworkCache(); + self.path = global.PATH = new FrameworkPath(); + + self._request_check_redirect = false; + self._request_check_referer = false; + self._request_check_POST = false; + self._request_check_robot = false; + self._request_check_mobile = false; + self._request_check_proxy = false; + self._length_middleware = 0; + self._length_request_middleware = 0; + self._length_files = 0; + self._length_wait = 0; + self._length_themes = 0; + self._length_cors = 0; + self._length_subdomain_web = 0; + self._length_subdomain_websocket = 0; + self._length_convertors = 0; + + self.isVirtualDirectory = false; + self.isTheme = false; + self.isWindows = Os.platform().substring(0, 3).toLowerCase() === 'win'; + + self.$events = {}; + self.commands = { reload_preferences: [loadpreferences] }; } // ====================================================== @@ -812,6 +1345,12 @@ function Framework() { // ====================================================== Framework.prototype = { + get datetime() { + return global.NOW; + }, + set datetime(val) { + global.NOW = val; + }, get cluster() { return require('./cluster'); }, @@ -819,26 +1358,72 @@ Framework.prototype = { return F.$id; }, set id(value) { - CLUSTER_CACHE_SET.id = value; - CLUSTER_CACHE_REMOVE.id = value; - CLUSTER_CACHE_REMOVEALL.id = value; - CLUSTER_CACHE_CLEAR.id = value; + CLUSTER_CACHE_SET.ID = value; + CLUSTER_CACHE_REMOVE.ID = value; + CLUSTER_CACHE_REMOVEALL.ID = value; + CLUSTER_CACHE_CLEAR.ID = value; F.$id = value; return F.$id; - }, - get onLocate() { - return this.onLocale; - }, - set onLocate(value) { - OBSOLETE('F.onLocate', 'Rename "F.onLocate" method for "F.onLocale".'); - this.onLocale = value; } }; var framework = new Framework(); global.framework = global.F = module.exports = framework; +global.CMD = function(key, a, b, c, d) { + if (F.commands[key]) { + for (var i = 0; i < F.commands[key].length; i++) + F.commands[key][i](a, b, c, d); + } +}; + +F.callback_redirect = function(url) { + this.url = url; +}; + +F.dir = function(path) { + F.directory = path; + directory = path; +}; + +F.refresh = function() { + + NOW = new Date(); + + F.$events.clear && EMIT('clear', 'temporary', F.temporary); + F.temporary.path = {}; + F.temporary.range = {}; + F.temporary.views = {}; + F.temporary.other = {}; + F.temporary.keys = {}; + global.$VIEWCACHE && global.$VIEWCACHE.length && (global.$VIEWCACHE = []); + + // Clears command cache + Image.clear(); + + CONF.allow_debug && F.consoledebug('clear temporary cache'); + + var keys = Object.keys(F.temporary.internal); + for (var i = 0; i < keys.length; i++) + if (!F.temporary.internal[keys[i]]) + delete F.temporary.internal[keys[i]]; + + F.$events.clear && EMIT('clear', 'resources'); + F.resources = {}; + CONF.allow_debug && F.consoledebug('clear resources'); + + F.$events.clear && EMIT('clear', 'dns'); + CMD('clear_dnscache'); + CONF.allow_debug && F.consoledebug('clear DNS cache'); + + return F; +}; + F.prototypes = function(fn) { + + if (!global.framework_nosql) + global.framework_nosql = require('./nosql'); + var proto = {}; proto.Chunker = framework_utils.Chunker.prototype; proto.Controller = Controller.prototype; @@ -847,12 +1432,15 @@ F.prototypes = function(fn) { proto.DatabaseBuilder = framework_nosql.DatabaseBuilder.prototype; proto.DatabaseBuilder2 = framework_nosql.DatabaseBuilder2.prototype; proto.DatabaseCounter = framework_nosql.DatabaseCounter.prototype; + proto.DatabaseStorage = framework_nosql.DatabaseStorage.prototype; + proto.DatabaseTable = framework_nosql.DatabaseTable.prototype; proto.ErrorBuilder = framework_builders.ErrorBuilder.prototype; proto.HttpFile = framework_internal.HttpFile.prototype; proto.HttpRequest = PROTOREQ; proto.HttpResponse = PROTORES; proto.Image = framework_image.Image.prototype; proto.Message = Mail.Message.prototype; + proto.MiddlewareOptions = MiddlewareOptions.prototype; proto.OperationOptions = framework_builders.OperationOptions.prototype; proto.Page = framework_builders.Page.prototype; proto.Pagination = framework_builders.Pagination.prototype; @@ -860,55 +1448,75 @@ F.prototypes = function(fn) { proto.RESTBuilderResponse = framework_builders.RESTBuilderResponse.prototype; proto.SchemaBuilder = framework_builders.SchemaBuilder.prototype; proto.SchemaOptions = framework_builders.SchemaOptions.prototype; - proto.TransformBuilder = framework_builders.TransformBuilder.prototype; proto.UrlBuilder = framework_builders.UrlBuilder.prototype; proto.WebSocket = WebSocket.prototype; proto.WebSocketClient = WebSocketClient.prototype; + proto.AuthOptions = framework_builders.AuthOptions.prototype; fn.call(proto, proto); return F; }; -F.on = function(name, fn) { +global.ON = F.on = function(name, fn) { if (name === 'init' || name === 'ready' || name === 'load') { - if (this.isLoaded) { - fn.call(this); + if (F.isLoaded) { + fn.call(F); return; } } else if (name.indexOf('#') !== -1) { var arr = name.split('#'); switch (arr[0]) { case 'middleware': - F.temporary.ready[name] && fn.call(this); + F.temporary.ready[name] && fn.call(F); break; case 'component': - F.temporary.ready[name] && fn.call(this); + F.temporary.ready[name] && fn.call(F); break; case 'model': - F.temporary.ready[name] && fn.call(this, F.models[arr[1]]); + F.temporary.ready[name] && fn.call(F, F.models[arr[1]]); break; case 'source': - F.temporary.ready[name] && fn.call(this, F.sources[arr[1]]); + F.temporary.ready[name] && fn.call(F, F.sources[arr[1]]); break; case 'package': case 'module': - F.temporary.ready[name] && fn.call(this, F.modules[arr[1]]); + F.temporary.ready[name] && fn.call(F, F.modules[arr[1]]); break; case 'controller': - F.temporary.ready[name] && fn.call(this, F.controllers[arr[1]]); + F.temporary.ready[name] && fn.call(F, F.controllers[arr[1]]); break; } } + switch (name) { + case 'cache-set': + case 'controller-render-meta': + case 'request-end': + case 'websocket-begin': + case 'websocket-end': + case 'request-begin': + case 'upload-begin': + case 'upload-end': + OBSOLETE(name, 'Name of event has been replaced to "{0}"'.format(name.replace(/-/g, '_'))); + break; + case 'cache-expire': + OBSOLETE(name, 'Name of event has been replaced to "cache_expired"'); + break; + } + + if (isWORKER && name === 'service' && !F.cache.interval) + F.cache.init_timer(); + if (F.$events[name]) F.$events[name].push(fn); else F.$events[name] = [fn]; - return this; + return F; }; -F.emit = function(name, a, b, c, d, e, f, g) { +global.EMIT = F.emit = function(name, a, b, c, d, e, f, g) { + var evt = F.$events[name]; if (evt) { var clean = false; @@ -928,7 +1536,7 @@ F.emit = function(name, a, b, c, d, e, f, g) { return F; }; -F.once = function(name, fn) { +global.ONCE = F.once = function(name, fn) { fn.$once = true; return F.on(name, fn); }; @@ -968,8 +1576,12 @@ F.isSuccess = function(obj) { F.convert = function(value, convertor) { if (convertor) { - if (F.convertors.findIndex('name', value) !== -1) + + if (F.convertors.findIndex('name', value) !== -1) { + if (convertor == null) + F.convertors = F.convertors.remove('name', value); return false; + } if (convertor === Number) convertor = U.parseFloat; @@ -990,7 +1602,7 @@ F.convert = function(value, convertor) { convertor = U.parseInt2; break; default: - return console.log('F.convert unknown convertor type:', convertor); + return console.log('Unknown convertor type:', convertor); } } @@ -999,9 +1611,11 @@ F.convert = function(value, convertor) { return true; } - for (var i = 0, length = F.convertors.length; i < length; i++) { - if (value[F.convertors[i].name]) - value[F.convertors[i].name] = F.convertors[i].convertor(value[F.convertors[i].name]); + if (value) { + for (var i = 0, length = F.convertors.length; i < length; i++) { + if (value[F.convertors[i].name] != null) + value[F.convertors[i].name] = F.convertors[i].convertor(value[F.convertors[i].name]); + } } return value; @@ -1022,10 +1636,11 @@ F.controller = function(name) { * @return {Framework} */ F.useConfig = function(name) { + OBSOLETE('F.useConfig', 'F.useConfig will be moreved in Total.js v4'); return F.$configure_configs(name, true); }; -F.useSMTP = function(smtp, options, callback) { +Mail.use = function(smtp, options, callback) { if (typeof(options) === 'function') { callback = options; @@ -1035,9 +1650,9 @@ F.useSMTP = function(smtp, options, callback) { Mail.try(smtp, options, function(err) { if (!err) { - delete F.temporary['mail-settings']; - F.config['mail-smtp'] = smtp; - F.config['mail-smtp-options'] = options; + delete F.temporary.mail_settings; + CONF.mail_smtp = smtp; + CONF.mail_smtp_options = options; } if (callback) @@ -1045,7 +1660,11 @@ F.useSMTP = function(smtp, options, callback) { else if (err) F.error(err, 'F.useSMTP()', null); }); +}; +F.useSMTP = function(smtp, options, callback) { + OBSOLETE('F.useSMTP', 'Use `Mail.use() instead of F.useSMTP()'); + Mail.use(smtp, options, callback); return F; }; @@ -1097,14 +1716,14 @@ F.$routesSort = function(type) { F.parseComponent = parseComponent; -F.script = function(body, value, callback, param) { +global.SCRIPT = F.script = function(body, value, callback, param) { var fn; var compilation = value === undefined && callback === undefined; var err; try { - fn = new Function('next', 'value', 'now', 'var model=value;var global,require,process,GLOBAL,root,clearImmediate,clearInterval,clearTimeout,setImmediate,setInterval,setTimeout,console,$STRING,$VIEWCACHE,framework_internal,TransformBuilder,Pagination,Page,URLBuilder,UrlBuilder,SchemaBuilder,framework_builders,framework_utils,framework_mail,Image,framework_image,framework_nosql,Builders,U,utils,Utils,Mail,WTF,SOURCE,INCLUDE,MODULE,NOSQL,NOBIN,NOCOUNTER,NOSQLMEMORY,NOMEM,DATABASE,DB,CONFIG,INSTALL,UNINSTALL,RESOURCE,TRANSLATOR,LOG,LOGGER,MODEL,GETSCHEMA,CREATE,UID,TRANSFORM,MAKE,SINGLETON,NEWTRANSFORM,NEWSCHEMA,EACHSCHEMA,FUNCTION,ROUTING,SCHEDULE,OBSOLETE,DEBUG,TEST,RELEASE,is_client,is_server,F,framework,Controller,setTimeout2,clearTimeout2,String,Number,Boolean,Object,Function,Date,isomorphic,I,eval;UPTODATE,NEWOPERATION,OPERATION,$$$,EMIT,ON,$QUERY,$GET,$WORKFLOW,$TRANSFORM,$OPERATION,$MAKE,$CREATE,HttpFile;EMPTYCONTROLLER,ROUTE,FILE,TEST,WEBSOCKET,MAIL,LOGMAIL;try{' + body + ';\n}catch(e){next(e)}'); + fn = new Function('next', 'value', 'now', 'var model=value;var global,require,process,GLOBAL,root,clearImmediate,clearInterval,clearTimeout,setImmediate,setInterval,setTimeout,console,$STRING,$VIEWCACHE,framework_internal,TransformBuilder,Pagination,Page,URLBuilder,UrlBuilder,SchemaBuilder,framework_builders,framework_utils,framework_mail,Image,framework_image,framework_nosql,Builders,U,utils,Utils,Mail,WTF,SOURCE,INCLUDE,MODULE,NOSQL,NOBIN,NOCOUNTER,NOSQLMEMORY,NOMEM,DATABASE,DB,CONFIG,INSTALL,UNINSTALL,RESOURCE,TRANSLATOR,LOG,LOGGER,MODEL,GETSCHEMA,CREATE,UID,TRANSFORM,MAKE,SINGLETON,NEWTRANSFORM,NEWSCHEMA,EACHSCHEMA,FUNCTION,ROUTING,SCHEDULE,OBSOLETE,DEBUG,TEST,RELEASE,is_client,is_server,F,framework,Controller,setTimeout2,clearTimeout2,String,Number,Boolean,Object,Function,Date,isomorphic,I,eval;UPTODATE,NEWOPERATION,OPERATION,$$$,EMIT,ON,$QUERY,$GET,$WORKFLOW,$TRANSFORM,$OPERATION,$MAKE,$CREATE,HttpFile;EMPTYCONTROLLER,ROUTE,FILE,TEST,WEBSOCKET,MAIL,LOGMAIL,FUNC,REPO,FILESTORAGE;try{' + body + ';\n}catch(e){next(e)}'); } catch(e) { err = e; } @@ -1146,48 +1765,113 @@ function scriptNow() { return new Date(); } -F.database = function(name) { - return F.nosql(name); +function nosqlwrapper(name) { + var db = F.databases[name]; + if (db) + return db; + + // absolute + if (name[0] === '~') { + db = framework_nosql.load(U.getName(name), name.substring(1), true); + } else { + var is = name.substring(0, 6); + if (is === 'http:/' || is === 'https:') + db = framework_nosql.load(U.getName(name), name); + else { + F.path.verify('databases'); + db = framework_nosql.load(name, F.path.databases(name)); + } + } + + F.databases[name] = db; + return db; +} + +F.database = global.NOSQL = F.nosql = function(name) { + if (!global.framework_nosql) + global.framework_nosql = require('./nosql'); + // Someone rewrites F.database + if (F.database !== F.nosql) + global.NOSQL = F.nosql = nosqlwrapper; + else + F.database = nosqlwrapper; + + return nosqlwrapper(name); }; -global.NOSQL = F.nosql = function(name) { - var db = F.databases[name]; +function tablewrapper(name) { + var db = F.databases['$' + name]; if (db) return db; - var is = name.substring(0, 6); - if (is === 'http:/' || is === 'https:') - db = framework_nosql.load(U.getName(name), name); - else { + if (name[0] === '~') { + db = framework_nosql.load(U.getName(name), name.substring(1), true); + } else { F.path.verify('databases'); - db = framework_nosql.load(name, F.path.databases(name)); + db = framework_nosql.table(name, F.path.databases(name)); } - F.databases[name] = db; + F.databases['$' + name] = db; return db; +} + +global.TABLE = function(name) { + if (!global.framework_nosql) + global.framework_nosql = require('./nosql'); + global.TABLE = tablewrapper; + return tablewrapper(name); }; F.stop = F.kill = function(signal) { + if (F.isKilled) + return F; + + F.isKilled = true; + + if (!signal) + signal = 'SIGTERM'; + for (var m in F.workers) { var worker = F.workers[m]; - TRY(() => worker && worker.kill && worker.kill(signal || 'SIGTERM')); + TRY(() => worker && worker.kill && worker.kill(signal)); } - F.emit('exit', signal); + global.framework_nosql && global.framework_nosql.kill(signal); - if (!F.isWorker && process.send) + EMIT('exit', signal); + + if (!F.isWorker && process.send && process.connected) TRY(() => process.send('total:stop')); F.cache.stop(); - F.server && F.server.close && F.server.close(); - setTimeout(() => process.exit(signal || 'SIGTERM'), TEST ? 2000 : 100); + if (F.server) { + F.server.setTimeout(1); + F.server.close(); + } + + // var extenddelay = F.grapdbinstance && require('./graphdb').getImportantOperations() > 0; + // setTimeout(() => process.exit(signal), global.TEST || extenddelay ? 2000 : 300); + setTimeout(() => process.exit(1), global.TEST ? 2000 : 300); return F; }; -F.redirect = function(host, newHost, withPath, permanent) { +global.PROXY = F.proxy = function(url, target, copypath, before, after) { + + if (typeof(copypath) == 'function') { + after = before; + before = copypath; + copypath = false; + } + + var obj = { url: url, uri: require('url').parse(target), before: before, after: after, copypath: copypath }; + F.routes.proxies.push(obj); + F._request_check_proxy = true; +}; + +global.REDIRECT = F.redirect = function(host, newHost, withPath, permanent) { var external = host.startsWith('http://') || host.startsWith('https'); if (external) { @@ -1253,7 +1937,7 @@ F.redirect = function(host, newHost, withPath, permanent) { * @param {Function} fn * @return {Framework} */ -F.schedule = function(date, repeat, fn) { +global.SCHEDULE = F.schedule = function(date, repeat, fn) { if (fn === undefined) { fn = repeat; @@ -1263,20 +1947,20 @@ F.schedule = function(date, repeat, fn) { var type = typeof(date); if (type === 'string') { - date = date.parseDate(); - repeat && date < F.datetime && (date = F.datetime.add(repeat)); + date = date.parseDate().toUTC(); + repeat && date < NOW && (date = date.add(repeat)); } else if (type === 'number') date = new Date(date); var sum = date.getTime(); repeat && (repeat = repeat.replace('each', '1')); var id = U.GUID(5); - F.schedules.push({ expire: sum, fn: fn, repeat: repeat, owner: _owner, id: id }); + F.schedules[id] = { expire: sum, fn: fn, repeat: repeat, owner: _owner }; return id; }; F.clearSchedule = function(id) { - F.schedules = F.schedules.remove('id', id); + delete F.schedules[id]; return F; }; @@ -1287,7 +1971,7 @@ F.clearSchedule = function(id) { * @param {String Array} flags Optional, can contains extensions `.jpg`, `.gif' or watching path `/img/gallery/` * @return {Framework} */ -F.resize = function(url, fn, flags) { +global.RESIZE = F.resize = function(url, fn, flags) { var extensions = {}; var cache = true; @@ -1298,7 +1982,7 @@ F.resize = function(url, fn, flags) { fn = tmp; } - var ext = url.match(/\*.\*$|\*?\.(jpg|png|gif|jpeg)$/gi); + var ext = url.match(/\*.\*$|\*?\.(jpg|png|gif|jpeg|heif|heic|apng)$/gi); if (ext) { url = url.replace(ext, ''); switch (ext.toString().toLowerCase()) { @@ -1308,6 +1992,9 @@ F.resize = function(url, fn, flags) { case '*.jpg': case '*.gif': case '*.png': + case '*.heif': + case '*.heic': + case '*.apng': case '*.jpeg': extensions[ext.toString().toLowerCase().replace(/\*/g, '').substring(1)] = true; break; @@ -1333,6 +2020,9 @@ F.resize = function(url, fn, flags) { extensions['jpeg'] = true; extensions['png'] = true; extensions['gif'] = true; + extensions['heic'] = true; + extensions['heif'] = true; + extensions['apng'] = true; } if (extensions['jpg'] && !extensions['jpeg']) @@ -1357,6 +2047,8 @@ F.resize = function(url, fn, flags) { */ F.restful = function(url, flags, onQuery, onGet, onSave, onDelete) { + OBSOLETE('F.restful()', 'This method will be removed in v4.'); + var tmp; var index = flags ? flags.indexOf('cors') : -1; var cors = {}; @@ -1407,6 +2099,8 @@ F.restful = function(url, flags, onQuery, onGet, onSave, onDelete) { // This version of RESTful doesn't create advanced routing for insert/update/delete and all URL address of all operations are without "{id}" param because they expect some identificator in request body F.restful2 = function(url, flags, onQuery, onGet, onSave, onDelete) { + OBSOLETE('F.restful2()', 'This method will be removed in v4.'); + var tmp; var index = flags ? flags.indexOf('cors') : -1; var cors = {}; @@ -1459,8 +2153,19 @@ F.restful2 = function(url, flags, onQuery, onGet, onSave, onDelete) { */ global.CORS = F.cors = function(url, flags, credentials) { + if (!arguments.length) { + F.routes.corsall = true; + PERF.OPTIONS = true; + return F; + } + + if (flags === true) { + credentials = true; + flags = null; + } + var route = {}; - var origins = []; + var origin = []; var methods = []; var headers = []; var age; @@ -1483,8 +2188,13 @@ global.CORS = F.cors = function(url, flags, credentials) { continue; } + if (flag.substring(0, 2) === '//') { + origin.push(flag.substring(2)); + continue; + } + if (flag.startsWith('http://') || flag.startsWith('https://')) { - origins.push(flag); + origin.push(flag.substring(flag.indexOf('/') + 2)); continue; } @@ -1510,24 +2220,65 @@ global.CORS = F.cors = function(url, flags, credentials) { } } + if (!methods.length) + methods = 'POST,PUT,GET,DELETE,PATCH,GET,HEAD'.split(','); + + if (!origin.length && CONF.default_cors) + origin = CONF.default_cors; + route.isWILDCARD = url.lastIndexOf('*') !== -1; + var index = url.indexOf('{'); + if (index !== -1) { + route.isWILDCARD = true; + url = url.substring(0, index); + } + if (route.isWILDCARD) url = url.replace('*', ''); - url = framework_internal.preparePath(framework_internal.encodeUnicodeURL(url.trim())); + if (url[url.length - 1] !== '/') + url += '/'; + url = framework_internal.preparePath(framework_internal.encodeUnicodeURL(url.trim())); route.hash = url.hash(); route.owner = _owner; route.url = framework_internal.routeSplitCreate(url); - route.origins = origins.length ? origins : null; + route.origin = origin.length ? origin : null; route.methods = methods.length ? methods : null; route.headers = headers.length ? headers : null; route.credentials = credentials; - route.age = age || F.config['default-cors-maxage']; - route.id = id; + route.age = age || CONF.default_cors_maxage; + + var e = F.routes.cors.findItem(function(item) { + return item.hash === route.hash; + }); + + if (e) { + + // Extends existing + if (route.origin && e.origin) + corsextend(route.origin, e.origin); + else if (e.origin && !route.origin) + e.origin = null; + + if (route.methods && e.methods) + corsextend(route.methods, e.methods); + + if (route.headers && e.headers) + corsextend(route.headers, e.headers); + + if (route.credentials && !e.credentials) + e.credentials = true; + + if (route.isWILDCARD && !e.isWILDCARD) + e.isWILDCARD = true; + + } else { + F.routes.cors.push(route); + route.id = id; + } - F.routes.cors.push(route); F._length_cors = F.routes.cors.length; F.routes.cors.sort(function(a, b) { @@ -1540,24 +2291,50 @@ global.CORS = F.cors = function(url, flags, credentials) { return F; }; -F.group = function(flags, fn) { - _flags = flags; - fn.call(this); +function corsextend(a, b) { + for (var i = 0; i < a.length; i++) + b.indexOf(a[i]) === -1 && b.push(a[i]); +} + +global.GROUP = F.group = function() { + + var fn = null; + + _flags = null; + _prefix = null; + + for (var i = 0; i < arguments.length; i++) { + var o = arguments[i]; + + if (o instanceof Array) { + _flags = o; + continue; + } + + switch (typeof(o)) { + case 'string': + if (o.indexOf('/') === -1) { + // flags + _flags = o.split(',').trim(); + } else { + if (o.endsWith('/')) + o = o.substring(0, o.length - 1); + _prefix = o; + } + break; + case 'function': + fn = o; + break; + } + } + + fn && fn.call(F); + _prefix = undefined; _flags = undefined; - return this; + return F; }; -/** - * Add a route - * @param {String} url - * @param {Function} funcExecute Action. - * @param {String Array} flags - * @param {Number} length Maximum length of request data. - * @param {String Array} middleware Loads custom middleware. - * @param {Number} timeout Response timeout. - * @return {Framework} - */ -F.web = F.route = function(url, funcExecute, flags, length, language) { +global.ROUTE = F.web = F.route = function(url, funcExecute, flags, length, language) { var name; var tmp; @@ -1569,10 +2346,56 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { return F; } + if (typeof(flags) === 'number') { + length = flags; + flags = null; + } + + var type = typeof(funcExecute); + + if (funcExecute instanceof Array) { + tmp = funcExecute; + funcExecute = flags; + flags = tmp; + } + + var search = (typeof(url) === 'string' ? url.toLowerCase().replace(/\s{2,}/g, ' ') : '') + (flags ? (' ' + flags.where(n => typeof(n) === 'string' && n.substring(0, 2) !== '//' && n[2] !== ':').join(' ')).toLowerCase() : ''); + var method = ''; var CUSTOM = typeof(url) === 'function' ? url : null; if (CUSTOM) url = '/'; + if (url) { + + url = url.replace(/\t/g, ' ').trim(); + + var first = url.substring(0, 1); + if (first === '+' || first === '-' || url.substring(0, 2) === '🔒') { + // auth/unauth + url = url.replace(/^(\+|-|🔒)+/g, '').trim(); + !flags && (flags = []); + flags.push(first === '-' ? 'unauthorized' : 'authorized'); + } + + url = url.replace(/(^|\s?)\*([{}a-z0-9}]|\s).*?$/i, function(text) { + !flags && (flags = []); + flags.push(text.trim()); + return ''; + }).trim(); + + var index = url.indexOf(' '); + if (index !== -1) { + method = url.substring(0, index).toLowerCase().trim(); + url = url.substring(index + 1).trim(); + } + + if (method.indexOf(',') !== -1) { + !flags && (flags = []); + method.split(',').forEach(m => flags.push(m.trim())); + method = ''; + } + } + if (url[0] === '#') { url = url.substring(1); if (url !== '400' && url !== '401' && url !== '403' && url !== '404' && url !== '408' && url !== '409' && url !== '431' && url !== '500' && url !== '501') { @@ -1585,7 +2408,8 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { if (index !== -1) { tmp = url.substring(index); url = url.substring(0, index); - } + } else + tmp = ''; sitemap = F.sitemap(url, true, language); @@ -1620,22 +2444,23 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { if (url[0] !== '[' && url[0] !== '/') url = '/' + url; + if (_prefix) + url = _prefix + url; + if (url.endsWith('/')) url = url.substring(0, url.length - 1); url = framework_internal.encodeUnicodeURL(url); - var type = typeof(funcExecute); - var index = 0; var urlcache = url; if (!name) name = url; - if (type === 'object' || funcExecute instanceof Array) { - tmp = funcExecute; - funcExecute = flags; - flags = tmp; + if (method) { + !flags && (flags = []); + flags.push(method); + method = ''; } var priority = 0; @@ -1660,7 +2485,6 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { var isRaw = false; var isNOXHR = false; - var method = ''; var schema; var workflow; var isMOBILE = false; @@ -1677,15 +2501,14 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { var corsflags = []; var membertype = 0; var isGENERATOR = false; + var isDYNAMICSCHEMA = false; var description; var id = null; + var groups = []; if (_flags) { - if (!flags) - flags = []; - _flags.forEach(function(flag) { - flags.indexOf(flag) === -1 && flags.push(flag); - }); + !flags && (flags = []); + _flags.forEach(flag => flags.indexOf(flag) === -1 && flags.push(flag)); } if (flags) { @@ -1707,11 +2530,11 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { continue; } - var first = flags[i][0]; + flags[i] = flags[i].replace(/\t/g, ' '); + var first = flags[i][0]; if (first === '&') { - // resource (sitemap localization) - // isn't used now + groups.push(flags[i].substring(1).trim()); continue; } @@ -1723,7 +2546,7 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { if (first === '#') { !middleware && (middleware = []); - middleware.push(flags[i].substring(1)); + middleware.push(flags[i].substring(1).trim()); continue; } @@ -1739,20 +2562,36 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { schema = workflow; workflow = null; } - schema = schema.replace(/\\/g, '/').split('/'); - if (schema.length === 1) { - schema[1] = schema[0]; - schema[0] = 'default'; - } + schema = schema.replace(/\\/g, '/').split('/').trim(); - index = schema[1].indexOf('#'); - if (index !== -1) { - schema[2] = schema[1].substring(index + 1).trim(); - schema[1] = schema[1].substring(0, index).trim(); - (schema[2] && schema[2][0] !== '*') && (schema[2] = '*' + schema[2]); - } + if (schema.length) { + + if (schema.length === 1) { + schema[1] = schema[0]; + schema[0] = 'default'; + } + + // Is dynamic schema? + if (schema[0][0] === '{') { + isDYNAMICSCHEMA = true; + schema[0] = schema[0].substring(1).trim(); + schema[1] = schema[1].substring(0, schema[1].length - 1).trim(); + } + + if (schema[1][0] === '{') { + isDYNAMICSCHEMA = true; + schema[1] = schema[1].substring(1, schema[1].length - 1).trim(); + } + + index = schema[1].indexOf('#'); + if (index !== -1) { + schema[2] = schema[1].substring(index + 1).trim(); + schema[1] = schema[1].substring(0, index).trim(); + (schema[2] && schema[2][0] !== '*') && (schema[2] = '*' + schema[2]); + } + } // else it's an operation continue; } @@ -1852,6 +2691,8 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { tmp.push(flag); method += (method ? ',' : '') + flag; corsflags.push(flag); + PERF[flag.toUpperCase()] = true; + PERF[flag] = true; break; default: if (flag[0] === '@') @@ -1859,7 +2700,19 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { tmp.push(flag); break; } + + if (flag === 'get') + priority -= 2; + + } + + if (isROLE && !membertype) { + tmp.push('authorize'); + priority += 2; + membertype = 1; + count++; } + flags = tmp; priority += (count * 2); } else { @@ -1867,13 +2720,25 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { method = 'get'; } + if (workflow && workflow[0] === '@') { + var tmpa = workflow.replace(/,/g, ' ').split('@').trim(); + var rindex = null; + for (var i = 0; i < tmpa.length; i++) { + var a = tmpa[i].split(' '); + if (a[1] && (/response|res/i).test(a[1])) + rindex = i; + tmpa[i] = a[0]; + } + workflow = { id: tmpa.length > 1 ? tmpa : tmpa[0], index: rindex }; + } + if (type === 'string') { viewname = funcExecute; - funcExecute = (function(name, sitemap, language) { + funcExecute = (function(name, sitemap, language, workflow) { var themeName = U.parseTheme(name); if (themeName) name = prepare_viewname(name); - return function() { + return function(id) { if (language && !this.language) this.language = language; sitemap && this.sitemap(sitemap.id, language); @@ -1883,19 +2748,27 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { this.themeName = themeName; if (!this.route.workflow) return this.view(name); + var self = this; - this.$exec(this.route.workflow, null, function(err, response) { - if (err) - self.content(err); + if (this.route.workflow instanceof Object) { + workflow.view = name; + if (workflow.id instanceof Array) + controller_json_workflow_multiple.call(self, id); else - self.view(name, response); - }); + controller_json_workflow.call(self, id); + } else { + this.$exec(this.route.workflow, null, function(err, response) { + if (err) + self.content(err); + else + self.view(name, response); + }); + } }; - })(viewname, sitemap, language); + })(viewname, sitemap, language, workflow); } else if (typeof(funcExecute) !== 'function') { viewname = (sitemap && sitemap.url !== '/' ? sitemap.id : workflow ? '' : url) || ''; - if (!workflow || (!viewname && !workflow)) { if (viewname.endsWith('/')) viewname = viewname.substring(0, viewname.length - 1); @@ -1908,24 +2781,38 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { viewname = 'index'; funcExecute = (function(name, sitemap, language) { - return function() { - if (language && !this.language) - this.language = language; - sitemap && this.sitemap(sitemap.id, language); - name[0] === '~' && this.theme(''); - if (!this.route.workflow) - return this.view(name); + return function(id) { var self = this; - this.$exec(this.route.workflow, null, function(err, response) { - if (err) - self.content(err); + + if (language && !self.language) + self.language = language; + + sitemap && self.sitemap(sitemap.id, language); + + if (name[0] === '~') + self.themeName = ''; + + if (!self.route.workflow) + return self.view(name); + + if (self.route.workflow instanceof Object) { + workflow.view = name; + if (workflow.id instanceof Array) + controller_json_workflow_multiple.call(self, id); else - self.view(name, response); - }); + controller_json_workflow.call(self, id); + } else { + self.$exec(self.route.workflow, null, function(err, response) { + if (err) + self.content(err); + else + self.view(name, response); + }); + } }; })(viewname, sitemap, language); } else if (workflow) - funcExecute = controller_json_workflow; + funcExecute = workflow.id instanceof Array ? controller_json_workflow_multiple : controller_json_workflow; } if (!isGENERATOR) @@ -1939,6 +2826,7 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { var params = []; var reg = null; var regIndex = null; + var dynamicidindex = -1; if (url.indexOf('{') !== -1) { routeURL.forEach(function(o, i) { @@ -1950,6 +2838,9 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { var sub = o.substring(1, o.length - 1); var name = o.substring(1, o.length - 1).trim(); + if (name === 'id') + dynamicidindex = i; + params.push(name); if (sub[0] !== '/') @@ -1969,17 +2860,12 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { regIndex.push(i); }); - priority -= arr.length; + priority -= arr.length + 1; } if (url.indexOf('#') !== -1) priority -= 100; - if (flags.indexOf('proxy') !== -1) { - isJSON = true; - priority++; - } - if ((isJSON || flags.indexOf('xml') !== -1 || isRaw) && (flags.indexOf('delete') === -1 && flags.indexOf('post') === -1 && flags.indexOf('put') === -1) && flags.indexOf('patch') === -1) { flags.push('post'); method += (method ? ',' : '') + 'post'; @@ -1998,7 +2884,7 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { method += (method ? ',' : '') + 'get'; } - if (F.config['allow-head'] && flags.indexOf('get') !== -1) { + if (CONF.allow_head && flags.indexOf('get') !== -1) { flags.append('head'); method += (method ? ',' : '') + 'head'; } @@ -2027,14 +2913,25 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { console.warn('F.route() skips "binary" flag because the "raw" flag is not defined.'); } + if (workflow && workflow.id) { + workflow.meta = {}; + if (workflow.id instanceof Array) { + for (var i = 0; i < workflow.id.length; i++) + workflow.meta[workflow.id[i]] = i + 1; + } else + workflow.meta[workflow.id] = 1; + } + if (subdomain) F._length_subdomain_web++; var instance = new FrameworkRoute(); var r = instance.route; r.hash = hash; + r.search = search.split(' '); r.id = id; - r.name = name; + r.name = name.trim(); + r.groups = flags_to_object(groups); r.priority = priority; r.sitemap = sitemap ? sitemap.id : ''; r.schema = schema; @@ -2047,14 +2944,15 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { r.urlraw = urlraw; r.url = routeURL; r.param = arr; + r.paramidindex = isDYNAMICSCHEMA ? dynamicidindex : -1; r.paramnames = params.length ? params : null; r.flags = flags || EMPTYARRAY; r.flags2 = flags_to_object(flags); r.method = method; r.execute = funcExecute; - r.length = (length || F.config['default-request-length']) * 1024; + r.length = (length || CONF.default_request_maxlength) * 1024; r.middleware = middleware; - r.timeout = timeout === undefined ? (isDELAY ? 0 : F.config['default-request-timeout']) : timeout; + r.timeout = timeout === undefined ? (isDELAY ? 0 : CONF.default_request_timeout) : timeout; r.isGET = flags.indexOf('get') !== -1; r.isMULTIPLE = isMULTIPLE; r.isJSON = isJSON; @@ -2073,7 +2971,6 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { r.isHTTP = flags.indexOf('http') !== -1; r.isDEBUG = flags.indexOf('debug') !== -1; r.isRELEASE = flags.indexOf('release') !== -1; - r.isPROXY = flags.indexOf('proxy') !== -1; r.isBOTH = isNOXHR ? false : true; r.isXHR = flags.indexOf('xhr') !== -1; r.isUPLOAD = flags.indexOf('upload') !== -1; @@ -2081,11 +2978,13 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { r.isCACHE = !url.startsWith('/#') && !CUSTOM && !arr.length && !isWILDCARD; r.isPARAM = arr.length > 0; r.isDELAY = isDELAY; + r.isDYNAMICSCHEMA = isDYNAMICSCHEMA; r.CUSTOM = CUSTOM; r.options = options; r.regexp = reg; r.regexpIndexer = regIndex; r.type = 'web'; + r.remove = remove_route_web; if (r.isUPLOAD) PERF.upload = true; @@ -2116,39 +3015,75 @@ F.web = F.route = function(url, funcExecute, flags, length, language) { !_controller && F.$routesSort(1); } - F.emit('route', 'web', instance); + if (isMOBILE) + F._request_check_mobile = true; + + EMIT('route', 'web', instance); return instance; }; function flags_to_object(flags) { var obj = {}; - flags.forEach(function(flag) { - obj[flag] = true; - }); + flags.forEach(flag => obj[flag] = true); return obj; } -F.mmr = function(url, process) { - url = framework_internal.preparePath(U.path(url)); - F.routes.mmr[url] = { exec: process }; - F._request_check_POST = true; - return F; -}; +function remove_route_web() { + + if (this.isSYSTEM) { + var keys = Object.keys(F.routes.system); + for (var i = 0; i < keys.length; i++) { + if (F.routes.system[keys[i]] === this) { + delete F.routes.system[keys]; + F.temporary.other = {}; + return; + } + } + } + + var index = F.routes.web.indexOf(this); + if (index !== -1) { + F.routes.web.splice(index, 1); + F.$routesSort(); + F.temporary.other = {}; + } +} /** * Get routing by name * @param {String} name * @return {Object} */ -F.routing = function(name) { +global.ROUTING = F.routing = function(name, flags) { + + var id = name.substring(0, 3) === 'id:' ? name.substring(3) : null; + if (id) + name = null; + + var search = id ? null : (name.toLowerCase().replace(/\s{2,}/g, ' ') + (flags ? (' ' + flags.where(n => typeof(n) === 'string' && n.substring(0, 2) !== '//' && n[2] !== ':').join(' ')).toLowerCase() : '')).split(' '); + for (var i = 0, length = F.routes.web.length; i < length; i++) { var route = F.routes.web[i]; - if (route.name === name) { - var url = U.path(route.url.join('/')); - if (url[0] !== '/') - url = '/' + url; - return { controller: route.controller, url: url, id: route.id, flags: route.flags, middleware: route.middleware, execute: route.execute, timeout: route.timeout, options: route.options, length: route.length }; + var is = true; + if (id && route.id !== id) + is = false; + else if (search) { + for (var j = 0; j < search.length; j++) { + if (route.search.indexOf(search[j]) === -1) { + is = false; + break; + } + } } + + if (!is) + continue; + + var url = U.path(route.url.join('/')); + if (url[0] !== '/') + url = '/' + url; + + return route; } }; @@ -2163,9 +3098,22 @@ F.routing = function(name) { */ global.MERGE = F.merge = function(url) { + F.temporary.other['merge_' + url] = 1; + if (url[0] === '#') url = sitemapurl(url.substring(1)); + url = F.$version(framework_internal.preparePath(url)); + + if (url === 'auto') { + // auto-generating + var arg = arguments; + setTimeout(function(arg) { + F.merge.apply(F, arg); + }, 500, arg); + return F; + } + var arr = []; for (var i = 1, length = arguments.length; i < length; i++) { @@ -2187,15 +3135,15 @@ global.MERGE = F.merge = function(url) { } } - url = framework_internal.preparePath(F.$version(url)); - if (url[0] !== '/') url = '/' + url; - var filename = F.path.temp((F.id ? 'i-' + F.id + '_' : '') + 'merged_' + createTemporaryKey(url)); + var key = createTemporaryKey(url); + var filename = F.path.temp((F.id ? 'i-' + F.id + '_' : '') + 'merged_' + key); F.routes.merge[url] = { filename: filename.replace(/\.(js|css)$/g, ext => '.min' + ext), files: arr }; Fs.unlink(F.routes.merge[url].filename, NOOP); F.owners.push({ type: 'merge', owner: _owner, id: url }); + delete F.temporary.notfound[key]; return F; }; @@ -2258,7 +3206,7 @@ global.MAP = F.map = function(url, filename, filter) { isPackage = true; } else if (c === '=') { if (F.isWindows) - filename = U.combine(F.config['directory-themes'], filename.substring(1)); + filename = U.combine(CONF.directory_themes, filename.substring(1)); else filename = F.path.themes(filename.substring(1)); isPackage = true; @@ -2351,7 +3299,7 @@ global.MAP = F.map = function(url, filename, filter) { }); }, isPackage ? 500 : 1); - return this; + return F; }; /** @@ -2375,6 +3323,13 @@ global.MIDDLEWARE = F.middleware = function(name, funcExecute) { * @return {Framework} */ F.use = function(name, url, types, first) { + + if (typeof(name) === 'function') { + var tmp = 'mid' + GUID(5); + MIDDLEWARE(tmp, name); + name = tmp; + } + if (!url && !types) { if (name instanceof Array) { for (var i = 0; i < name.length; i++) @@ -2461,7 +3416,7 @@ function merge_middleware(a, b, first) { * @param {Object} options Optional, additional options for middleware. * @return {Framework} */ -F.websocket = function(url, funcInitialize, flags, length) { +global.WEBSOCKET = F.websocket = function(url, funcInitialize, flags, length) { var tmp; @@ -2489,6 +3444,18 @@ F.websocket = function(url, funcInitialize, flags, length) { throw new Error('Sitemap item "' + url + '" not found.'); } + var first = url.substring(0, 1); + if (first === '+' || first === '-' || url.substring(0, 2) === '🔒') { + // auth/unauth + url = url.replace(/^(\+|-|🔒)+/g, '').trim(); + !flags && (flags = []); + flags.push(first === '-' ? 'unauthorized' : 'authorized'); + } + + var index = url.substring(0, 7).indexOf(' '); + if (index !== -1) + url = url.substring(index + 1).trim(); + if (url === '') url = '/'; @@ -2503,6 +3470,7 @@ F.websocket = function(url, funcInitialize, flags, length) { var options; var protocols; var id; + var groups = []; priority = url.count('/'); @@ -2569,16 +3537,12 @@ F.websocket = function(url, funcInitialize, flags, length) { var isJSON = false; var isBINARY = false; var isROLE = false; + var isBUFFER = false; var count = 0; var membertype = 0; - if (!flags) - flags = []; - - _flags && _flags.forEach(function(flag) { - if (flags.indexOf(flag) === -1) - flags.push(flag); - }); + !flags && (flags = []); + _flags && _flags.forEach(flag => flags.indexOf(flag) === -1 && flags.push(flag)); for (var i = 0; i < flags.length; i++) { @@ -2587,7 +3551,7 @@ F.websocket = function(url, funcInitialize, flags, length) { // Middleware options if (type === 'object') { - options = flags[i]; + options = flag; continue; } @@ -2602,16 +3566,22 @@ F.websocket = function(url, funcInitialize, flags, length) { continue; } + // Groups + if (flag[0] === '&') { + groups.push(flag.substring(1).trim()); + continue; + } + // Middleware if (flag[0] === '#') { !middleware && (middleware = []); - middleware.push(flags[i].substring(1)); + middleware.push(flag.substring(1).trim()); continue; } flag = flag.toString().toLowerCase(); - // Origins + // Origin if (flag.startsWith('http://') || flag.startsWith('https://')) { !allow && (allow = []); allow.push(flag); @@ -2631,6 +3601,9 @@ F.websocket = function(url, funcInitialize, flags, length) { isJSON = false; } + if (flag === 'buffer') + isBUFFER = true; + if (flag[0] === '@') { isROLE = true; tmp.push(flag); @@ -2663,13 +3636,19 @@ F.websocket = function(url, funcInitialize, flags, length) { tmp.push(flag); break; default: - if (!protocols) - protocols = []; + !protocols && (protocols = []); protocols.push(flag); break; } } + if (isROLE && !membertype) { + tmp.push('authorize'); + membertype = 1; + priority++; + count++; + } + flags = tmp; flags.indexOf('get') === -1 && flags.unshift('get'); @@ -2683,6 +3662,7 @@ F.websocket = function(url, funcInitialize, flags, length) { r.id = id; r.urlraw = urlraw; r.hash = hash; + r.groups = flags_to_object(groups); r.controller = _controller ? _controller : 'unknown'; r.owner = _owner; r.url = routeURL; @@ -2695,10 +3675,11 @@ F.websocket = function(url, funcInitialize, flags, length) { r.onInitialize = funcInitialize; r.protocols = protocols || EMPTYARRAY; r.allow = allow || []; - r.length = (length || F.config['default-websocket-request-length']) * 1024; + r.length = (length || CONF.default_websocket_maxlength) * 1024; r.isWEBSOCKET = true; r.MEMBER = membertype; r.isJSON = isJSON; + r.isBUFFER = isBUFFER; r.isBINARY = isBINARY; r.isROLE = isROLE; r.isWILDCARD = isWILDCARD; @@ -2715,14 +3696,14 @@ F.websocket = function(url, funcInitialize, flags, length) { r.type = 'websocket'; F.routes.websockets.push(r); F.initwebsocket && F.initwebsocket(); - F.emit('route', 'websocket', r); + EMIT('route', 'websocket', r); !_controller && F.$routesSort(2); return instance; }; F.initwebsocket = function() { - if (F.routes.websockets.length && F.config['allow-websocket'] && F.server) { - F.server.on('upgrade', F._upgrade); + if (F.routes.websockets.length && CONF.allow_websocket && F.server) { + F.server.on('upgrade', F.$upgrade); F.initwebsocket = null; } }; @@ -2735,7 +3716,7 @@ F.initwebsocket = function() { * @param {String Array} middleware * @return {Framework} */ -F.file = function(fnValidation, fnExecute, flags) { +global.FILE = F.file = function(fnValidation, fnExecute, flags) { var a; @@ -2764,38 +3745,29 @@ F.file = function(fnValidation, fnExecute, flags) { var fixedfile = false; var id = null; var urlraw = fnValidation; + var groups = []; if (_flags) { !flags && (flags = []); - _flags.forEach(function(flag) { - flags.indexOf(flag) === -1 && flags.push(flag); - }); + _flags.forEach(flag => flags.indexOf(flag) === -1 && flags.push(flag)); } if (flags) { for (var i = 0, length = flags.length; i < length; i++) { var flag = flags[i]; - - if (typeof(flag) === 'object') { + if (typeof(flag) === 'object') options = flag; - continue; - } - - if (flag.substring(0, 3) === 'id:') { - id = flag.substring(3).trim(); - continue; - } - - if (flag[0] === '#') { + else if (flag[0] === '&') + groups.push(flag.substring(1).trim()); + else if (flag[0] === '#') { !middleware && (middleware = []); - middleware.push(flag.substring(1)); - } - - if (flag[0] === '.') { - flag = flag.substring(1).toLowerCase(); + middleware.push(flag.substring(1).trim()); + } else if (flag[0] === '.') { + flag = flag.substring(1).toLowerCase().trim(); !extensions && (extensions = {}); extensions[flag] = true; - } + } else if (flag.substring(0, 3) === 'id:') + id = flag.substring(3).trim(); } } @@ -2832,6 +3804,7 @@ F.file = function(fnValidation, fnExecute, flags) { var r = instance.route; r.id = id; r.urlraw = urlraw; + r.groups = flags_to_object(groups); r.controller = _controller ? _controller : 'unknown'; r.owner = _owner; r.url = url; @@ -2846,11 +3819,15 @@ F.file = function(fnValidation, fnExecute, flags) { F.routes.files.push(r); F.routes.files.sort((a, b) => !a.url ? -1 : !b.url ? 1 : a.url.length > b.url.length ? -1 : 1); - F.emit('route', 'file', r); - F._length_files++; + EMIT('route', 'file', r); + F._length_files = F.routes.files.length; return F; }; +global.FILE404 = function(fn) { + F.routes.filesfallback = fn; +}; + function sitemapurl(url) { var index = url.indexOf('/'); @@ -2877,10 +3854,15 @@ function sitemapurl(url) { global.LOCALIZE = F.localize = function(url, flags, minify) { + if (typeof(url) === 'function') { + F.onLocale = url; + return; + } + if (url[0] === '#') url = sitemapurl(url.substring(1)); - url = url.replace('*', ''); + url = url.replace('*.*', ''); if (minify == null) minify = true; @@ -2892,75 +3874,96 @@ global.LOCALIZE = F.localize = function(url, flags, minify) { flags = []; var index; + var ext = false; flags = flags.remove(function(item) { item = item.toLowerCase(); if (item === 'nocompress') minify = false; + if (item[0] === '.') + ext = true; return item === 'compress' || item === 'nocompress' || item === 'minify'; }); var index = url.lastIndexOf('.'); - if (index === -1) - flags.push('.html', '.htm', '.md', '.txt'); - else { - flags.push(url.substring(index).toLowerCase()); - url = url.substring(0, index); + if (!ext) { + if (index === -1) + flags.push('.html', '.htm', '.md', '.txt'); + else { + flags.push(url.substring(index).toLowerCase()); + url = url.substring(0, index).replace('*', ''); + } } - url = framework_internal.preparePath(url); - F.file(url, function(req, res) { + url = framework_internal.preparePath(url.replace('.*', '')); + + if (minify) + F.file(url, F.$filelocalize, flags); + else + F.file(url, filelocalize_nominify, flags); +}; - F.onLocale && (req.$language = F.onLocale(req, res, req.isStaticFile)); +function filelocalize_nominify(req, res) { + F.$filelocalize(req, res, true); +} - var key = 'locate_' + (req.$language ? req.$language : 'default') + '_' + req.url; - var output = F.temporary.other[key]; +F.$filelocalize = function(req, res, nominify) { - if (output) { - if (!F.$notModified(req, res, output.$mtime)) { - HEADERS.responseLocalize['Last-Modified'] = output.$mtime; - res.options.body = output; - res.options.type = U.getContentType(req.extension); - res.$text(); - } - return; + // options.filename + // options.code + // options.callback + // options.headers + // options.download + + F.onLocale && (req.$language = F.onLocale(req, res, req.isStaticFile)); + + var key = 'locate_' + (req.$language ? req.$language : 'default') + '_' + (req.$key || req.url); + var output = F.temporary.other[key]; + + if (output) { + if (!F.$notModified(req, res, output.$mtime)) { + HEADERS.responseLocalize['Last-Modified'] = output.$mtime; + res.options.body = output; + res.options.type = U.getContentType(req.extension); + res.$text(); } + return; + } - var name = req.uri.pathname; - var filename = F.onMapping(name, name, true, true); + var filename = (res.options ? res.options.filename : null) || F.onMapping(req.uri.pathname, req.uri.pathname, true, true); - Fs.readFile(filename, function(err, content) { + Fs.readFile(filename, function(err, content) { - if (err) - return res.throw404(); + if (err) { + if (!F.routes.filesfallback || !F.routes.filesfallback(req, res)) + res.throw404(); + return; + } - content = F.translator(req.$language, framework_internal.modificators(content.toString(ENCODING), filename, 'static')); + content = framework_internal.markup(F.translator(req.$language, framework_internal.modificators(content.toString(ENCODING), filename, 'static')), filename); - Fs.lstat(filename, function(err, stats) { + Fs.lstat(filename, function(err, stats) { - var mtime = stats.mtime.toUTCString(); + var mtime = stats.mtime.toUTCString(); - if (minify && (req.extension === 'html' || req.extension === 'htm')) - content = framework_internal.compile_html(content, filename); + if (CONF.allow_compile_html && CONF.allow_compile && !nominify && (req.extension === 'html' || req.extension === 'htm')) + content = framework_internal.compile_html(content, filename, true); - if (RELEASE) { - F.temporary.other[key] = U.createBuffer(content); - F.temporary.other[key].$mtime = mtime; - if (F.$notModified(req, res, mtime)) - return; - } + if (RELEASE) { + F.temporary.other[key] = Buffer.from(content); + F.temporary.other[key].$mtime = mtime; + if (F.$notModified(req, res, mtime)) + return; + } - HEADERS.responseLocalize['Last-Modified'] = mtime; - res.options.body = content; - res.options.type = U.getContentType(req.extension); - res.options.headers = HEADERS.responseLocalize; - res.$text(); - }); + HEADERS.responseLocalize['Last-Modified'] = mtime; + res.options.body = content; + res.options.type = U.getContentType(req.extension); + res.options.headers = HEADERS.responseLocalize; + res.$text(); }); - - }, flags); - return F; + }); }; F.$notModified = function(req, res, date) { @@ -2984,18 +3987,16 @@ F.$notModified = function(req, res, date) { */ F.error = function(err, name, uri) { - if (!arguments.length) { - return function(err) { - err && F.error(err, name, uri); - }; - } + if (!arguments.length) + return F.errorcallback; if (!err) return F; if (F.errors) { - F.datetime = new Date(); - F.errors.push({ error: err.stack, name: name, url: uri ? typeof(uri) === 'string' ? uri : Parser.format(uri) : undefined, date: F.datetime }); + F.stats.error++; + NOW = new Date(); + F.errors.push({ error: err.stack ? err.stack : err, name: name, url: uri ? typeof(uri) === 'string' ? uri : Parser.format(uri) : undefined, date: NOW }); F.errors.length > 50 && F.errors.shift(); } @@ -3003,6 +4004,10 @@ F.error = function(err, name, uri) { return F; }; +F.errorcallback = function(err) { + err && F.error(err); +}; + /** * Registers a new problem * @param {String} message @@ -3012,7 +4017,10 @@ F.error = function(err, name, uri) { * @return {Framework} */ F.problem = F.wtf = function(message, name, uri, ip) { - F.$events.problem && F.emit('problem', message, name, uri, ip); + + // OBSOLETE('F.problem()', 'This method will be removed in v4'); + + F.$events.problem && EMIT('problem', message, name, uri, ip); if (message instanceof framework_builders.ErrorBuilder) message = message.plain(); @@ -3030,6 +4038,10 @@ F.problem = F.wtf = function(message, name, uri, ip) { return F; }; +global.PRINTLN = function(msg) { + console.log('------>', '[' + new Date().format('yyyy-MM-dd HH:mm:ss') + ']', msg); +}; + /** * Registers a new change * @param {String} message @@ -3039,7 +4051,10 @@ F.problem = F.wtf = function(message, name, uri, ip) { * @return {Framework} */ F.change = function(message, name, uri, ip) { - F.$events.change && F.emit('change', message, name, uri, ip); + + OBSOLETE('F.change()', 'This method will be removed in v4.'); + + F.$events.change && EMIT('change', message, name, uri, ip); if (message instanceof framework_builders.ErrorBuilder) message = message.plain(); @@ -3065,23 +4080,25 @@ F.change = function(message, name, uri, ip) { * @param {String} ip * @return {Framework} */ -F.trace = function(message, name, uri, ip) { +global.TRACE = F.trace = function(message, name, uri, ip) { + + OBSOLETE('TRACE()', 'This method will be removed in v4.'); - if (!F.config.trace) + if (!CONF.trace) return F; - F.$events.trace && F.emit('trace', message, name, uri, ip); + F.$events.trace && EMIT('trace', message, name, uri, ip); if (message instanceof framework_builders.ErrorBuilder) message = message.plain(); else if (typeof(message) === 'object') message = JSON.stringify(message); - F.datetime = new Date(); - var obj = { message: message, name: name, url: uri ? typeof(uri) === 'string' ? uri : Parser.format(uri) : undefined, ip: ip, date: F.datetime }; + NOW = new Date(); + var obj = { message: message, name: name, url: uri ? typeof(uri) === 'string' ? uri : Parser.format(uri) : undefined, ip: ip, date: NOW }; F.logger('traces', obj.message, 'url: ' + obj.url, 'source: ' + obj.name, 'ip: ' + obj.ip); - F.config['trace-console'] && console.log(F.datetime.format('yyyy-MM-dd HH:mm:ss'), '[trace]', message, '|', 'url: ' + obj.url, 'source: ' + obj.name, 'ip: ' + obj.ip); + CONF.trace_console && console.log(NOW.format('yyyy-MM-dd HH:mm:ss'), '[trace]', message, '|', 'url: ' + obj.url, 'source: ' + obj.name, 'ip: ' + obj.ip); if (F.traces) { F.traces.push(obj); @@ -3105,14 +4122,89 @@ global.MODULE = F.module = function(name) { * @param {Function(type, filename, content)} fn The `fn` must return modified value. * @return {Framework} */ -F.modify = function(fn) { - if (!F.modificators) - F.modificators = []; - F.modificators.push(fn); +global.MODIFY = F.modify = function(filename, fn) { + + if (typeof(filename) === 'function') { + fn = filename; + filename = null; + } + + if (filename) { + if (!F.modificators2) + F.modificators2 = {}; + if (F.modificators2[filename]) + F.modificators2[filename].push(fn); + else + F.modificators2[filename] = [fn]; + } else { + if (!F.modificators) + F.modificators = []; + F.modificators.push(fn); + } + fn.$owner = _owner; return F; }; +F.$bundle = function(callback) { + + var bundledir = F.path.root(CONF.directory_bundles); + + var makebundle = function() { + + var arr = Fs.readdirSync(bundledir); + var url = []; + + for (var i = 0; i < arr.length; i++) { + if (arr[i].endsWith('.url')) + url.push(arr[i]); + } + + url.wait(function(item, next) { + + var filename = F.path.root(CONF.directory_bundles) + item.replace('.url', '.bundle'); + var link = Fs.readFileSync(F.path.root(CONF.directory_bundles) + item).toString('utf8'); + + F.consoledebug('Download bundle: ' + link); + + U.download(link, FLAGS_INSTALL, function(err, response) { + + if (err) { + F.error(err, 'Bundle: ' + link); + next(); + return; + } + + var stream = Fs.createWriteStream(filename); + + response.pipe(stream); + response.on('error', function(err) { + F.error(err, 'Bundle: ' + link); + next(); + }); + + CLEANUP(stream, next); + }); + + }, function() { + require('./bundles').make(function() { + F.directory = HEADERS.workers.cwd = directory = F.path.root(CONF.directory_src); + callback(); + }); + }); + }; + + try { + Fs.statSync(bundledir); + if (F.$bundling) { + makebundle(); + return; + } else + F.directory = HEADERS.workers.cwd = directory = F.path.root(CONF.directory_src); + } catch(e) {} + callback(); +}; + F.$load = function(types, targetdirectory, callback, packageName) { var arr = []; @@ -3155,8 +4247,9 @@ F.$load = function(types, targetdirectory, callback, packageName) { var ext = U.getExtension(o); if (ext) ext = '.' + ext; - if (ext !== extension) + if (ext !== extension || o[0] === '.' || o.endsWith('-bk' + extension) || o.endsWith('_bk' + extension)) return; + var name = (level ? U.$normalize(directory).replace(dir, '') + '/' : '') + o.substring(0, o.length - ext.length); output.push({ name: name[0] === '/' ? name.substring(1) : name, filename: Path.join(dir, name) + extension }); }); @@ -3172,10 +4265,28 @@ F.$load = function(types, targetdirectory, callback, packageName) { var dependencies = []; var operations = []; var isPackage = targetdirectory.indexOf('.package') !== -1; + var isNo = true; + + if (types) { + for (var i = 0; i < types.length; i++) { + if (types[i].substring(0, 2) !== 'no') { + isNo = false; + break; + } + } + } + + var can = function(type) { + if (!types) + return true; + if (types.indexOf('no' + type) !== -1) + return false; + return isNo ? true : types.indexOf(type) !== -1; + }; - if (!types || types.indexOf('modules') !== -1) { + if (can('modules')) { operations.push(function(resume) { - dir = U.combine(targetdirectory, isPackage ? '/modules/' : F.config['directory-modules']); + dir = U.combine(targetdirectory, isPackage ? '/modules/' : CONF.directory_modules); arr = []; listing(dir, 0, arr, '.js'); arr.forEach((item) => dependencies.push(next => F.install('module', item.name, item.filename, undefined, undefined, undefined, true, undefined, undefined, next, packageName))); @@ -3183,9 +4294,9 @@ F.$load = function(types, targetdirectory, callback, packageName) { }); } - if (!types || types.indexOf('isomorphic') !== -1) { + if (can('isomorphic')) { operations.push(function(resume) { - dir = U.combine(targetdirectory, isPackage ? '/isomorphic/' : F.config['directory-isomorphic']); + dir = U.combine(targetdirectory, isPackage ? '/isomorphic/' : CONF.directory_isomorphic); arr = []; listing(dir, 0, arr, '.js'); arr.forEach((item) => dependencies.push(next => F.install('isomorphic', item.name, item.filename, undefined, undefined, undefined, true, undefined, undefined, next, packageName))); @@ -3193,9 +4304,9 @@ F.$load = function(types, targetdirectory, callback, packageName) { }); } - if (!types || types.indexOf('packages') !== -1) { + if (can('packages')) { operations.push(function(resume) { - dir = U.combine(targetdirectory, isPackage ? '/packages/' : F.config['directory-packages']); + dir = U.combine(targetdirectory, isPackage ? '/packages/' : CONF.directory_packages); arr = []; listing(dir, 0, arr, '.package'); var dirtmp = U.$normalize(dir); @@ -3218,10 +4329,13 @@ F.$load = function(types, targetdirectory, callback, packageName) { files.wait(function(filename, next) { - var stream = Fs.createReadStream(filename); - var writer = Fs.createWriteStream(Path.join(dir, filename.replace(item.filename, '').replace(/\.package$/i, ''))); - stream.pipe(writer); - writer.on('finish', next); + if (F.$bundling) { + var stream = Fs.createReadStream(filename); + var writer = Fs.createWriteStream(Path.join(dir, filename.replace(item.filename, '').replace(/\.package$/i, ''))); + stream.pipe(writer); + writer.on('finish', next); + } else + next(); }, function() { @@ -3237,9 +4351,9 @@ F.$load = function(types, targetdirectory, callback, packageName) { }); } - if (!types || types.indexOf('models') !== -1) { + if (can('models')) { operations.push(function(resume) { - dir = U.combine(targetdirectory, isPackage ? '/models/' : F.config['directory-models']); + dir = U.combine(targetdirectory, isPackage ? '/models/' : CONF.directory_models); arr = []; listing(dir, 0, arr); arr.forEach((item) => dependencies.push(next => F.install('model', item.name, item.filename, undefined, undefined, undefined, true, undefined, undefined, next, packageName))); @@ -3247,10 +4361,40 @@ F.$load = function(types, targetdirectory, callback, packageName) { }); } - if (!types || types.indexOf('themes') !== -1) { + if (can('schemas')) { + operations.push(function(resume) { + dir = U.combine(targetdirectory, isPackage ? '/schemas/' : CONF.directory_schemas); + arr = []; + listing(dir, 0, arr); + arr.forEach((item) => dependencies.push(next => F.install('schema', item.name, item.filename, undefined, undefined, undefined, true, undefined, undefined, next, packageName))); + resume(); + }); + } + + if (can('tasks')) { + operations.push(function(resume) { + dir = U.combine(targetdirectory, isPackage ? '/tasks/' : CONF.directory_tasks); + arr = []; + listing(dir, 0, arr); + arr.forEach((item) => dependencies.push(next => F.install('task', item.name, item.filename, undefined, undefined, undefined, true, undefined, undefined, next, packageName))); + resume(); + }); + } + + if (can('operations')) { + operations.push(function(resume) { + dir = U.combine(targetdirectory, isPackage ? '/operations/' : CONF.directory_operations); + arr = []; + listing(dir, 0, arr); + arr.forEach((item) => dependencies.push(next => F.install('operation', item.name, item.filename, undefined, undefined, undefined, true, undefined, undefined, next, packageName))); + resume(); + }); + } + + if (can('themes')) { operations.push(function(resume) { arr = []; - dir = U.combine(targetdirectory, isPackage ? '/themes/' : F.config['directory-themes']); + dir = U.combine(targetdirectory, isPackage ? '/themes/' : CONF.directory_themes); listing(dir, 0, arr, undefined, true); arr.forEach(function(item) { var themeName = item.name; @@ -3264,9 +4408,9 @@ F.$load = function(types, targetdirectory, callback, packageName) { }); } - if (!types || types.indexOf('definitions') !== -1) { + if (can('definitions')) { operations.push(function(resume) { - dir = U.combine(targetdirectory, isPackage ? '/definitions/' : F.config['directory-definitions']); + dir = U.combine(targetdirectory, isPackage ? '/definitions/' : CONF.directory_definitions); arr = []; listing(dir, 0, arr); arr.forEach((item) => dependencies.push(next => F.install('definition', item.name, item.filename, undefined, undefined, undefined, true, undefined, undefined, next, packageName))); @@ -3274,26 +4418,55 @@ F.$load = function(types, targetdirectory, callback, packageName) { }); } - if (!types || types.indexOf('controllers') !== -1) { + if (can('controllers')) { operations.push(function(resume) { arr = []; - dir = U.combine(targetdirectory, isPackage ? '/controllers/' : F.config['directory-controllers']); + dir = U.combine(targetdirectory, isPackage ? '/controllers/' : CONF.directory_controllers); listing(dir, 0, arr); arr.forEach((item) => dependencies.push(next => F.install('controller', item.name, item.filename, undefined, undefined, undefined, true, undefined, undefined, next, packageName))); resume(); }); } - if (!types || types.indexOf('components') !== -1) { + if (can('components')) { operations.push(function(resume) { arr = []; - dir = U.combine(targetdirectory, isPackage ? '/components/' : F.config['directory-components']); + dir = U.combine(targetdirectory, isPackage ? '/components/' : CONF.directory_components); listing(dir, 0, arr, '.html'); arr.forEach((item) => dependencies.push(next => F.install('component', item.name, item.filename, undefined, undefined, undefined, undefined, undefined, undefined, next, packageName))); resume(); }); } + var thread = global.THREAD; + if (thread) { + + // Updates PREF file + PREFFILE = PREFFILE.replace('.json', '_' + thread + '.json'); + + operations.push(function(resume) { + arr = []; + dir = '/threads/' + thread; + F.$configure_env(dir + '/.env'); + F.$configure_env(dir + '/.env-' + (DEBUG ? 'debug' : 'release')); + F.$configure_configs(dir + '/config'); + F.$configure_configs(dir + '/config-' + (DEBUG ? 'debug' : 'release')); + dir = U.combine(targetdirectory, '/threads/' + thread); + listing(dir, 0, arr); + arr.forEach(item => dependencies.push(next => F.install('module', 'threads/' + item.name, item.filename, undefined, undefined, undefined, true, undefined, undefined, next, packageName))); + resume(); + }); + } + + if (can('preferences')) { + operations.push(function(resume) { + if (F.onPrefLoad) + loadpreferences(resume); + else + resume(); + }); + } + operations.async(function() { var count = dependencies.length; F.consoledebug('load dependencies ' + count + 'x'); @@ -3309,6 +4482,19 @@ F.$load = function(types, targetdirectory, callback, packageName) { return F; }; +function loadpreferences(callback) { + F.onPrefLoad(function(value) { + if (value) { + var keys = Object.keys(value); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + F.pref[key] = global.PREF[key] = value[key]; + } + } + callback && callback(); + }); +} + F.$startup = function(callback) { var dir = Path.join(directory, '/startup/'); @@ -3320,7 +4506,7 @@ F.$startup = function(callback) { Fs.readdirSync(dir).forEach(function(o) { var extension = U.getExtension(o); - if (extension === 'js') + if (JSFILES[extension]) run.push(o); }); @@ -3328,7 +4514,7 @@ F.$startup = function(callback) { return callback(); run.wait(function(filename, next) { - var fn = dir + filename + new Date().format('yyMMdd_HHmmss'); + var fn = dir + filename + '_bk'; Fs.renameSync(dir + filename, fn); var fork = Child.fork(fn, [], { cwd: directory }); fork.on('exit', function() { @@ -3337,17 +4523,19 @@ F.$startup = function(callback) { }); }, callback); - return this; + return F; }; -F.uptodate = function(type, url, options, interval, callback, next) { +global.UPTODATE = F.uptodate = function(type, url, options, interval, callback, next) { if (typeof(options) === 'string' && typeof(interval) !== 'string') { interval = options; options = null; } - var obj = { type: type, name: '', url: url, interval: interval, options: options, count: 0, updated: F.datetime, errors: [], callback: callback }; + OBSOLETE('UPTODATE()', 'This method is deprecated and it will be removed in v4.'); + + var obj = { type: type, name: '', url: url, interval: interval, options: options, count: 0, updated: NOW, errors: [], callback: callback }; if (!F.uptodates) F.uptodates = []; @@ -3375,7 +4563,7 @@ F.uptodate = function(type, url, options, interval, callback, next) { * @param {String} packageName Internal, optional. * @return {Framework} */ -F.install = function(type, name, declaration, options, callback, internal, useRequired, skipEmit, uptodateName, next, packageName) { +global.INSTALL = F.install = function(type, name, declaration, options, callback, internal, useRequired, skipEmit, uptodateName, next, packageName) { var obj = null; @@ -3401,7 +4589,7 @@ F.install = function(type, name, declaration, options, callback, internal, useRe var content; var err; - F.datetime = new Date(); + NOW = new Date(); if (t === 'object') { t = typeof(options); @@ -3421,6 +4609,16 @@ F.install = function(type, name, declaration, options, callback, internal, useRe options = undefined; } + if (type === 'command') { + if (typeof(declaration) === 'function') { + if (F.commands[name]) + F.commands[name].push(declaration); + else + F.commands[name] = [declaration]; + } + return F; + } + // Check if declaration is a valid URL address if (type !== 'eval' && typeof(declaration) === 'string') { @@ -3505,15 +4703,20 @@ F.install = function(type, name, declaration, options, callback, internal, useRe F.routes.middleware[name] = typeof(declaration) === 'function' ? declaration : eval(declaration); F._length_middleware = Object.keys(F.routes.middleware).length; + if (REG_NEWIMPL.test(F.routes.middleware[name].toString())) + F.routes.middleware[name].$newversion = true; + else + OBSOLETE('MIDDLEWARE("{0}")'.format(name), 'You used older declaration of this delegate and you must rewrite it. Read more in docs.'); + next && next(); callback && callback(null, name); key = type + '.' + name; if (F.dependencies[key]) { - F.dependencies[key].updated = F.datetime; + F.dependencies[key].updated = NOW; } else { - F.dependencies[key] = { name: name, type: type, installed: F.datetime, updated: null, count: 0 }; + F.dependencies[key] = { name: name, type: type, installed: NOW, updated: null, count: 0 }; if (internal) F.dependencies[key].url = internal; } @@ -3521,9 +4724,9 @@ F.install = function(type, name, declaration, options, callback, internal, useRe F.dependencies[key].count++; setTimeout(function() { - F.emit(type + '#' + name); - F.emit('install', type, name); - F.temporary.ready[type + '#' + name] = F.datetime; + EMIT(type + '#' + name); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; }, 500); F.consoledebug('install', type + '#' + name); @@ -3531,13 +4734,12 @@ F.install = function(type, name, declaration, options, callback, internal, useRe } if (type === 'config' || type === 'configuration' || type === 'settings') { - F.$configure_configs(declaration instanceof Array ? declaration : declaration.toString().split('\n'), true); setTimeout(function() { - delete F.temporary['mail-settings']; - F.emit(type + '#' + name, F.config); - F.emit('install', type, name); - F.temporary.ready[type + '#' + name] = F.datetime; + delete F.temporary.mail_settings; + EMIT(type + '#' + name, CONF); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; }, 500); F.consoledebug('install', type + '#' + name); @@ -3550,9 +4752,9 @@ F.install = function(type, name, declaration, options, callback, internal, useRe F.$configure_versions(declaration.toString().split('\n')); setTimeout(function() { - F.emit(type + '#' + name); - F.emit('install', type, name); - F.temporary.ready[type + '#' + name] = F.datetime; + EMIT(type + '#' + name); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; }, 500); F.consoledebug('install', type + '#' + name); @@ -3565,9 +4767,9 @@ F.install = function(type, name, declaration, options, callback, internal, useRe F.$configure_workflows(declaration.toString().split('\n')); setTimeout(function() { - F.emit(type + '#' + name); - F.emit('install', type, name); - F.temporary.ready[type + '#' + name] = F.datetime; + EMIT(type + '#' + name); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; F.consoledebug('install', type + '#' + name); }, 500); @@ -3580,9 +4782,9 @@ F.install = function(type, name, declaration, options, callback, internal, useRe F.$configure_sitemap(declaration.toString().split('\n')); setTimeout(function() { - F.emit(type + '#' + name); - F.emit('install', type, name); - F.temporary.ready[type + '#' + name] = F.datetime; + EMIT(type + '#' + name); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; F.consoledebug('install', type + '#' + name); }, 500); @@ -3598,12 +4800,18 @@ F.install = function(type, name, declaration, options, callback, internal, useRe F.uninstall(type, uptodateName || name, uptodateName ? 'uptodate' : undefined); - var hash = '\n/*' + name.hash() + '*/\n'; + var hash = '\n/*' + name.crc32(true) + '*/\n'; var temporary = (F.id ? 'i-' + F.id + '_' : '') + 'components'; content = parseComponent(internal ? declaration : Fs.readFileSync(declaration).toString(ENCODING), name); - content.js && Fs.appendFileSync(F.path.temp(temporary + '.js'), hash + (F.config.debug ? component_debug(name, content.js, 'js') : content.js) + hash.substring(0, hash.length - 1)); - content.css && Fs.appendFileSync(F.path.temp(temporary + '.css'), hash + (F.config.debug ? component_debug(name, content.css, 'css') : content.css) + hash.substring(0, hash.length - 1)); + + if (F.$bundling) { + content.js && Fs.appendFileSync(F.path.temp(temporary + '.js'), hash + (DEBUG ? component_debug(name, content.js, 'js') : content.js) + hash.substring(0, hash.length - 1)); + content.css && Fs.appendFileSync(F.path.temp(temporary + '.css'), hash + (DEBUG ? component_debug(name, content.css, 'css') : content.css) + hash.substring(0, hash.length - 1)); + } + + if (!Object.keys(content.parts).length) + content.parts = null; if (content.js) F.components.js = true; @@ -3611,16 +4819,21 @@ F.install = function(type, name, declaration, options, callback, internal, useRe if (content.css) F.components.css = true; + if (content.files) + F.components.files[name] = content.files; + else + delete F.components.files[name]; + if (content.body) { F.components.views[name] = '.' + F.path.temp('component_' + name); - Fs.writeFile(F.components.views[name].substring(1) + '.html', U.minifyHTML(content.body), NOOP); + F.$bundling && Fs.writeFile(F.components.views[name].substring(1) + '.html', U.minifyHTML(content.body), NOOP); } else delete F.components.views[name]; F.components.has = true; - var link = F.config['static-url-components']; - F.components.version = F.datetime.getTime(); + var link = CONF.static_url_components; + F.components.version = NOW.getTime(); F.components.links = (F.components.js ? ''.format(link, F.components.version) : '') + (F.components.css ? ''.format(link, F.components.version) : ''); if (content.install) { @@ -3637,8 +4850,10 @@ F.install = function(type, name, declaration, options, callback, internal, useRe obj.$owner = _owner; F.temporary.owners[_owner] = true; _controller = ''; + obj.name = name; + obj.parts = content.parts; F.components.instances[name] = obj; - obj && typeof(obj.install) === 'function' && obj.install(options || F.config[_owner], name); + obj && typeof(obj.install) === 'function' && obj.install(options || CONF[_owner], name); } catch(e) { F.error(e, 'F.install(\'component\', \'{0}\')'.format(name)); } @@ -3648,10 +4863,12 @@ F.install = function(type, name, declaration, options, callback, internal, useRe _owner = (packageName ? packageName + '@' : '') + type + '#' + name; F.temporary.owners[_owner] = true; obj = require(js); + obj.name = name; + obj.parts = content.parts; obj.$owner = _owner; _controller = ''; F.components.instances[name] = obj; - typeof(obj.install) === 'function' && obj.install(options || F.config[_owner], name); + typeof(obj.install) === 'function' && obj.install(options || CONF[_owner], name); (function(name) { setTimeout(function() { delete require.cache[name]; @@ -3660,31 +4877,35 @@ F.install = function(type, name, declaration, options, callback, internal, useRe } } - if (obj && obj.group) { - key = obj.group.hash(); + if (obj) { + + if (!obj.group) + obj.group = 'default'; + + key = obj.group.crc32(true); temporary += '_g' + key; tmp = F.components.groups[obj.group]; if (!tmp) tmp = F.components.groups[obj.group] = {}; if (content.js) { - Fs.appendFileSync(F.path.temp(temporary + '.js'), hash + (F.config.debug ? component_debug(name, content.js, 'js') : content.js) + hash.substring(0, hash.length - 1)); + Fs.appendFileSync(F.path.temp(temporary + '.js'), hash + (DEBUG ? component_debug(name, content.js, 'js') : content.js) + hash.substring(0, hash.length - 1)); tmp.js = true; } if (content.css) { - Fs.appendFileSync(F.path.temp(temporary + '.css'), hash + (F.config.debug ? component_debug(name, content.css, 'css') : content.css) + hash.substring(0, hash.length - 1)); + Fs.appendFileSync(F.path.temp(temporary + '.css'), hash + (DEBUG ? component_debug(name, content.css, 'css') : content.css) + hash.substring(0, hash.length - 1)); tmp.css = true; } - tmp.version = F.datetime.getTime(); - tmp.links = (tmp.js ? ''.format(link, tmp.version, key) : '') + (tmp.css ? ''.format(link, tmp.version, key) : ''); + tmp.version = GUID(5); + tmp.links = (tmp.js ? ''.format(link, tmp.version, key) : '') + (tmp.css ? ''.format(link, tmp.version, key) : ''); } !skipEmit && setTimeout(function() { - F.emit(type + '#' + name); - F.emit('install', type, name); - F.temporary.ready[type + '#' + name] = F.datetime; + EMIT(type + '#' + name); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; }, 500); F.consoledebug('install', type + '#' + name); @@ -3696,11 +4917,11 @@ F.install = function(type, name, declaration, options, callback, internal, useRe if (type === 'package') { var id = Path.basename(declaration, '.' + U.getExtension(declaration)); - var dir = F.config['directory-temp'][0] === '~' ? Path.join(F.config['directory-temp'].substring(1), id + '.package') : Path.join(F.path.root(), F.config['directory-temp'], id + '.package'); + var dir = CONF.directory_temp[0] === '~' ? Path.join(CONF.directory_temp.substring(1), id + '.package') : Path.join(F.path.root(), CONF.directory_temp, id + '.package'); F.routes.packages[id] = dir; - F.restore(declaration, dir, function() { + var restorecb = function() { var filename = Path.join(dir, 'index.js'); if (!existsSync(filename)) { next && next(); @@ -3708,20 +4929,25 @@ F.install = function(type, name, declaration, options, callback, internal, useRe return; } - F.install('module', id, filename, options || F.config['package#' + name], function(err) { + F.install('module', id, filename, options || CONF['package#' + name], function(err) { setTimeout(function() { - F.emit('module#' + name); - F.emit(type + '#' + name); - F.emit('install', 'module', name); - F.emit('install', type, name); - F.temporary.ready['package#' + name] = F.datetime; - F.temporary.ready['module#' + name] = F.datetime; + EMIT('module#' + name); + EMIT(type + '#' + name); + EMIT('install', 'module', name); + EMIT('install', type, name); + F.temporary.ready['package#' + name] = NOW; + F.temporary.ready['module#' + name] = NOW; }, 500); F.consoledebug('install', 'package#' + name); callback && callback(err, name); }, internal, useRequired, true, undefined); next && next(); - }); + }; + + if (F.$bundling) + F.restore(declaration, dir, restorecb); + else + restorecb(); return F; } @@ -3733,12 +4959,12 @@ F.install = function(type, name, declaration, options, callback, internal, useRe obj.$owner = _owner; F.temporary.owners[_owner] = true; - typeof(obj.install) === 'function' && obj.install(options || F.config[_owner], name); + typeof(obj.install) === 'function' && obj.install(options || CONF[_owner], name); !skipEmit && setTimeout(function() { - F.emit(type + '#' + name); - F.emit('install', type, name); - F.temporary.ready[type + '#' + name] = F.datetime; + EMIT(type + '#' + name); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; }, 500); F.consoledebug('install', type + '#' + name); @@ -3756,16 +4982,16 @@ F.install = function(type, name, declaration, options, callback, internal, useRe if (type === 'package2') { type = type.substring(0, type.length - 1); var id = U.getName(declaration, '.package'); - var dir = F.config['directory-temp'][0] === '~' ? Path.join(F.config['directory-temp'].substring(1), id) : Path.join(F.path.root(), F.config['directory-temp'], id); + var dir = CONF.directory_temp[0] === '~' ? Path.join(CONF.directory_temp.substring(1), id) : Path.join(F.path.root(), CONF.directory_temp, id); var filename = Path.join(dir, 'index.js'); - F.install('module', id.replace(/\.package$/i, ''), filename, options || F.config['package#' + name], function(err) { + F.install('module', id.replace(/\.package$/i, ''), filename, options || CONF['package#' + name], function(err) { setTimeout(function() { - F.emit('module#' + name); - F.emit(type + '#' + name); - F.emit('install', type, name); - F.emit('install', 'module', name); - F.temporary.ready['package#' + name] = F.datetime; - F.temporary.ready['module#' + name] = F.datetime; + EMIT('module#' + name); + EMIT(type + '#' + name); + EMIT('install', type, name); + EMIT('install', 'module', name); + F.temporary.ready['package#' + name] = NOW; + F.temporary.ready['module#' + name] = NOW; }, 500); F.consoledebug('install', 'package#' + name); callback && callback(err, name); @@ -3792,9 +5018,9 @@ F.install = function(type, name, declaration, options, callback, internal, useRe Fs.writeFileSync(item.filename, framework_internal.modificators(declaration, name)); setTimeout(function() { - F.emit(type + '#' + name); - F.emit('install', type, name); - F.temporary.ready[type + '#' + name] = F.datetime; + EMIT(type + '#' + name); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; }, 500); F.consoledebug('install', type + '#' + name); @@ -3803,7 +5029,7 @@ F.install = function(type, name, declaration, options, callback, internal, useRe return F; } - if (type === 'definition' || type === 'eval') { + if (type === 'definition' || type === 'eval' || type === 'schema' || type === 'operation' || type === 'task') { _controller = ''; _owner = (packageName ? packageName + '@' : '') + type + '#' + name; @@ -3822,8 +5048,7 @@ F.install = function(type, name, declaration, options, callback, internal, useRe (function(name) { setTimeout(() => delete require.cache[name], 1000); })(require.resolve(declaration)); - } - else + } else obj = typeof(declaration) === 'function' ? eval('(' + declaration.toString() + ')()') : eval(declaration); } catch (ex) { @@ -3842,9 +5067,9 @@ F.install = function(type, name, declaration, options, callback, internal, useRe callback && callback(null, name); setTimeout(function() { - F.emit(type + '#' + name); - F.emit('install', type, name); - F.temporary.ready[type + '#' + name] = F.datetime; + EMIT(type + '#' + name); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; }, 500); return F; @@ -3855,6 +5080,8 @@ F.install = function(type, name, declaration, options, callback, internal, useRe content = ''; err = null; + OBSOLETE('isomorphic', 'Isomorphic scripts will be removed in v4.'); + try { if (!name && typeof(internal) === 'string') { @@ -3904,16 +5131,17 @@ F.install = function(type, name, declaration, options, callback, internal, useRe tmp = F.path.temp('isomorphic_' + name + '.min.js'); F.map(framework_internal.preparePath(obj.url), tmp); F.isomorphic[name] = obj; - Fs.writeFileSync(tmp, prepare_isomorphic(name, framework_internal.compile_javascript(content, '#' + name))); + + F.$bundling && Fs.writeFileSync(tmp, prepare_isomorphic(name, framework_internal.compile_javascript(content, '#' + name))); F.consoledebug('install', type + '#' + name); next && next(); callback && callback(null, name); setTimeout(function() { - F.emit(type + '#' + name, obj); - F.emit('install', type, name, obj); - F.temporary.ready[type + '#' + name] = F.datetime; + EMIT(type + '#' + name, obj); + EMIT('install', type, name, obj); + F.temporary.ready[type + '#' + name] = NOW; }, 500); return F; @@ -3990,10 +5218,10 @@ F.install = function(type, name, declaration, options, callback, internal, useRe if (tmp) { F.dependencies[key] = tmp; - F.dependencies[key].updated = F.datetime; + F.dependencies[key].updated = NOW; } else { - F.dependencies[key] = { name: name, type: type, installed: F.datetime, updated: null, count: 0 }; + F.dependencies[key] = { name: name, type: type, installed: NOW, updated: null, count: 0 }; if (internal) F.dependencies[key].url = internal; } @@ -4010,12 +5238,12 @@ F.install = function(type, name, declaration, options, callback, internal, useRe else F.sources[name] = obj; - typeof(obj.install) === 'function' && obj.install(options || F.config[type + '#' + name], name); + typeof(obj.install) === 'function' && obj.install(options || CONF[type + '#' + name], name); !skipEmit && setTimeout(function() { - F.emit(type + '#' + name, obj); - F.emit('install', type, name, obj); - F.temporary.ready[type + '#' + name] = F.datetime; + EMIT(type + '#' + name, obj); + EMIT('install', type, name, obj); + F.temporary.ready[type + '#' + name] = NOW; }, 500); F.consoledebug('install', type + '#' + name); @@ -4094,20 +5322,17 @@ F.install = function(type, name, declaration, options, callback, internal, useRe F.directory = directory = tmpdir; F.temporary.path = {}; F.temporary.notfound = {}; + F.$configure_env(); F.$configure_configs(); F.$configure_versions(); F.$configure_dependencies(); F.$configure_sitemap(); F.$configure_workflows(); } else { - + F.$configure_env('@' + name + '/.env'); + F.$configure_env('@' + name + '/.env-' + (DEBUG ? 'debug' : 'release')); F.$configure_configs('@' + name + '/config'); - - if (F.config.debug) - F.$configure_configs('@' + name + '/config-debug'); - else - F.$configure_configs('@' + name + '/config-release'); - + F.$configure_configs('@' + name + '/config-' + (DEBUG ? 'debug' : 'release')); F.isTest && F.$configure_configs('@' + name + '/config-test'); F.$configure_versions('@' + name + '/versions'); F.$configure_dependencies('@' + name + '/dependencies'); @@ -4115,7 +5340,7 @@ F.install = function(type, name, declaration, options, callback, internal, useRe F.$configure_workflows('@' + name + '/workflows'); } - F.$load(undefined, tmpdir, undefined, name); + F.$bundle(() => F.$load(undefined, tmpdir, undefined, name)); }, 100); key = type + '.' + name; @@ -4126,10 +5351,10 @@ F.install = function(type, name, declaration, options, callback, internal, useRe if (tmp) { F.dependencies[key] = tmp; - F.dependencies[key].updated = F.datetime; + F.dependencies[key].updated = NOW; } else { - F.dependencies[key] = { name: name, type: type, installed: F.datetime, updated: null, count: 0, _id: _ID }; + F.dependencies[key] = { name: name, type: type, installed: NOW, updated: null, count: 0, _id: _ID }; if (internal) F.dependencies[key].url = internal; } @@ -4169,158 +5394,14 @@ F.install = function(type, name, declaration, options, callback, internal, useRe return F; }; -F.restart = function() { - if (!F.isRestarted) { - F.isRestarted = true; - F.emit('restart'); - setTimeout(() => F.$restart(), 1000); - } - return F; -}; - -F.$restart = function() { - - console.log('----------------------------------------------------> RESTART ' + new Date().format('yyyy-MM-dd HH:mm:ss')); - - F.server.setTimeout(0); - F.server.timeout = 0; - F.server.close(function() { - - Object.keys(F.modules).forEach(function(key) { - var item = F.modules[key]; - item && item.uninstall && item.uninstall(); - }); - - Object.keys(F.models).forEach(function(key) { - var item = F.models[key]; - item && item.uninstall && item.uninstall(); - }); - - Object.keys(F.controllers).forEach(function(key) { - var item = F.controllers[key]; - item && item.uninstall && item.uninstall(); - }); - - Object.keys(F.workers).forEach(function(key) { - var item = F.workers[key]; - if (item && item.kill) { - item.removeAllListeners(); - item.kill('SIGTERM'); - } - }); - - Object.keys(F.connections).forEach(function(key) { - var item = F.connections[key]; - if (item) { - item.removeAllListeners(); - item.close(); - } - }); - - framework_builders.restart(); - framework_image.restart(); - framework_mail.restart(); - U.restart(); - framework_internal.restart(); - - F.cache.clear(); - F.cache.stop(); - F.$events = {}; - F.global = {}; - F.resources = {}; - F.connections = {}; - F.functions = {}; - F.themes = {}; - F.uptodates = null; - F.versions = null; - F.schedules = []; - F.isLoaded = false; - F.isRestarted = false; - F.components = { has: false, css: false, js: false, views: {}, instances: {}, version: null, links: '', groups: {} }; - - F.routes = { - sitemap: null, - web: [], - system: {}, - files: [], - cors: [], - websockets: [], - middleware: {}, - redirects: {}, - resize: {}, - request: [], - views: {}, - merge: {}, - mapping: {}, - packages: {}, - blocks: {}, - resources: {}, - mmr: {} - }; - - F.temporary = { - path: {}, - notfound: {}, - processing: {}, - range: {}, - views: {}, - versions: {}, - dependencies: {}, - other: {}, - internal: {}, - owners: {}, - ready: {} - }; - - F.modificators = null; - F.helpers = {}; - F.modules = {}; - F.models = {}; - F.sources = {}; - F.controllers = {}; - F.dependencies = {}; - F.isomorphic = {}; - F.tests = []; - F.errors = []; - F.problems = []; - F.changes = []; - F.traces = []; - F.workers = {}; - F.convertors = []; - F.databases = {}; - - F._request_check_redirect = false; - F._request_check_referer = false; - F._request_check_POST = false; - F._request_check_robot = false; - F._length_middleware = 0; - F._length_request_middleware = 0; - F._length_files = 0; - F._length_wait = 0; - F._length_themes = 0; - F._length_cors = 0; - F._length_subdomain_web = 0; - F._length_subdomain_websocket = 0; - F.isVirtualDirectory = false; - F.isTheme = false; - F.stats.other.restart++; - - setTimeout(() => F.removeAllListeners(), 2000); - setTimeout(function() { - var init = F.temporary.init; - F.mode(init.isHTTPS ? require('https') : http, init.name, init.options); - }, 1000); - }); - return F; -}; - F.install_prepare = function(noRecursive) { var keys = Object.keys(F.temporary.dependencies); - if (!keys.length) return; + OBSOLETE('exports.dependencies()', 'Module dependencies will be removed in v4: "' + keys.join(', ') + '"'); + // check dependencies for (var i = 0, length = keys.length; i < length; i++) { @@ -4361,7 +5442,7 @@ F.install_prepare = function(noRecursive) { if (keys.length) throw new Error('Dependency exception, missing dependencies for: ' + keys.join(', ').trim()); delete F.temporary.other.dependencies; - }, F.config['default-dependency-timeout']); + }, CONF.default_dependency_timeout); if (!keys.length || noRecursive) return F; @@ -4380,16 +5461,24 @@ F.install_make = function(key, name, obj, options, callback, skipEmit, type) { _controller = routeID; _owner = type + '#' + name.replace(/\.package$/gi, ''); - typeof(obj.install) === 'function' && obj.install(options || F.config[_owner], name); + typeof(obj.install) === 'function' && obj.install(options || CONF[_owner], name); me.processed = true; var id = (type === 'module' ? '#' : '') + name; var length = F.routes.web.length; + for (var i = 0; i < length; i++) { if (F.routes.web[i].controller === routeID) F.routes.web[i].controller = id; } + var tmp = Object.keys(F.routes.system); + length = tmp.length; + for (var i = 0; i < length; i++) { + if (F.routes.system[tmp[i]].controller === routeID) + F.routes.system[tmp[i]].controller = id; + } + length = F.routes.websockets.length; for (var i = 0; i < length; i++) { if (F.routes.websockets[i].controller === routeID) @@ -4408,9 +5497,9 @@ F.install_make = function(key, name, obj, options, callback, skipEmit, type) { if (!skipEmit) { setTimeout(function() { - F.emit(type + '#' + name, obj); - F.emit('install', type, name, obj); - F.temporary.ready[type + '#' + name] = F.datetime; + EMIT(type + '#' + name, obj); + EMIT('install', type, name, obj); + F.temporary.ready[type + '#' + name] = NOW; }, 500); } @@ -4427,7 +5516,7 @@ F.install_make = function(key, name, obj, options, callback, skipEmit, type) { * @param {Object} skipEmit Internal, optional. * @return {Framework} */ -F.uninstall = function(type, name, options, skipEmit, packageName) { +global.UNINSTALL = F.uninstall = function(type, name, options, skipEmit, packageName) { var obj = null; var k, v, tmp; @@ -4451,6 +5540,7 @@ F.uninstall = function(type, name, options, skipEmit, packageName) { if (k !== 'id') v = framework_internal.preparePath(framework_internal.encodeUnicodeURL(v.replace('*', '').trim())); F.routes.cors = F.routes.cors.remove(k, v); + F._length_cors = F.routes.cors.length; F.consoledebug('uninstall', type + '#' + name); return F; } @@ -4461,6 +5551,12 @@ F.uninstall = function(type, name, options, skipEmit, packageName) { return F; } + if (type === 'convertor') { + F.convertor(name, null); + F.consoledebug('uninstall', type + '#' + name); + return F; + } + if (type === 'schedule') { F.clearSchedule(name); F.consoledebug('uninstall', type + '#' + name); @@ -4482,6 +5578,7 @@ F.uninstall = function(type, name, options, skipEmit, packageName) { k = typeof(name) === 'string' ? name.substring(0, 3) === 'id:' ? 'id' : 'urlraw' : 'execute'; v = k === 'execute' ? name : k === 'id' ? name.substring(3).trim() : name; F.routes.files = F.routes.files.remove(k, v); + F._length_files = F.routes.files.length; F.consoledebug('uninstall', type + '#' + name); return F; } @@ -4606,6 +5703,7 @@ F.uninstall = function(type, name, options, skipEmit, packageName) { delete F.components.instances[name]; delete F.components.views[name]; + delete F.components.files[name]; delete F.temporary.ready[type + '#' + name]; var temporary = (F.id ? 'i-' + F.id + '_' : '') + 'components'; @@ -4660,17 +5758,17 @@ F.uninstall = function(type, name, options, skipEmit, packageName) { } } - tmp.version = F.datetime.getTime(); + tmp.version = NOW.getTime(); } } if (is) - F.components.version = F.datetime.getTime(); + F.components.version = NOW.getTime(); F.consoledebug('uninstall', type + '#' + name); } - !skipEmit && F.emit('uninstall', type, name); + !skipEmit && EMIT('uninstall', type, name); return F; }; @@ -4689,7 +5787,14 @@ F.$uninstall = function(owner, controller) { F.routes.files = F.routes.files.remove('owner', owner); F.routes.websockets = F.routes.websockets.remove('owner', owner); F.routes.cors = F.routes.cors.remove('owner', owner); - F.schedules = F.schedules.remove('owner', owner); + + var keys = Object.keys(F.schedules); + + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + if (F.schedules[key].owner == owner) + delete F.schedules[key]; + } if (F.modificators) F.modificators = F.modificators.remove('$owner', owner); @@ -4757,7 +5862,7 @@ F.register = function(path) { path = F.path.package(path.substring(1)); else if (c === '=') { if (path[1] === '?') - F.path.themes(F.config['default-theme'] + path.substring(2)); + F.path.themes(CONF.default_theme + path.substring(2)); else path = F.path.themes(path.substring(1)); } @@ -4797,8 +5902,8 @@ F.eval = function(script) { * @return {Framework} */ F.onError = function(err, name, uri) { - F.datetime = new Date(); - console.log('======= ' + (F.datetime.format('yyyy-MM-dd HH:mm:ss')) + ': ' + (name ? name + ' ---> ' : '') + err.toString() + (uri ? ' (' + Parser.format(uri) + ')' : ''), err.stack); + NOW = new Date(); + console.log('======= ' + (NOW.format('yyyy-MM-dd HH:mm:ss')) + ': ' + (name ? name + ' ---> ' : '') + err.toString() + (uri ? ' (' + Parser.format(uri) + ')' : ''), err.stack); return F; }; @@ -4845,18 +5950,29 @@ F.onMapping = function(url, def, ispublic, encode) { if (url[0] !== '/') url = '/' + url; - if (F._length_themes) { - var index = url.indexOf('/', 1); - if (index !== -1) { - var themeName = url.substring(1, index); - if (F.themes[themeName]) - return F.themes[themeName] + 'public' + url.substring(index); - } + var tmp = url; + if (CONF.default_root) + tmp = tmp.substring(CONF.default_root.length - 1); + + // component files + if (tmp[1] === '~') { + var index = tmp.indexOf('/', 2); + var name = tmp.substring(2, index); + return F.components.files[name] && F.components.files[name][tmp.substring(index + 1)] ? (F.path.temp() + tmp.substring(1)) : null; } if (F.routes.mapping[url]) return F.routes.mapping[url]; + if (F._length_themes) { + var index = tmp.indexOf('/', 2); + if (index !== -1) { + var themeName = tmp.substring(1, index); + if (F.themes[themeName]) + return F.themes[themeName] + 'public' + tmp.substring(index); + } + } + def = framework_internal.preparePath(def, true); if (encode) @@ -4870,12 +5986,10 @@ F.onMapping = function(url, def, ispublic, encode) { return def; }; -F.download = F.snapshot = function(url, filename, callback) { +global.DOWNLOAD = F.download = F.snapshot = function(url, filename, callback) { - if (!F.isLoaded) { - setTimeout(function(url, filename, callback) { - F.snapshot(url, filename, callback); - }, 200, url, filename, callback); + if (!F.isLoaded && url[0] === '/') { + setTimeout(F.download, 200, url, filename, callback); return F; } @@ -4885,11 +5999,11 @@ F.download = F.snapshot = function(url, filename, callback) { if (url[0] !== '/') url = '/' + url; if (F.isWorker) - throw new Error('Worker can\'t create a snapshot from relative URL address "{0}".'.format(url)); + throw new Error('Worker can\'t create a snapshot from the relative URL address "{0}".'.format(url)); url = 'http://' + (F.ip === 'auto' ? '0.0.0.0' : F.ip) + ':' + F.port + url; } - U.download(url, FLAGS_INSTALL, function(err, response) { + U.download(url, FLAGS_DOWNLOAD, function(err, response) { if (err) { callback && callback(err); @@ -4898,17 +6012,18 @@ F.download = F.snapshot = function(url, filename, callback) { } var stream = Fs.createWriteStream(filename); - response.pipe(stream); - response.on('error', function(err) { - callback && callback(err); - callback = null; - }); + var done = function(err) { + if (callback) { + callback(err); + callback = null; + } + }; - CLEANUP(stream, function() { - callback && callback(null, filename); - callback = null; - }); + response.pipe(stream); + response.on('error', done); + stream.on('error', done); + CLEANUP(stream, done); }); return F; @@ -4967,8 +6082,8 @@ F.onValidate = null; * @param {String} value * @return {Object} */ -F.onParseXML = function(value) { - var val = U.parseXML(value); +F.onParseXML = function(value, replace) { + var val = U.parseXML(value, replace); F._length_convertors && F.convert(val); return val; }; @@ -4988,7 +6103,11 @@ F.$onParseXML = function(req) { * @return {Object} */ F.onParseJSON = function(value) { - return JSON.parse(value); + if (value) { + try { + return JSON.parse(value); + } catch (e) {} + } }; F.onParseJSON.$def = true; @@ -4996,6 +6115,115 @@ F.$onParseJSON = function(req) { req.body = F.onParseJSON.$def ? JSON.parse(req.buffer_data) : F.onParseJSON(req.buffer_data); }; +function parseQueryArgumentsDecode(val) { + try { + return decodeURIComponent(val); + } catch (e) { + return ''; + } +} + +const QUERY_ALLOWED = { '45': 1, '95': 1, 46: 1, '91': 1, '92': 1 }; + +function parseQueryArguments(str) { + + var obj = {}; + var key = ''; + var val = ''; + var is = false; + var decodev = false; + var decodek = false; + var count = 0; + var pos = 0; + + str += '&'; + + for (var i = 0; i < str.length; i++) { + var n = str.charCodeAt(i); + + if (n === 38) { + + if (key) { + if (pos < i) + val += str.substring(pos, i); + + if (decodev) + val = parseQueryArgumentsDecode(val); + + if (decodek) + key = parseQueryArgumentsDecode(key); + + obj[key] = val; + } + + if (key) + key = ''; + + if (val) + val = ''; + + pos = i + 1; + is = false; + decodek = false; + decodev = false; + + if ((count++) >= QUERYPARSEROPTIONS.maxKeys) + break; + + } else { + + if (n === 61) { + if ((i - pos) > CONF.default_request_maxkey) + key = ''; + else { + if (pos < i) + key += str.substring(pos, i); + pos = i + 1; + is = true; + } + continue; + } + + if (!is) { + + var can = false; + + if (n > 47 && n < 58) + can = true; + else if ((n > 64 && n < 91) || (n > 96 && n < 123)) + can = true; + else if (QUERY_ALLOWED[n]) + can = true; + + if (!can) + break; + } + + if (n === 43) { + if (is) + val += str.substring(pos, i) + ' '; + else + key += str.substring(pos, i) + ' '; + pos = i + 1; + } + + if (n === 37) { + if (str.charCodeAt(i + 1) === 48 && str.charCodeAt(i + 2) === 48) + pos = i + 3; + else if (is) { + if (!decodev) + decodev = true; + } else { + if (!decodev) + decodek = true; + } + } + } + } + + return obj; +} + /** * Global JSON parsing * @param {String} value @@ -5003,7 +6231,8 @@ F.$onParseJSON = function(req) { */ F.onParseQuery = function(value) { if (value) { - var val = Qs.parse(value, null, null, QUERYPARSEROPTIONS); + // var val = Qs.parse(value, null, null, QUERYPARSEROPTIONS); + var val = parseQueryArguments(value); F._length_convertors && F.convert(val); return val; } @@ -5014,7 +6243,8 @@ F.onParseQuery.$def = true; F.$onParseQueryBody = function(req) { if (F.onParseQuery.$def) { if (req.buffer_data) { - req.body = Qs.parse(req.buffer_data, null, null, QUERYPARSEROPTIONS); + // req.body = Qs.parse(req.buffer_data, null, null, QUERYPARSEROPTIONS); + req.body = parseQueryArguments(req.buffer_data); F._length_convertors && F.convert(req.body); } else req.body = {}; @@ -5024,8 +6254,12 @@ F.$onParseQueryBody = function(req) { F.$onParseQueryUrl = function(req) { if (F.onParseQuery.$def) { - req._querydata = Qs.parse(req.uri.query, null, null, QUERYPARSEROPTIONS); - F._length_convertors && F.convert(req._querydata); + if (req.uri.query) { + // req._querydata = Qs.parse(req.uri.query, null, null, QUERYPARSEROPTIONS); + req._querydata = parseQueryArguments(req.uri.query); + F._length_convertors && F.convert(req._querydata); + } else + req._querydata = {}; } else req._querydata = F.onParseQuery(req.uri.query, req); }; @@ -5037,12 +6271,24 @@ F.$onParseQueryUrl = function(req) { * @param {String} name * @param {Function(err, body)} callback */ -F.onSchema = function(req, group, name, callback, filter, novalidate) { - var schema = GETSCHEMA(group, name); +F.onSchema = function(req, route, callback) { + + var schema; + + if (route.isDYNAMICSCHEMA) { + var index = route.param[route.paramnames.indexOf(route.schema[1])]; + req.$schemaname = route.schema[0] + '/' + req.split[index]; + schema = framework_builders.findschema(req.$schemaname); + } else + schema = GETSCHEMA(route.schema[0], route.schema[1]); + + if (req.method === 'PATCH' || req.method === 'DELETE') + req.$patch = true; + if (schema) - schema.make(req.body, filter, onSchema_callback, callback, novalidate); + schema.make(req.body, route.schema[2], onSchema_callback, callback, route.novalidate, route.workflow ? route.workflow.meta : null, req); else - callback(new Error('Schema "' + group + '/' + name + '" not found.')); + callback('Schema "' + (route.isDYNAMICSCHEMA ? req.$schemaname : (route.schema[0] + '/' + route.schema[1])) + '" not found.'); }; function onSchema_callback(err, res, callback) { @@ -5052,6 +6298,8 @@ function onSchema_callback(err, res, callback) { callback(null, res); } +var onmailsendforce = (cb, message) => message.send2(cb); + /** * Mail delegate * @param {String or Array String} address @@ -5079,19 +6327,19 @@ F.onMail = function(address, subject, body, callback, replyTo) { } else message.to(address); - message.from(F.config['mail-address-from'] || '', F.config.name); + message.from(CONF.mail_address_from || '', CONF.name); if (replyTo) message.reply(replyTo); else { - tmp = F.config['mail-address-reply']; + tmp = CONF.mail_address_reply; tmp && tmp.length > 3 && message.reply(tmp); } - tmp = F.config['mail-address-copy']; + tmp = CONF.mail_address_copy; tmp && tmp.length > 3 && message.bcc(tmp); - message.$sending = setImmediate(cb => message.send2(cb), callback); + message.$sending = setImmediate(onmailsendforce, callback, message); return message; }; @@ -5109,7 +6357,7 @@ F.onMeta = function() { switch (i) { case 0: - builder += '' + (arg + (F.url !== '/' && !F.config['allow-custom-titles'] ? ' - ' + F.config.name : '')) + ''; + builder += '' + (arg + (F.url !== '/' && !CONF.allow_custom_titles ? ' - ' + CONF.name : '')) + ''; break; case 1: builder += ''; @@ -5119,7 +6367,7 @@ F.onMeta = function() { break; case 3: var tmp = arg.substring(0, 6); - var img = tmp === 'http:/' || tmp === 'https:' || arg.substring(0, 2) === '//' ? arg : self.hostname(self.routeImage(arg)); + var img = tmp === 'http:/' || tmp === 'https:' || arg.substring(0, 2) === '//' ? arg : self.hostname(self.public_image(arg)); builder += ''; break; } @@ -5128,12 +6376,62 @@ F.onMeta = function() { return builder; }; +global.AUDIT = function(name, $, type, message) { + + if (message == null) { + message = type; + type = null; + } + + var data = {}; + + if ($.user) { + data.userid = $.user.id; + data.username = $.user.name || $.user.nick || $.user.alias; + } + + if ($.req) { + if ($.req.sessionid) + data.sessionid = $.req.sessionid; + data.ua = $.req.ua; + data.ip = $.ip; + } + + if (type) + data.type = type; + + if ($.name) + data.caller = ($.schema ? ($.schema.name + '/') : '') + $.name; + + if (F.id) + data.instance = F.id; + + data.created = NOW = new Date(); + + if (message) + data.message = message; + + DEF.onAudit(name, data); +}; + +global.NOSQLREADER = function(filename) { + if (!global.framework_nosql) + global.framework_nosql = require('./nosql'); + return new framework_nosql.Database('readonlynosql', filename, true); +}; + +global.TABLEREADER = function(filename) { + if (!global.framework_nosql) + global.framework_nosql = require('./nosql'); + return new framework_nosql.Table('readonlytable', filename, true); +}; + // @arguments {Object params} -F.log = function() { +global.LOG = F.log = function() { - F.datetime = new Date(); - var filename = F.datetime.getFullYear() + '-' + (F.datetime.getMonth() + 1).toString().padLeft(2, '0') + '-' + F.datetime.getDate().toString().padLeft(2, '0'); - var time = F.datetime.getHours().toString().padLeft(2, '0') + ':' + F.datetime.getMinutes().toString().padLeft(2, '0') + ':' + F.datetime.getSeconds().toString().padLeft(2, '0'); + NOW = new Date(); + var filename = NOW.getFullYear() + '-' + (NOW.getMonth() + 1).toString().padLeft(2, '0') + '-' + NOW.getDate().toString().padLeft(2, '0'); + var time = NOW.getHours().toString().padLeft(2, '0') + ':' + NOW.getMinutes().toString().padLeft(2, '0') + ':' + NOW.getSeconds().toString().padLeft(2, '0'); var str = ''; var length = arguments.length; @@ -5149,13 +6447,13 @@ F.log = function() { } F.path.verify('logs'); - U.queue('F.log', 5, (next) => Fs.appendFile(U.combine(F.config['directory-logs'], filename + '.log'), time + ' | ' + str + '\n', next)); + U.queue('F.log', 5, (next) => Fs.appendFile(U.combine(CONF.directory_logs, filename + '.log'), time + ' | ' + str + '\n', next)); return F; }; -F.logger = function() { - F.datetime = new Date(); - var dt = F.datetime.getFullYear() + '-' + (F.datetime.getMonth() + 1).toString().padLeft(2, '0') + '-' + F.datetime.getDate().toString().padLeft(2, '0') + ' ' + F.datetime.getHours().toString().padLeft(2, '0') + ':' + F.datetime.getMinutes().toString().padLeft(2, '0') + ':' + F.datetime.getSeconds().toString().padLeft(2, '0'); +global.LOGGER = F.logger = function() { + NOW = new Date(); + var dt = NOW.getFullYear() + '-' + (NOW.getMonth() + 1).toString().padLeft(2, '0') + '-' + NOW.getDate().toString().padLeft(2, '0') + ' ' + NOW.getHours().toString().padLeft(2, '0') + ':' + NOW.getMinutes().toString().padLeft(2, '0') + ':' + NOW.getSeconds().toString().padLeft(2, '0'); var str = ''; var length = arguments.length; @@ -5171,11 +6469,11 @@ F.logger = function() { } F.path.verify('logs'); - U.queue('F.logger', 5, (next) => Fs.appendFile(U.combine(F.config['directory-logs'], arguments[0] + '.log'), dt + ' | ' + str + '\n', next)); + U.queue('F.logger', 5, (next) => Fs.appendFile(U.combine(CONF.directory_logs, arguments[0] + '.log'), dt + ' | ' + str + '\n', next)); return F; }; -F.logmail = function(address, subject, body, callback) { +global.LOGMAIL = F.logmail = function(address, subject, body, callback) { if (typeof(body) === FUNCTION) { callback = body; @@ -5187,18 +6485,20 @@ F.logmail = function(address, subject, body, callback) { } if (!subject) - subject = F.config.name + ' v' + F.config.version; + subject = CONF.name + ' v' + CONF.version; - var body = '' + subject + '
' + (typeof(body) === 'object' ? JSON.stringify(body).escape() : body) + '
'; + var body = '' + subject + '
' + (typeof(body) === 'object' ? JSON.stringify(body).escape() : body) + '
'; return F.onMail(address, subject, body, callback); }; F.usage = function(detailed) { + var memory = process.memoryUsage(); var cache = Object.keys(F.cache.items); var resources = Object.keys(F.resources); var controllers = Object.keys(F.controllers); var connections = Object.keys(F.connections); + var schedules = Object.keys(F.schedules); var workers = Object.keys(F.workers); var modules = Object.keys(F.modules); var isomorphic = Object.keys(F.isomorphic); @@ -5208,11 +6508,15 @@ F.usage = function(detailed) { var staticNotfound = Object.keys(F.temporary.notfound); var staticRange = Object.keys(F.temporary.range); var redirects = Object.keys(F.routes.redirects); + var commands = Object.keys(F.commands); var output = {}; + var nosqlcleaner = Object.keys(F.databasescleaner); + var sessions = Object.keys(F.sessions); + var shortcache = Object.keys(F.temporary.shortcache); output.framework = { id: F.id, - datetime: F.datetime, + datetime: NOW, pid: process.pid, node: process.version, version: 'v' + F.version_header, @@ -5222,12 +6526,15 @@ F.usage = function(detailed) { memoryTotal: (memory.heapTotal / 1024 / 1024).floor(2), memoryUsage: (memory.heapUsed / 1024 / 1024).floor(2), memoryRss: (memory.rss / 1024 / 1024).floor(2), - mode: F.config.debug ? 'debug' : 'release', + mode: DEBUG, port: F.port, ip: F.ip, directory: process.cwd() }; + if (CONF.nosql_worker && global.framework_nosql) + output.framework.pidnosql = framework_nosql.pid(); + var keys = Object.keys(U.queuecache); var pending = 0; for (var i = 0, length = keys.length; i < length; i++) @@ -5241,7 +6548,7 @@ F.usage = function(detailed) { cache: cache.length, worker: workers.length, connection: connections.length, - schedule: F.schedules.length, + schedule: schedules.length, helpers: helpers.length, error: F.errors.length, problem: F.problems.length, @@ -5251,7 +6558,11 @@ F.usage = function(detailed) { streaming: staticRange.length, modificator: F.modificators ? F.modificators.length : 0, viewphrases: $VIEWCACHE.length, - uptodates: F.uptodates ? F.uptodates.length : 0 + uptodates: F.uptodates ? F.uptodates.length : 0, + nosqlcleaner: nosqlcleaner.length, + commands: commands.length, + sessions: sessions.length, + shortcache: shortcache.length }; output.routing = { @@ -5260,8 +6571,7 @@ F.usage = function(detailed) { websocket: F.routes.websockets.length, file: F.routes.files.length, middleware: Object.keys(F.routes.middleware).length, - redirect: redirects.length, - mmr: Object.keys(F.routes.mmr).length + redirect: redirects.length }; output.stats = F.stats; @@ -5271,46 +6581,76 @@ F.usage = function(detailed) { return output; output.controllers = []; - - controllers.forEach(function(o) { - var item = F.controllers[o]; - output.controllers.push({ name: o, usage: item.usage ? item.usage() : null }); - }); + for (var i = 0, length = controllers.length; i < length; i++) { + var key = controllers[i]; + var item = F.controllers[key]; + output.controllers.push({ name: key, usage: item.usage ? item.usage() : null }); + } output.connections = []; - - connections.forEach(function(o) { - output.connections.push({ name: o, online: F.connections[o].online }); - }); + for (var i = 0, length = connections.length; i < length; i++) { + var key = connections[i]; + output.connections.push({ name: key, online: F.connections[key].online }); + } output.modules = []; - - modules.forEach(function(o) { - var item = F.modules[o]; - output.modules.push({ name: o, usage: item.usage ? item.usage() : null }); - }); + for (var i = 0, length = modules.length; i < length; i++) { + var key = modules[i]; + var item = F.modules[key]; + output.modules.push({ name: key, usage: item.usage ? item.usage() : null }); + } output.models = []; + for (var i = 0, length = models.length; i < length; i++) { + var key = models[i]; + var item = F.models[key]; + output.models.push({ name: key, usage: item.usage ? item.usage() : null }); + } - models.forEach(function(o) { - var item = F.models[o]; - output.models.push({ name: o, usage: item.usage ? item.usage() : null }); - }); + output.sessions = []; + + for (var i = 0, length = sessions.length; i < length; i++) { + var key = sessions[i]; + var item = F.sessions[key]; + output.sessions.push({ name: key, usage: item.usage() }); + } - output.uptodates = F.uptodates; - output.helpers = helpers; output.cache = cache; - output.resources = resources; - output.errors = F.errors; - output.problems = F.problems; output.changes = F.changes; - output.traces = F.traces; + output.errors = F.errors; output.files = staticFiles; - output.streaming = staticRange; + output.helpers = helpers; + output.nosqlcleaner = nosqlcleaner; output.other = Object.keys(F.temporary.other); + output.problems = F.problems; + output.resources = resources; + output.commands = commands; + output.streaming = staticRange; + output.traces = F.traces; + output.uptodates = F.uptodates; + output.shortcache = shortcache; + return output; }; +F.onPrefSave = function(val) { + Fs.writeFile(F.path.databases(PREFFILE), JSON.stringify(val), ERROR('F.onPrefSave')); +}; + +F.onPrefLoad = function(next) { + Fs.readFile(U.combine(CONF.directory_databases, PREFFILE), function(err, data) { + if (data) + next(data.toString('utf8').parseJSON(true)); + else + next(); + }); +}; + +DEF.onAudit = F.onAudit = function(name, data) { + F.path.verify('logs'); + U.queue('F.logger', 5, (next) => Fs.appendFile(U.combine(CONF.directory_logs, name + '.log'), JSON.stringify(data) + '\n', next)); +}; + /** * Compiles content in the view @{compile}...@{end}. The function has controller context, this === controler. * @param {String} name @@ -5318,7 +6658,8 @@ F.usage = function(detailed) { * @param {Object} model * @return {String} */ -F.onCompileView = function(name, html, model) { +// name, html, model +F.onCompileView = function(name, html) { return html; }; @@ -5356,15 +6697,16 @@ function compile_file(res) { F.path.verify('temp'); Fs.writeFileSync(file, compile_content(req.extension, framework_internal.parseBlock(F.routes.blocks[uri.pathname], buffer.toString(ENCODING)), res.options.filename), ENCODING); var stats = Fs.statSync(file); - var tmp = F.temporary.path[req.$key] = [file, stats.size, stats.mtime.toUTCString()]; - compile_gzip(tmp, function() { + var tmp = [file, stats.size, stats.mtime.toUTCString()]; + compile_gzip(tmp, function(tmp) { + F.temporary.path[req.$key] = tmp; delete F.temporary.processing[req.$key]; res.$file(); }); }); } -function compile_merge(res) { +function compile_merge(res, repeated) { var req = res.req; var uri = req.uri; @@ -5372,28 +6714,18 @@ function compile_merge(res) { var merge = F.routes.merge[uri.pathname]; var filename = merge.filename; - if (!F.config.debug && existsSync(filename)) { + if (!DEBUG && existsSync(filename)) { var stats = Fs.statSync(filename); - var tmp = F.temporary.path[req.$key] = [filename, stats.size, stats.mtime.toUTCString()]; - compile_gzip(tmp, function() { + var tmp = [filename, stats.size, stats.mtime.toUTCString()]; + compile_gzip(tmp, function(tmp) { delete F.temporary.processing[req.$key]; + F.temporary.path[req.$key] = tmp; res.$file(); }); return; } var writer = Fs.createWriteStream(filename); - - writer.on('finish', function() { - var stats = Fs.statSync(filename); - var tmp = F.temporary.path[req.$key] = [filename, stats.size, stats.mtime.toUTCString()]; - this.destroy && this.destroy(); - compile_gzip(tmp, function() { - delete F.temporary.processing[req.$key]; - res.$file(); - }); - }); - var index = 0; var remove = null; @@ -5413,7 +6745,7 @@ function compile_merge(res) { var output = compile_content(req.extension, framework_internal.parseBlock(block, data), filename); - if (req.extension === 'js') { + if (JSFILES[req.extension]) { if (output[output.length - 1] !== ';') output += ';'; } else if (req.extension === 'html') { @@ -5463,7 +6795,7 @@ function compile_merge(res) { } var output = compile_content(req.extension, framework_internal.parseBlock(block, buffer.toString(ENCODING)), filename); - if (req.extension === 'js') { + if (JSFILES[req.extension]) { if (output[output.length - 1] !== ';') output += ';' + NEWLINE; } else if (req.extension === 'html') { @@ -5478,6 +6810,35 @@ function compile_merge(res) { }, function() { + CLEANUP(writer, function() { + + var stats; + + try { + stats = Fs.statSync(filename); + } catch (e) { + + e && F.error(e, 'compile_merge' + (repeated ? ' - repeated' : ''), req.url); + + // Try it again + if (repeated) { + delete F.temporary.processing[req.$key]; + if (!F.routes.filesfallback || !F.routes.filesfallback(req, res)) + res.throw404(); + } else + compile_merge(res, true); + + return; + } + + var tmp = [filename, stats.size, stats.mtime.toUTCString()]; + compile_gzip(tmp, function(tmp) { + F.temporary.path[req.$key] = tmp; + delete F.temporary.processing[req.$key]; + res.$file(); + }); + }); + writer.end(); // Removes all directories from merge list (because the files are added into the queue) @@ -5492,16 +6853,16 @@ function compile_merge(res) { function merge_debug_writer(writer, filename, extension, index, block) { var plus = '==========================================================================================='; - var beg = extension === 'js' ? '/*\n' : extension === 'css' ? '/*!\n' : ''; + var beg = JSFILES[extension] ? '/*\n' : extension === 'css' ? '/*!\n' : ''; var mid = extension !== 'html' ? ' * ' : ' '; writer.write((index > 0 ? '\n\n' : '') + beg + mid + plus + '\n' + mid + 'MERGED: ' + filename + '\n' + (block ? mid + 'BLOCKS: ' + block + '\n' : '') + mid + plus + end + '\n\n', ENCODING); } function component_debug(filename, value, extension) { var plus = '==========================================================================================='; - var beg = extension === 'js' ? '/*\n' : extension === 'css' ? '/*!\n' : ''; + var beg = JSFILES[extension] ? '/*\n' : extension === 'css' ? '/*!\n' : ''; var mid = extension !== 'html' ? ' * ' : ' '; return beg + mid + plus + '\n' + mid + 'COMPONENT: ' + filename + '\n' + mid + plus + end + '\n\n' + value; } @@ -5509,7 +6870,7 @@ function component_debug(filename, value, extension) { F.compile_virtual = function(res) { var req = res.req; - var tmpname = res.options.filename.replace(F.config['directory-public'], F.config['directory-public-virtual']); + var tmpname = res.options.filename.replace(CONF.directory_public, CONF.directory_public_virtual); if (tmpname === res.options.filename) { F.temporary.notfound[req.$key] = true; @@ -5527,11 +6888,20 @@ F.compile_virtual = function(res) { return; } - if (!res.noCompress && (req.extension === 'js' || req.extension === 'css') && F.config['allow-compile'] && !REG_NOCOMPRESS.test(res.options.filename)) { + if (!res.noCompress && COMPRESSIONSPECIAL[req.extension] && CONF.allow_compile && !REG_NOCOMPRESS.test(res.options.filename)) { res.options.filename = tmpname; - compile_file(res); + return compile_file(res); + } + + var tmp = [tmpname, size, stats.mtime.toUTCString()]; + if (CONF.allow_gzip && COMPRESSION[U.getContentType(req.extension)]) { + compile_gzip(tmp, function(tmp) { + F.temporary.path[req.$key] = tmp; + delete F.temporary.processing[req.$key]; + res.$file(); + }); } else { - F.temporary.path[req.$key] = [tmpname, size, stats.mtime.toUTCString()]; + F.temporary.path[req.$key] = tmp; delete F.temporary.processing[req.$key]; res.$file(); } @@ -5547,23 +6917,25 @@ function compile_check(res) { if (F.routes.merge[uri.pathname]) { compile_merge(res); - return F; + return; } fsFileExists(res.options.filename, function(e, size, sfile, stats) { if (e) { - if (!res.noCompress && (req.extension === 'js' || req.extension === 'css') && F.config['allow-compile'] && !REG_NOCOMPRESS.test(res.options.filename)) + if (!res.noCompress && COMPRESSIONSPECIAL[req.extension] && CONF.allow_compile && !REG_NOCOMPRESS.test(res.options.filename)) return compile_file(res); - var tmp = F.temporary.path[req.$key] = [res.options.filename, size, stats.mtime.toUTCString()]; - if (F.config['allow-gzip'] && COMPRESSION[U.getContentType(req.extension)]) { - compile_gzip(tmp, function() { + var tmp = [res.options.filename, size, stats.mtime.toUTCString()]; + if (CONF.allow_gzip && COMPRESSION[U.getContentType(req.extension)]) { + compile_gzip(tmp, function(tmp) { + F.temporary.path[req.$key] = tmp; res.$file(); delete F.temporary.processing[req.$key]; }); } else { + F.temporary.path[req.$key] = tmp; res.$file(); delete F.temporary.processing[req.$key]; } @@ -5585,13 +6957,14 @@ function compile_gzip(arr, callback) { var filename = F.path.temp('file' + arr[0].hash().toString().replace('-', '0') + '.gz'); arr.push(filename); + F.stats.performance.open++; var reader = Fs.createReadStream(arr[0]); var writer = Fs.createWriteStream(filename); CLEANUP(writer, function() { fsFileExists(filename, function(e, size) { arr.push(size); - callback(); + callback(arr); }); }); @@ -5606,16 +6979,17 @@ function compile_content(extension, content, filename) { switch (extension) { case 'js': - return F.config['allow-compile-script'] ? framework_internal.compile_javascript(content, filename) : content; + case 'mjs': + return CONF.allow_compile_script ? framework_internal.compile_javascript(content, filename) : content; case 'css': - content = F.config['allow-compile-style'] ? framework_internal.compile_css(content, filename) : content; + content = CONF.allow_compile_style ? framework_internal.compile_css(content, filename) : content; var matches = content.match(REG_COMPILECSS); if (matches) { for (var i = 0, length = matches.length; i < length; i++) { var key = matches[i]; var url = key.substring(4, key.length - 1); - content = content.replace(key, 'url(' + F.$version(url) + ')'); + content = content.replace(key, 'url(' + F.$version(url, true) + ')'); } } return content; @@ -5624,18 +6998,11 @@ function compile_content(extension, content, filename) { return content; } -// OBSOLETE -F.responseStatic = function(req, res, done) { - res.options.callback = done; - res.continue(); - return F; -}; - F.restore = function(filename, target, callback, filter) { - var buffer_key = U.createBuffer(':'); - var buffer_new = U.createBuffer('\n'); - var buffer_dir = U.createBuffer('#'); + var buffer_key = Buffer.from(':'); + var buffer_new = Buffer.from('\n'); + var buffer_dir = Buffer.from('#'); var cache = {}; var data = null; var type = 0; @@ -5659,7 +7026,7 @@ F.restore = function(filename, target, callback, filter) { index++; item = data.slice(0, index - 1).toString('utf8').trim(); - data = data.slice(index); + data = data.slice(index + (data[index] === 32 ? 1 : 0)); type = 1; parser.next(); }; @@ -5683,7 +7050,10 @@ F.restore = function(filename, target, callback, filter) { cache[path] = true; var npath = path.substring(0, path.lastIndexOf(F.isWindows ? '\\' : '/')); - if (!filter || filter(item, false) !== false) + + var filename = filter && filter(item, false); + + if (!filter || filename || filename == null) F.path.mkdir(npath); else { type = 5; // skip @@ -5692,6 +7062,9 @@ F.restore = function(filename, target, callback, filter) { } } + if (typeof(filename) === 'string') + path = Path.join(target, filename); + // File type = 2; var tmp = open[item] = {}; @@ -5704,6 +7077,16 @@ F.restore = function(filename, target, callback, filter) { output.count++; + tmp.zlib.on('error', function(e) { + pending--; + var tmp = this.$self; + tmp.writer.end(); + tmp.writer = null; + tmp.zlib = null; + delete open[tmp.name]; + F.error(e, 'bundling', path); + }); + tmp.zlib.on('data', function(chunk) { this.$self.writer.write(chunk); }); @@ -5741,15 +7124,15 @@ F.restore = function(filename, target, callback, filter) { if (type) { var remaining = data.length % 4; if (remaining) { - open[item].zlib.write(U.createBuffer(data.slice(0, data.length - remaining).toString('ascii'), 'base64')); + open[item].zlib.write(Buffer.from(data.slice(0, data.length - remaining).toString('ascii'), 'base64')); data = data.slice(data.length - remaining); skip = true; } else { - open[item].zlib.write(U.createBuffer(data.toString('ascii'), 'base64')); + open[item].zlib.write(Buffer.from(data.toString('ascii'), 'base64')); data = null; } } else { - open[item].zlib.end(U.createBuffer(data.slice(0, index).toString('ascii'), 'base64')); + open[item].zlib.end(Buffer.from(data.slice(0, index).toString('ascii'), 'base64')); data = data.slice(index + 1); } @@ -5836,14 +7219,36 @@ F.backup = function(filename, filelist, callback, filter) { return a.localeCompare(b); }); + var clean = function(path, files) { + var index = 0; + while (true) { + var filename = files[index]; + if (!filename) + break; + if (filename.substring(0, path.length) === path) { + files.splice(index, 1); + continue; + } else + index++; + } + }; + var writer = Fs.createWriteStream(filename); + writer.on('finish', function() { + callback && Fs.stat(filename, (e, stat) => callback(null, { filename: filename, files: counter, size: stat.size })); + }); + filelist.wait(function(item, next) { + var file = Path.join(path, item); + + if (F.isWindows) + item = item.replace(/\\/g, '/'); + if (item[0] !== '/') item = '/' + item; - var file = Path.join(path, item); Fs.stat(file, function(err, stats) { if (err) { @@ -5852,12 +7257,27 @@ F.backup = function(filename, filelist, callback, filter) { } if (stats.isDirectory()) { - var dir = item.replace(/\\/g, '/') + '/'; + var dir = item.replace(/\\/g, '/'); + + if (dir[dir.length - 1] !== '/') + dir += '/'; + if (filter && !filter(dir, true)) return next(); + U.ls(file, function(f, d) { + var length = path.length; + if (path[path.length - 1] === '/') + length--; + d.wait(function(item, next) { + + if (filter && !filter(item.substring(length), true)) { + clean(item, f); + return next(); + } + writer.write(item.substring(length).padRight(padding) + ':#\n', 'utf8'); next(); }, function() { @@ -5869,10 +7289,12 @@ F.backup = function(filename, filelist, callback, filter) { return; } - var data = U.createBufferSize(0); + if (filter && !filter(file.substring(path.length - 1), false)) + return next(); + var data = Buffer.alloc(0); writer.write(item.padRight(padding) + ':'); - CLEANUP(Fs.createReadStream(file).pipe(Zlib.createGzip(GZIPFILE)).on('data', function(chunk) { + Fs.createReadStream(file).pipe(Zlib.createGzip(GZIPFILE)).on('data', function(chunk) { CONCAT[0] = data; CONCAT[1] = chunk; @@ -5884,17 +7306,15 @@ F.backup = function(filename, filelist, callback, filter) { data = data.slice(data.length - remaining); } - }), function() { + }).on('end', function() { data.length && writer.write(data.toString('base64')); writer.write('\n', 'utf8'); counter++; - next(); + setImmediate(next); }); }); - }, function() { - callback && Fs.stat(filename, (e, stat) => callback(null, { filename: filename, files: counter, size: stat.size })); - }); + }, () => writer.end()); }); return F; @@ -5909,9 +7329,7 @@ F.exists = function(req, res, max, callback) { var name = req.$key = createTemporaryKey(req); var filename = F.path.temp(name); - var httpcachevalid = false; - - RELEASE && (req.headers['if-none-match'] === ETAG + F.config['etag-version']) && (httpcachevalid = true); + var httpcachevalid = RELEASE && (req.headers['if-none-match'] === (ETAG + CONF.etag_version)); if (F.isProcessed(name) || httpcachevalid) { res.options.filename = filename; @@ -5967,33 +7385,10 @@ F.isProcessing = function(filename) { if (index !== -1) name = name.substring(0, index); - filename = U.combine(F.config['directory-public'], $decodeURIComponent(name)); + filename = U.combine(CONF.directory_public, $decodeURIComponent(name)); return !!F.temporary.processing[filename]; }; -/** - * Disable HTTP cache for current request/response - * @param {Request} req Request - * @param {Response} res (optional) Response - * @return {Framework} - */ -F.noCache = function(req) { - OBSOLETE('F.noCache()', 'Use req.noCache() or res.noCache() --> they have same functionality.'); - req.noCache(); - return F; -}; - -// OBSOLETE -F.responseFile = function(req, res, filename, downloadName, headers, done, key) { - res.$key = key; - res.options.filename = filename; - res.options.download = downloadName; - res.options.headers = headers; - res.options.callback = done; - res.$file(); - return F; -}; - /** * Clears file information in release mode * @param {String/Request} url @@ -6011,291 +7406,174 @@ F.touch = function(url) { return F; }; -// OBSOLETE -F.responsePipe = function(req, res, url, headers, timeout, callback) { - res.pipe(url, headers, timeout, callback); +F.response503 = function(req, res) { + res.options.code = 503; + res.options.headers = HEADERS.response503; + res.options.body = VIEW('.' + PATHMODULES + res.options.code, F.waits); + res.$text(); return F; }; -// OBSOLETE -F.responseCustom = function(req, res) { - res.$custom(); - return F; -}; +global.LOAD = F.load = function(debug, types, pwd, ready) { -// OBSOLETE -F.responseImage = function(req, res, filename, make, headers, done) { + if (typeof(types) === 'function') { + ready = types; + types = null; + } - if (typeof(filename) === 'object') - res.options.stream = filename; - else - res.options.filename = filename; + if (typeof(pwd) === 'function') { + ready = pwd; + pwd = null; + } - res.options.headers = headers; - res.options.callback = done; - res.options.make = make; - res.$image(); - return F; -}; + if (!types) + types = ['nobundles', 'nopackages', 'nocomponents', 'nothemes']; -// OBSOLETE -F.responseImageWithoutCache = function(req, res, filename, make, headers, done) { + if (pwd && pwd[0] === '.' && pwd.length < 4) + F.directory = directory = U.$normalize(Path.normalize(directory + '/..')); + else if (pwd) + F.directory = directory = U.$normalize(pwd); + else if (process.env.istotaljsworker) + F.directory = process.cwd(); + else if ((/\/scripts\/.*?.js/).test(process.argv[1])) + F.directory = directory = U.$normalize(Path.normalize(directory + '/..')); - if (typeof(filename) === 'object') - res.options.stream = filename; - else - res.options.filename = filename; + if (typeof(debug) === 'string') { + switch (debug.toLowerCase().replace(/\.|\s/g, '-')) { + case 'release': + case 'production': + debug = false; + break; - res.options.headers = headers; - res.options.callback = done; - res.options.make = make; - res.options.cache = false; - res.$image(); -}; - -// OBSOLETE -F.responseStream = function(req, res, type, stream, download, headers, done, nocompress) { - res.options.type = type; - res.options.stream = stream; - res.options.download = download; - res.options.headers = headers; - res.options.compress = nocompress ? false : true; - res.options.callback = done; - res.$stream(); - return F; -}; - -// OBSOLETE -F.responseBinary = function(req, res, type, buffer, encoding, download, headers, done) { - res.options.type = type; - res.options.body = buffer; - res.options.encoding = encoding; - res.options.download = download; - res.options.headers = headers; - res.options.callback = done; - res.$binary(); - return F; -}; - -F.setModified = function(req, res, value) { - if (typeof(value) === 'string') - res.setHeader('Etag', value + F.config['etag-version']); - else - res.setHeader('Last-Modified', value.toUTCString()); - return F; -}; + case 'debug': + case 'develop': + case 'development': + debug = true; + break; -F.notModified = function(req, res, compare, strict) { + case 'test-debug': + case 'debug-test': + case 'testing-debug': + debug = true; + F.isTest = true; + break; - var type = typeof(compare); - if (type === 'boolean') { - var tmp = compare; - compare = strict; - strict = tmp; - type = typeof(compare); + case 'test': + case 'testing': + case 'test-release': + case 'release-test': + case 'testing-release': + case 'test-production': + case 'testing-production': + debug = false; + F.isTest = true; + break; + } } - var isEtag = type === 'string'; - var val = req.headers[isEtag ? 'if-none-match' : 'if-modified-since']; + F.isWorker = true; + F.isDebug = debug; - if (isEtag) { - if (val !== (compare + F.config['etag-version'])) - return false; - } else { + global.isWORKER = true; + global.DEBUG = debug; + global.RELEASE = !debug; + global.I = global.isomorphic = F.isomorphic; - if (!val) - return false; + var isNo = true; - var date = compare === undefined ? new Date().toUTCString() : compare.toUTCString(); - if (strict) { - if (new Date(Date.parse(val)) === new Date(date)) - return false; - } else { - if (new Date(Date.parse(val)) < new Date(date)) - return false; + if (types) { + for (var i = 0; i < types.length; i++) { + if (types[i].substring(0, 2) !== 'no') { + isNo = false; + break; + } } } + var can = function(type) { + if (!types) + return true; + if (types.indexOf('no' + type) !== -1) + return false; + return isNo ? true : types.indexOf(type) !== -1; + }; - var headers; - - if (isEtag) { - headers = HEADERS.notModifiedEtag; - headers['Etag'] = val; - } else { - headers = HEADERS.notModifiedLastModifiedDate; - headers['Last-Modified'] = val; - } - - res.success = true; - res.writeHead(304, headers); - res.end(); - - F.stats.response.notModified++; - response_end(res); - return true; -}; - -F.responseCode = function(req, res, code, problem) { - res.options.code = code; - problem && (res.options.problem = problem); - res.$throw(); - return F; -}; - -F.response400 = function(req, res, problem) { - res.options.code = 400; - problem && (res.options.problem = problem); - res.$throw(); - return F; -}; - -F.response401 = function(req, res, problem) { - res.options.code = 401; - problem && (res.options.problem = problem); - res.$throw(); - return F; -}; - -F.response403 = function(req, res, problem) { - res.options.code = 403; - problem && (res.options.problem = problem); - res.$throw(); - return F; -}; - -F.response404 = function(req, res, problem) { - res.options.code = 404; - problem && (res.options.problem = problem); - res.$throw(); - return F; -}; - -F.response408 = function(req, res, problem) { - res.options.code = 408; - problem && (res.options.problem = problem); - res.$throw(); - return F; -}; - -F.response431 = function(req, res, problem) { - res.options.code = 431; - problem && (res.options.problem = problem); - res.$throw(); - return F; -}; - -F.response500 = function(req, res, error) { - res.throw500(error); - return F; -}; + F.$bundle(function() { + F.consoledebug('startup'); + F.$startup(function() { -F.response501 = function(req, res, problem) { - res.options.code = 501; - problem && (res.options.problem = problem); - res.$throw(); - return F; -}; + F.consoledebug('startup (done)'); + F.$configure_env(); + F.$configure_configs(); -F.response503 = function(req, res) { - var keys = ''; - for (var m in F.waits) - keys += (res.options.body ? ', ' : '') + '' + m + ''; - res.options.code = 503; - res.options.headers = HEADERS.response503; - res.options.body = '
Please wait (10) for ' + (F.config.name + ' v' + F.config.version) + ' application.
The application is waiting for: ' + keys + '.'; - res.$throw(); - return F; -}; + if (can('versions')) + F.$configure_versions(); -// OBSOLETE -F.responseContent = function(req, res, code, body, type, compress, headers) { - res.options.code = code; - res.options.body = body; - res.options.type = type; - res.options.compress = compress === undefined || compress === true; - res.options.headers = headers; - res.$text(); - return F; -}; + if (can('workflows')) + F.$configure_workflows(); -// OBSOLETE -F.responseRedirect = function(req, res, url, permanent) { - res.options.url = url; - res.options.permanent = permanent; - res.$redirect(); - return F; -}; + if (can('sitemap')) + F.$configure_sitemap(); -F.load = function(debug, types, pwd) { + F.consoledebug('init'); - if (pwd && pwd[0] === '.' && pwd.length < 4) - F.directory = directory = U.$normalize(Path.normalize(directory + '/..')); - else if (pwd) - F.directory = directory = U.$normalize(pwd); + var noservice = true; - if (debug === 'release') - debug = false; - else if (debug === 'debug') - debug = true; + for (var i = 0; i < types.length; i++) { + switch(types[i]) { + case 'service': + case 'services': + noservice = false; + break; + } + if (!noservice) + break; + } - F.isWorker = true; - F.config.debug = debug; - F.isDebug = debug; + F.cache.init(noservice); + EMIT('init'); - global.DEBUG = debug; - global.RELEASE = !debug; - global.I = global.isomorphic = F.isomorphic; + F.$load(types, directory, function() { - F.consoledebug('startup'); - F.$startup(function() { + F.isLoaded = true; - F.consoledebug('startup (done)'); - F.$configure_configs(); + process.send && process.send('total:ready'); - if (!types || types.indexOf('versions') !== -1) - F.$configure_versions(); + setTimeout(function() { - if (!types || types.indexOf('workflows') !== -1) - F.$configure_workflows(); + try { + EMIT('load'); + EMIT('ready'); + } catch (err) { + F.error(err, 'ON("load/ready")'); + } - if (!types || types.indexOf('sitemap') !== -1) - F.$configure_sitemap(); + ready && ready(); - F.consoledebug('init'); - F.cache.init(); - F.emit('init'); + F.removeAllListeners('load'); + F.removeAllListeners('ready'); - F.$load(types, directory, function() { + if (F.isTest) { + F.console(); + F.test(); + return F; + } - F.isLoaded = true; - process.send && process.send('total:ready'); + // Because this is worker + // setTimeout(function() { + // if (!F.isTest) + // delete F.test; + // }, 5000); - setTimeout(function() { + }, 500); - try { - F.emit('load', F); - F.emit('ready', F); - } catch (err) { - F.error(err, 'F.on("load/ready")'); + if (CONF.allow_debug) { + F.consoledebug('done'); + F.usagesnapshot(); } - - F.removeAllListeners('load'); - F.removeAllListeners('ready'); - - // clear unnecessary items - delete F.tests; - delete F.test; - delete F.testing; - delete F.assert; - }, 500); - - if (F.config['allow-debug']) { - F.consoledebug('done'); - F.usagesnapshot(); - } + }); }); - }); + }, can('bundles')); return F; }; @@ -6307,7 +7585,7 @@ F.load = function(debug, types, pwd) { * @param {Object} options * @return {Framework} */ -F.initialize = function(http, debug, options, restart) { +F.initialize = function(http, debug, options) { if (!options) options = {}; @@ -6316,10 +7594,13 @@ F.initialize = function(http, debug, options, restart) { var ip = options.ip; var listenpath = options.listenpath; - options.config && U.extend(F.config, options.config, true); + if (options.thread) + global.THREAD = options.thread; - if (options.debug || options['allow-debug']) - F.config['allow-debug'] = true; + options.config && U.extend_headers2(CONF, options.config); + + if (options.debug || options['allow-debug'] || options.allow_debug) + CONF.allow_debug = true; F.isHTTPS = http.STATUS_CODES === undefined; @@ -6329,128 +7610,156 @@ F.initialize = function(http, debug, options, restart) { if (options.id) F.id = options.id; - F.config.debug = debug; F.isDebug = debug; + if (options.bundling != null) + F.$bundling = options.bundling == true; + global.DEBUG = debug; global.RELEASE = !debug; global.I = global.isomorphic = F.isomorphic; - F.$configure_configs(); - F.$configure_versions(); - F.$configure_workflows(); - F.$configure_sitemap(); - F.isTest && F.$configure_configs('config-test', true); - F.cache.init(); - F.consoledebug('init'); - F.emit('init'); - - if (!port) { - if (F.config['default-port'] === 'auto') { - var envPort = +(process.env.PORT || ''); - if (!isNaN(envPort)) - port = envPort; - } else - port = F.config['default-port']; + if (options.tests) { + F.testlist = options.tests; + for (var i = 0; i < F.testlist.length; i++) + F.testlist[i] = F.testlist[i].replace(/\.js$/, ''); } - F.port = port || 8000; + F.$bundle(function() { - if (ip !== null) { - F.ip = ip || F.config['default-ip'] || '0.0.0.0'; - if (F.ip === 'null' || F.ip === 'undefined' || F.ip === 'auto') - F.ip = null; - } else - F.ip = undefined; + F.$configure_env(); + F.$configure_configs(); + F.$configure_versions(); + F.$configure_workflows(); + F.$configure_sitemap(); + F.isTest && F.$configure_configs('config-test', true); + F.cache.init(); + F.consoledebug('init'); + EMIT('init'); - if (F.ip == null) - F.ip = '0.0.0.0'; + if (!port) { + if (CONF.default_port === 'auto') { + var envPort = +(process.env.PORT || ''); + if (!isNaN(envPort)) + port = envPort; + } else + port = CONF.default_port; + } - !listenpath && (listenpath = F.config['default-listenpath']); - F.listenpath = listenpath; + F.port = port || 8000; - if (F.server) { - F.server.removeAllListeners(); - Object.keys(F.connections).forEach(function(key) { - var item = F.connections[key]; - if (item) { - item.removeAllListeners(); - item.close(); - } - }); + if (ip !== null) { + F.ip = ip || CONF.default_ip || '0.0.0.0'; + if (F.ip === 'null' || F.ip === 'undefined' || F.ip === 'auto') + F.ip = null; + } else + F.ip = undefined; - F.server.close(); - } + if (F.ip == null) + F.ip = '0.0.0.0'; - var listen = function() { + !listenpath && (listenpath = CONF.default_listenpath); + F.listenpath = listenpath; - if (options.https) - F.server = http.createServer(options.https, F.listener); - else - F.server = http.createServer(F.listener); + if (F.server) { + F.server.removeAllListeners(); + Object.keys(F.connections).forEach(function(key) { + var item = F.connections[key]; + if (item) { + item.removeAllListeners(); + item.close(); + } + }); - F.config['allow-performance'] && F.server.on('connection', connection_tunning); - F.initwebsocket && F.initwebsocket(); - F.consoledebug('HTTP listening'); + F.server.close(); + } - if (listenpath) - F.server.listen(listenpath); - else - F.server.listen(F.port, F.ip); - }; + var listen = function() { + + if (options.https) { - // clears static files - F.consoledebug('clear temporary'); - F.clear(function() { - F.consoledebug('clear temporary (done)'); - F.$load(undefined, directory, function() { + var meta = options.https; - F.isLoaded = true; - process.send && process.send('total:ready'); + if (typeof(meta.key) === 'string') { + if (meta.key.indexOf('.') === -1) + meta.key = Buffer.from(meta.key, 'base64'); + else + meta.key = Fs.readFileSync(meta.key); + } + + if (typeof(meta.cert) === 'string') { + if (meta.cert.indexOf('.') === -1) + meta.cert = Buffer.from(meta.cert, 'base64'); + else + meta.cert = Fs.readFileSync(meta.cert); + } + + F.server = http.createServer(meta, F.listener); + + } else + F.server = http.createServer(F.listener); - if (options.middleware) - options.middleware(listen); + CONF.allow_performance && F.server.on('connection', connection_tunning); + F.initwebsocket && F.initwebsocket(); + F.consoledebug('HTTP listening'); + + if (listenpath) + F.server.listen(listenpath); else - listen(); + F.server.listen(F.port, F.ip); + }; - if (F.config['allow-debug']) { - F.consoledebug('done'); - F.usagesnapshot(); - } + // clears static files + F.consoledebug('clear temporary'); + F.clear(function() { - if (!process.connected || restart) - F.console(); + F.consoledebug('clear temporary (done)'); + F.$load(undefined, directory, function() { - setTimeout(function() { + F.isLoaded = true; + process.send && process.send('total:ready'); - try { - F.emit('load', F); - F.emit('ready', F); - } catch (err) { - F.error(err, 'F.on("load/ready")'); + if (options.middleware) + options.middleware(listen); + else + listen(); + + if (CONF.allow_debug) { + F.consoledebug('done'); + F.usagesnapshot(); } - F.removeAllListeners('load'); - F.removeAllListeners('ready'); - options.package && INSTALL('package', options.package); - }, 500); + if (!process.connected) + F.console(); - if (F.isTest) { - var sleep = options.sleep || options.delay || 1000; - setTimeout(() => F.test(true, options.tests || options.test), sleep); - return F; - } + setTimeout(function() { - setTimeout(function() { - if (F.isTest) - return; - delete F.tests; - delete F.test; - delete F.testing; - delete F.assert; - }, 5000); - }); - }, true); + try { + EMIT('load'); + EMIT('ready'); + } catch (err) { + F.error(err, 'ON("load/ready")'); + } + + F.removeAllListeners('load'); + F.removeAllListeners('ready'); + options.package && INSTALL('package', options.package); + runsnapshot(); + }, 500); + + if (F.isTest) { + var sleep = options.sleep || options.delay || 1000; + setTimeout(F.test, sleep); + return F; + } + + setTimeout(function() { + if (!F.isTest) + delete F.test; + }, 5000); + }); + }, true); + }); return F; }; @@ -6478,9 +7787,15 @@ F.http = function(mode, options, middleware) { options == null && (options = {}); !options.port && (options.port = +process.argv[2]); + if (options.port && isNaN(options.port)) + options.port = 0; + if (typeof(middleware) === 'function') options.middleware = middleware; + if (options.bundling != null) + F.$bundling = options.bundling; + var http = require('http'); extend_request(http.IncomingMessage.prototype); extend_response(http.ServerResponse.prototype); @@ -6506,6 +7821,9 @@ F.https = function(mode, options, middleware) { options == null && (options = {}); !options.port && (options.port = +process.argv[2]); + if (options.port && isNaN(options.port)) + options.port = 0; + if (typeof(middleware) === 'function') options.middleware = middleware; @@ -6528,8 +7846,8 @@ F.mode = function(http, name, options) { debug = true; break; } - F.config.debug = debug; - F.config.trace = debug; + DEBUG = debug; + CONF.trace = debug; F.isDebug = debug; global.DEBUG = debug; global.RELEASE = !debug; @@ -6568,17 +7886,11 @@ F.mode = function(http, name, options) { break; } - var restart = false; - if (F.temporary.init) - restart = true; - else - F.temporary.init = { name: name, isHTTPS: typeof(http.STATUS_CODES) === 'undefined', options: options }; - - F.config.trace = debug; + CONF.trace = debug; F.consoledebug('startup'); F.$startup(function() { F.consoledebug('startup (done)'); - F.initialize(http, debug, options, restart); + F.initialize(http, debug, options); }); return F; }; @@ -6623,47 +7935,73 @@ F.custom = function(mode, http, request, response, options) { break; } - var restart = false; - if (F.temporary.init) - restart = true; - else - F.temporary.init = { name: mode, isHTTPS: false, options: options }; - - F.config.trace = debug; + CONF.trace = debug; F.consoledebug('startup'); F.$startup(function() { F.consoledebug('startup (done)'); - F.initialize(http, debug, options, restart); + F.initialize(http, debug, options); }); return F; }; F.console = function() { + var memory = process.memoryUsage(); console.log('===================================================='); - console.log('PID : ' + process.pid); - console.log('Node.js : ' + process.version); - console.log('Total.js : v' + F.version_header); - console.log('OS : ' + Os.platform() + ' ' + Os.release()); + console.log('PID : ' + process.pid); + console.log('Node.js : ' + process.version); + console.log('Total.js : v' + F.version_header); + console.log('OS : ' + Os.platform() + ' ' + Os.release()); + + // Removed worker in v4 + CONF.nosql_worker && global.framework_nosql && console.log('NoSQL PID : ' + global.framework_nosql.pid()); + + console.log('Memory : ' + memory.heapUsed.filesize(2) + ' / ' + memory.heapTotal.filesize(2)); + console.log('User : ' + Os.userInfo().username); console.log('===================================================='); - console.log('Name : ' + F.config.name); - console.log('Version : ' + F.config.version); - console.log('Author : ' + F.config.author); - console.log('Date : ' + F.datetime.format('yyyy-MM-dd HH:mm:ss')); - console.log('Mode : ' + (F.config.debug ? 'debug' : 'release')); + console.log('Name : ' + CONF.name); + console.log('Version : ' + CONF.version); + CONF.author && console.log('Author : ' + CONF.author); + console.log('Date : ' + NOW.format('yyyy-MM-dd HH:mm:ss')); + console.log('Mode : ' + (DEBUG ? 'debug' : 'release')); + global.THREAD && console.log('Thread : ' + global.THREAD); + console.log('===================================================='); + CONF.default_root && console.log('Root : ' + F.config.default_root); + console.log('Directory : ' + process.cwd()); + console.log('node_modules : ' + PATHMODULES); console.log('====================================================\n'); - console.log('{2}://{0}:{1}/'.format(F.ip, F.port, F.isHTTPS ? 'https' : 'http')); - console.log(''); + + if (!F.isWorker) { + + var hostname = '{2}://{0}:{1}/'.format(F.ip, F.port, F.isHTTPS ? 'https' : 'http'); + + if (F.ip === '0.0.0.0') { + var ni = Os.networkInterfaces(); + if (ni.en0) { + for (var i = 0; i < ni.en0.length; i++) { + var nii = ni.en0[i]; + // nii.family === 'IPv6' || + if (nii.family === 'IPv4') { + hostname += '\n{2}://{0}:{1}/'.format(nii.address, F.port, F.isHTTPS ? 'https' : 'http'); + break; + } + } + } + } + + console.log(hostname); + console.log(''); + } }; F.usagesnapshot = function(filename) { - Fs.writeFile(filename || F.path.root('usage.log'), JSON.stringify(F.usage(true), null, ' '), NOOP); + Fs.writeFile(filename || F.path.root('usage' + (F.id ? ('-' + F.id) : '') + '.log'), JSON.stringify(F.usage(true), null, '\t'), NOOP); return F; }; F.consoledebug = function() { - if (!F.config['allow-debug']) + if (!CONF.allow_debug) return F; var arr = [new Date().format('yyyy-MM-dd HH:mm:ss'), '--------->']; @@ -6678,10 +8016,10 @@ F.consoledebug = function() { * @return {Framework} */ F.reconnect = function() { - if (F.config['default-port'] !== undefined) - F.port = F.config['default-port']; - if (F.config['default-ip'] !== undefined) - F.ip = F.config['default-ip']; + if (CONF.default_port !== undefined) + F.port = CONF.default_port; + if (CONF.default_ip !== undefined) + F.ip = CONF.default_ip; F.server.close(() => F.server.listen(F.port, F.ip)); return F; }; @@ -6694,49 +8032,90 @@ F.reconnect = function() { */ F.service = function(count) { - UIDGENERATOR.date = F.datetime.format('yyMMddHHmm'); - UIDGENERATOR.index = 1; + UIDGENERATOR_REFRESH(); + var keys; var releasegc = false; + F.temporary.service.request = F.stats.performance.request; + F.temporary.service.file = F.stats.performance.file; + F.temporary.service.message = F.stats.performance.message; + F.temporary.service.mail = F.stats.performance.mail; + F.temporary.service.open = F.stats.performance.open; + F.temporary.service.dbrm = F.stats.performance.dbrm; + F.temporary.service.dbwm = F.stats.performance.dbwm; + F.temporary.service.external = F.stats.performance.external; + + F.stats.performance.external = 0; + F.stats.performance.dbrm = 0; + F.stats.performance.dbwm = 0; + F.stats.performance.request = 0; + F.stats.performance.file = 0; + F.stats.performance.message = 0; + F.stats.performance.mail = 0; + F.stats.performance.open = 0; + + // clears short cahce temporary cache + F.temporary.shortcache = {}; + // clears temporary memory for non-exist files F.temporary.notfound = {}; - // every 7 minutes (default) service clears static cache - if (count % F.config['default-interval-clear-cache'] === 0) { - F.$events.clear && F.emit('clear', 'temporary', F.temporary); + if (CONF.allow_reqlimit) + F.temporary.ddos = {}; + + // every 10 minutes (default) service clears static cache + if (count % CONF.default_interval_clear_cache === 0) { + F.$events.clear && EMIT('clear', 'temporary', F.temporary); F.temporary.path = {}; F.temporary.range = {}; F.temporary.views = {}; F.temporary.other = {}; + + global.TEMP = {}; global.$VIEWCACHE && global.$VIEWCACHE.length && (global.$VIEWCACHE = []); // Clears command cache Image.clear(); - var dt = F.datetime.add('-5 minutes'); + var dt = NOW.add('-5 minutes'); for (var key in F.databases) F.databases[key] && F.databases[key].inmemorylastusage < dt && F.databases[key].release(); releasegc = true; - F.config['allow-debug'] && F.consoledebug('clear temporary cache'); + CONF.allow_debug && F.consoledebug('clear temporary cache'); + + keys = Object.keys(F.temporary.internal); + for (var i = 0; i < keys.length; i++) + if (!F.temporary.internal[keys[i]]) + delete F.temporary.internal[keys[i]]; + + // Clears released sessions + keys = Object.keys(F.sessions); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + if (F.sessions[key]) { + F.sessions[key].clean(); + CONF.allow_sessions_unused && F.sessions[key].releaseunused(CONF.allow_sessions_unused); + } + } } // every 61 minutes (default) services precompile all (installed) views - if (count % F.config['default-interval-precompile-views'] === 0) { + if (count % CONF.default_interval_precompile_views === 0) { for (var key in F.routes.views) { var item = F.routes.views[key]; F.install('view', key, item.url, null); } } - if (count % F.config['default-interval-clear-dnscache'] === 0) { - F.$events.clear && F.emit('clear', 'dns'); - U.clearDNS(); - F.config['allow-debug'] && F.consoledebug('clear DNS cache'); + if (count % CONF.default_interval_clear_dnscache === 0) { + F.$events.clear && EMIT('clear', 'dns'); + CMD('clear_dnscache'); + CONF.allow_debug && F.consoledebug('clear DNS cache'); } - var ping = F.config['default-interval-websocket-ping']; + var ping = CONF.default_interval_websocket_ping; if (ping > 0 && count % ping === 0) { var has = false; for (var item in F.connections) { @@ -6747,24 +8126,25 @@ F.service = function(count) { has = true; } } - has && F.config['allow-debug'] && F.consoledebug('ping websocket connections'); + has && CONF.allow_debug && F.consoledebug('ping websocket connections'); } - if (F.uptodates && (count % F.config['default-interval-uptodate'] === 0) && F.uptodates.length) { + // OBSOLETE, it will be deleted in v4 + if (F.uptodates && (count % CONF.default_interval_uptodate === 0) && F.uptodates.length) { var hasUpdate = false; F.uptodates.wait(function(item, next) { - if (item.updated.add(item.interval) > F.datetime) + if (item.updated.add(item.interval) > NOW) return next(); - item.updated = F.datetime; + item.updated = NOW; item.count++; setTimeout(function() { - F.config['allow-debug'] && F.consoledebug('uptodate', item.type + '#' + item.url); + CONF.allow_debug && F.consoledebug('uptodate', item.type + '#' + item.url); F.install(item.type, item.url, item.options, function(err, name, skip) { - F.config['allow-debug'] && F.consoledebug('uptodate', item.type + '#' + item.url + ' (done)'); + CONF.allow_debug && F.consoledebug('uptodate', item.type + '#' + item.url + ' (done)'); if (skip) return next(); @@ -6775,7 +8155,7 @@ F.service = function(count) { } else { hasUpdate = true; item.name = name; - F.$events.uptodate && F.emit('uptodate', item.type, name); + F.$events.uptodate && EMIT('uptodate', item.type, name); } item.callback && item.callback(err, name); @@ -6797,51 +8177,72 @@ F.service = function(count) { } // every 20 minutes (default) service clears resources - if (count % F.config['default-interval-clear-resources'] === 0) { - F.$events.clear && F.emit('clear', 'resources'); + if (count % CONF.default_interval_clear_resources === 0) { + F.$events.clear && EMIT('clear', 'resources'); F.resources = {}; releasegc = true; - F.config['allow-debug'] && F.consoledebug('clear resources'); + CONF.allow_debug && F.consoledebug('clear resources'); + } + + // Session DDOS cleaner + if (F.sessionscount && count % 15 === 0) { + keys = Object.keys(F.sessions); + for (var i = 0; i < keys.length; i++) { + var session = F.sessions[keys[i]]; + if (session.ddosis) { + session.ddos = {}; + session.ddosis = false; + } + } } // Update expires date - count % 1000 === 0 && (DATE_EXPIRES = F.datetime.add('y', 1).toUTCString()); + count % 1000 === 0 && (DATE_EXPIRES = NOW.add('y', 1).toUTCString()); + + if (count % CONF.nosql_cleaner === 0 && CONF.nosql_cleaner) { + keys = Object.keys(F.databasescleaner); + keys.wait(function(item, next) { + if (item[0] === '$') + TABLE(item.substring(1)).clean(next); + else + NOSQL(item).clean(next); + }); + } - F.$events.service && F.emit('service', count); + F.$events.service && EMIT('service', count); - if (F.config['allow-debug']) { + if (CONF.allow_debug) { F.consoledebug('service ({0}x)'.format(count)); F.usagesnapshot(); } releasegc && global.gc && setTimeout(function() { global.gc(); - F.config['allow-debug'] && F.consoledebug('gc()'); + CONF.allow_debug && F.consoledebug('gc()'); }, 1000); - // Run schedules - if (!F.schedules.length) - return F; - - var expire = F.datetime.getTime(); - var index = 0; + if (WORKERID > 9999999999) + WORKERID = 0; - while (true) { - var schedule = F.schedules[index++]; - if (!schedule) - break; - if (schedule.expire > expire) - continue; + // Run schedules + keys = Object.keys(F.schedules); - index--; + if (!keys.length) + return F; - if (schedule.repeat) - schedule.expire = F.datetime.add(schedule.repeat); - else - F.schedules.splice(index, 1); + var expire = NOW.getTime(); - F.config['allow-debug'] && F.consoledebug('schedule', schedule.id); - schedule.fn.call(F); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var schedule = F.schedules[key]; + if (schedule.expire <= expire) { + if (schedule.repeat) + schedule.expire = NOW.add(schedule.repeat); + else + delete F.schedules[key]; + CONF.allow_debug && F.consoledebug('schedule', key); + schedule.fn.call(F); + } } return F; @@ -6864,13 +8265,39 @@ F.listener = function(req, res) { else if (!req.host) // HTTP 1.0 without host return res.throw400(); + if (CONF.allow_reqlimit) { + var ip = req.ip; + if (F.temporary.ddos[ip] > CONF.allow_reqlimit) { + F.stats.response.ddos++; + res.options.code = 503; + res.options.headers = HEADERS.response503ddos; + res.options.body = '503 Service Unavailable'; + res.$text(); + return; + } + if (F.temporary.ddos[ip]) + F.temporary.ddos[ip]++; + else + F.temporary.ddos[ip] = 1; + } + + if (F._request_check_proxy) { + for (var i = 0; i < F.routes.proxies.length; i++) { + var proxy = F.routes.proxies[i]; + if (req.url.substring(0, proxy.url.length) === proxy.url) { + F.stats.response.proxy++; + makeproxy(proxy, req, res); + return; + } + } + } + var headers = req.headers; req.$protocol = ((req.connection && req.connection.encrypted) || ((headers['x-forwarded-proto'] || ['x-forwarded-protocol']) === 'https')) ? 'https' : 'http'; - req.uri = framework_internal.parseURI(req); F.stats.request.request++; - F.$events.request && F.emit('request', req, res); + F.$events.request && EMIT('request', req, res); if (F._request_check_redirect) { var redirect = F.routes.redirects[req.$protocol + '://' + req.host]; @@ -6884,18 +8311,20 @@ F.listener = function(req, res) { } req.path = framework_internal.routeSplit(req.uri.pathname); + req.processing = 0; req.isAuthorized = true; req.xhr = headers['x-requested-with'] === 'XMLHttpRequest'; res.success = false; req.user = req.session = null; - req.isStaticFile = F.config['allow-static-files'] && U.isStaticFile(req.uri.pathname); + req.isStaticFile = CONF.allow_static_files && U.isStaticFile(req.uri.pathname); if (req.isStaticFile) req.extension = U.getExtension(req.uri.pathname); else if (F.onLocale) req.$language = F.onLocale(req, res, req.isStaticFile); + req.on('aborted', onrequesterror); F.reqstats(true, true); if (F._length_request_middleware) @@ -6904,10 +8333,74 @@ F.listener = function(req, res) { F.$requestcontinue(req, res, headers); }; +function onrequesterror() { + F.reqstats(false); + this.success = true; + if (this.res) + this.res.$aborted = true; +} + function requestcontinue_middleware(req, res) { + if (req.$total_middleware) + req.$total_middleware = null; F.$requestcontinue(req, res, req.headers); } +function makeproxy(proxy, req, res) { + + var secured = proxy.uri.protocol === 'https:'; + var uri = proxy.uri; + uri.headers = req.headers; + uri.method = req.method; + + if (proxy.copypath) + uri.path = req.url; + + if (!secured) + uri.agent = PROXYKEEPALIVE; + + proxy.before && proxy.before(uri, req, res); + uri.headers.host = uri.host; + + var request; + if (secured) { + var https = require('https'); + if (uri.method === 'GET') + request = https.get(uri, makeproxycallback); + else + request = https.request(uri, makeproxycallback); + } else { + if (uri.method === 'GET') + request = http.get(uri, makeproxycallback); + else + request = http.request(uri, makeproxycallback); + } + + F.stats.performance.external++; + + request.on('error', makeproxyerror); + request.$res = res; + request.$proxy = proxy; + req.pipe(request, PROXYOPTIONS); +} + +function makeproxyerror(err) { + MODELERROR.code = 503; + MODELERROR.status = U.httpStatus(503, false); + MODELERROR.error = err.toString(); + this.$res.writeHead(503, HEADERS.response503); + this.$res.end(VIEW('.' + PATHMODULES + 'error', MODELERROR)); +} + +function makeproxycallback(response) { + this.$proxy.after && this.proxy.after(response); + this.$res.writeHead(response.statusCode, response.headers); + response.pipe(this.$res, PROXYOPTIONS); +} + + +const TRAVELCHARS = { e: 1, E: 1 }; + /** * Continue to process * @private @@ -6922,39 +8415,80 @@ F.$requestcontinue = function(req, res, headers) { if (!req || !res || res.headersSent || res.success) return; + var tmp; + // Validates if this request is the file (static file) if (req.isStaticFile) { + + F.stats.performance.file++; + tmp = F.temporary.shortcache[req.uri.pathname]; + + if (!tmp) { + // Stops path travelsation outside of "public" directory + // A potential security issue + for (var i = 0; i < req.uri.pathname.length - 1; i++) { + var c = req.uri.pathname[i]; + var n = req.uri.pathname[i + 1]; + if ((c === '.' && (n === '/' || n === '%')) || (c === '%' && n === '2' && TRAVELCHARS[req.uri.pathname[i + 2]])) { + F.temporary.shortcache[req.uri.pathname] = 2; + req.$total_status(404); + return; + } + } + F.temporary.shortcache[req.uri.pathname] = 1; + } else if (tmp === 2) { + req.$total_status(404); + return; + } + F.stats.request.file++; + if (F._length_files) req.$total_file(); else res.continue(); + return; } + F.stats.performance.request++; + if (!PERF[req.method]) { req.$total_status(404); return; } - F.stats.request.web++; + if (req.uri.search) { + tmp = F.temporary.shortcache[req.uri.search]; + + if (!tmp) { + tmp = 1; + for (var i = 1; i < req.uri.search.length - 2; i++) { + if (req.uri.search[i] === '%' && req.uri.search[i + 1] === '0' && req.uri.search[i + 2] === '0') { + tmp = 2; + break; + } + } + F.temporary.shortcache[req.uri.search] = tmp; + } + + if (tmp === 2) { + req.$total_status(404); + return; + } + } + F.stats.request.web++; req.body = EMPTYOBJECT; req.files = EMPTYARRAY; req.buffer_exceeded = false; req.buffer_has = false; req.$flags = req.method[0] + req.method[1]; - if (headers['x-proxy'] === 'total.js') { - req.isProxy = true; - req.$flags += 'f'; - flags.push('proxy'); - } - var flags = [req.method.toLowerCase()]; var multipart; - if (req.mobile) { + if (F._request_check_mobile && req.mobile) { req.$flags += 'a'; F.stats.request.mobile++; } else @@ -6969,11 +8503,12 @@ F.$requestcontinue = function(req, res, headers) { if (first === 'P' || first === 'D') { multipart = req.headers['content-type'] || ''; - req.buffer_data = U.createBuffer(); - var index = multipart.lastIndexOf(';'); + req.buffer_data = Buffer.alloc(0); + var index = multipart.indexOf(';', 6); var tmp = multipart; if (index !== -1) tmp = tmp.substring(0, index); + switch (tmp.substring(tmp.length - 4)) { case 'json': req.$flags += 'b'; @@ -6996,11 +8531,6 @@ F.$requestcontinue = function(req, res, headers) { req.$type = 2; multipart = ''; break; - case 'lace': - req.$type = 4; - flags.push('mmr'); - req.$flags += 'e'; - break; default: if (multipart) { // 'undefined' DATA @@ -7019,7 +8549,7 @@ F.$requestcontinue = function(req, res, headers) { flags.push('sse'); } - if (F.config.debug) { + if (DEBUG) { req.$flags += 'h'; flags.push('debug'); } @@ -7042,9 +8572,11 @@ F.$requestcontinue = function(req, res, headers) { } req.flags = flags; - F.$events['request-begin'] && F.emit('request-begin', req, res); - var isCORS = F._length_cors && req.headers['origin']; + F.$events['request-begin'] && EMIT('request-begin', req, res); + F.$events.request_begin && EMIT('request_begin', req, res); + + var isCORS = (F._length_cors || F.routes.corsall) && req.headers.origin != null; switch (first) { case 'G': @@ -7084,15 +8616,13 @@ F.$requestcontinue = function(req, res, headers) { if (multipart) { if (isCORS) F.$cors(req, res, cors_callback_multipart, multipart); - else if (req.$type === 4) - F.$requestcontinue_mmr(req, res, multipart); else req.$total_multipart(multipart); } else { if (method === 'PUT') F.stats.request.put++; else if (method === 'PATCH') - F.stats.request.path++; + F.stats.request.patch++; else F.stats.request.post++; if (isCORS) @@ -7117,112 +8647,125 @@ function cors_callback1(req) { } function cors_callback_multipart(req, res, multipart) { - if (req.$type === 4) - F.$requestcontinue_mmr(req, res, multipart); - else - req.$total_multipart(multipart); + req.$total_multipart(multipart); } -F.$requestcontinue_mmr = function(req, res, header) { - var route = F.routes.mmr[req.url]; - F.stats.request.mmr++; - if (route) { - F.path.verify('temp'); - framework_internal.parseMULTIPART_MIXED(req, header, F.config['directory-temp'], route.exec); - } else - req.$total_status(404); -}; - F.$cors = function(req, res, fn, arg) { - var isAllowed = false; - var cors; - - for (var i = 0; i < F._length_cors; i++) { - cors = F.routes.cors[i]; - if (framework_internal.routeCompare(req.path, cors.url, false, cors.isWILDCARD)) { - isAllowed = true; - break; - } - } - - if (!isAllowed) - return fn(req, res, arg); - - var stop = false; + var isAllowed = F.routes.corsall; + var cors, origin; var headers = req.headers; + var key; - if (!isAllowed) - stop = true; - - isAllowed = false; + if (!isAllowed) { - if (!stop && cors.headers) { - isAllowed = false; - for (var i = 0, length = cors.headers.length; i < length; i++) { - if (headers[cors.headers[i]]) { + for (var i = 0; i < F._length_cors; i++) { + cors = F.routes.cors[i]; + if (framework_internal.routeCompare(req.path, cors.url, false, cors.isWILDCARD)) { isAllowed = true; break; } } + if (!isAllowed) - stop = true; - } + return fn(req, res, arg); - if (!stop && cors.methods) { - isAllowed = false; - var current = headers['access-control-request-method'] || req.method; - if (current !== 'OPTIONS') { - for (var i = 0, length = cors.methods.length; i < length; i++) { - if (current.indexOf(cors.methods[i]) !== -1) { - isAllowed = true; - break; + var stop = false; + + key = 'cors' + cors.hash + '_' + headers.origin; + + if (F.temporary.other[key]) { + stop = F.temporary.other[key] === 2; + } else { + + isAllowed = false; + + if (cors.headers) { + isAllowed = false; + for (var i = 0, length = cors.headers.length; i < length; i++) { + if (headers[cors.headers[i]]) { + isAllowed = true; + break; + } } + if (!isAllowed) + stop = true; } - if (!isAllowed) - stop = true; - } - } + if (!stop && cors.methods) { + isAllowed = false; + var current = headers['access-control-request-method'] || req.method; + if (current !== 'OPTIONS') { + for (var i = 0, length = cors.methods.length; i < length; i++) { + if (current === cors.methods[i]) { + isAllowed = true; + break; + } + } + if (!isAllowed) + stop = true; + } + } - var origin = headers['origin'].toLowerCase(); - if (!stop && cors.origins) { - isAllowed = false; - for (var i = 0, length = cors.origins.length; i < length; i++) { - if (cors.origins[i].indexOf(origin) !== -1) { - isAllowed = true; - break; + if (!stop && cors.origin) { + origin = headers.origin.toLowerCase().substring(headers.origin.indexOf('/') + 2); + if (origin !== headers.host) { + isAllowed = false; + for (var i = 0, length = cors.origin.length; i < length; i++) { + if (cors.origin[i].indexOf(origin) !== -1) { + isAllowed = true; + break; + } + } + if (!isAllowed) + stop = true; + } } + + F.temporary.other[key] = stop ? 2 : 1; + } + } else if (CONF.default_cors) { + key = headers.origin; + if (F.temporary.other[key]) { + stop = F.temporary.other[key] === 2; + } else { + origin = key.toLowerCase().substring(key.indexOf('/') + 2); + stop = origin !== headers.host && CONF.default_cors.indexOf(origin) === -1; + F.temporary.other[key] = stop ? 2 : 1; } - if (!isAllowed) - stop = true; } - var name; - var isOPTIONS = req.method === 'OPTIONS'; + if (stop) + origin = 'null'; + else + origin = headers.origin; + + res.setHeader('Access-Control-Allow-Origin', origin); - res.setHeader('Access-Control-Allow-Origin', cors.origins ? cors.origins : cors.credentials ? isAllowed ? origin : cors.origins ? cors.origins : origin : headers['origin']); - cors.credentials && res.setHeader('Access-Control-Allow-Credentials', 'true'); + if (!cors || cors.credentials) + res.setHeader('Access-Control-Allow-Credentials', 'true'); - name = 'Access-Control-Allow-Methods'; + var name = 'Access-Control-Allow-Methods'; + var isOPTIONS = req.method === 'OPTIONS'; - if (cors.methods) + if (cors && cors.methods) res.setHeader(name, cors.methods.join(', ')); else res.setHeader(name, isOPTIONS ? headers['access-control-request-method'] || '*' : req.method); name = 'Access-Control-Allow-Headers'; - if (cors.headers) + if (cors && cors.headers) res.setHeader(name, cors.headers.join(', ')); else res.setHeader(name, headers['access-control-request-headers'] || '*'); - cors.age && res.setHeader('Access-Control-Max-Age', cors.age); + cors && cors.age && res.setHeader('Access-Control-Max-Age', cors.age); if (stop) { fn = null; - F.$events['request-end'] && F.emit('request-end', req, res); + F.$events['request-end'] && EMIT('request-end', req, res); + F.$events.request_end && EMIT('request_end', req, res); F.reqstats(false, false); F.stats.request.blocked++; res.writeHead(404); @@ -7234,7 +8777,8 @@ F.$cors = function(req, res, fn, arg) { return fn(req, res, arg); fn = null; - F.$events['request-end'] && F.emit('request-end', req, res); + F.$events['request-end'] && EMIT('request-end', req, res); + F.$events.request_end && EMIT('request_end', req, res); F.reqstats(false, false); res.writeHead(200); res.end(); @@ -7247,9 +8791,10 @@ F.$cors = function(req, res, fn, arg) { * @param {Socket} socket * @param {Buffer} head */ -F._upgrade = function(req, socket, head) { +const REGWS = /websocket/i; +F.$upgrade = function(req, socket, head) { - if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') + if (!REGWS.test(req.headers.upgrade || '') || F._length_wait) return; // disables timeout @@ -7261,7 +8806,7 @@ F._upgrade = function(req, socket, head) { req.uri = framework_internal.parseURI(req); - F.$events.websocket && F.emit('websocket', req, socket, head); + F.$events.websocket && EMIT('websocket', req, socket, head); F.stats.request.websocket++; req.session = null; @@ -7284,33 +8829,82 @@ F._upgrade = function(req, socket, head) { }; function websocketcontinue_middleware(req) { + if (req.$total_middleware) + req.$total_middleware = null; F.$websocketcontinue(req, req.$wspath, req.headers); } +function websocketcontinue_authnew(isAuthorized, user, $) { + + // @isAuthorized "null" for callbacks(err, user) + // @isAuthorized "true" + // @isAuthorized "object" is as user but "user" must be "undefined" + + if (isAuthorized instanceof Error || isAuthorized instanceof ErrorBuilder) { + // Error handling + isAuthorized = false; + } else if (isAuthorized == null && user) { + // A callback error handling + isAuthorized = true; + } else if (user == null && isAuthorized && isAuthorized !== true) { + user = isAuthorized; + isAuthorized = true; + } + + var req = $.req; + if (user) + req.user = user; + var route = F.lookup_websocket(req, req.websocket.uri.pathname, isAuthorized ? 1 : 2); + if (route) { + F.$websocketcontinue_process(route, req, req.websocketpath); + } else + req.websocket.$close(4001, '401: unauthorized'); +} + F.$websocketcontinue = function(req, path) { - var auth = F.onAuthorize; - if (auth) { - auth.call(F, req, req.websocket, req.flags, function(isLogged, user) { + req.websocketpath = path; + if (F.onAuthorize) { + if (F.onAuthorize.$newversion) { + F.onAuthorize(req, req.websocket, req.flags, websocketcontinue_authnew); + } else { + // @TODO: remove in v4 + F.onAuthorize.call(F, req, req.websocket, req.flags, function(isAuthorized, user) { - if (user) - req.user = user; + if (!F.onAuthorize.isobsolete) { + F.onAuthorize.isobsolete = 1; + OBSOLETE('F.onAuthorize', 'You need to use a new authorization declaration: "AUTH(function($) {})"'); + } - var route = F.lookup_websocket(req, req.websocket.uri.pathname, isLogged ? 1 : 2); - if (route) { - F.$websocketcontinue_process(route, req, path); - } else { - req.websocket.close(); - req.connection.destroy(); - } - }); + // @isAuthorized "null" for callbacks(err, user) + // @isAuthorized "true" + // @isAuthorized "object" is as user but "user" must be "undefined" + + if (isAuthorized instanceof Error || isAuthorized instanceof ErrorBuilder) { + // Error handling + isAuthorized = false; + } else if (isAuthorized == null && user) { + // A callback error handling + isAuthorized = true; + } else if (user == null && isAuthorized && isAuthorized !== true) { + user = isAuthorized; + isAuthorized = true; + } + + if (user) + req.user = user; + var route = F.lookup_websocket(req, req.websocket.uri.pathname, isAuthorized ? 1 : 2); + if (route) { + F.$websocketcontinue_process(route, req, path); + } else + req.websocket.$close(4001, '401: unauthorized'); + }); + } } else { var route = F.lookup_websocket(req, req.websocket.uri.pathname, 0); if (route) { F.$websocketcontinue_process(route, req, path); - } else { - req.websocket.close(); - req.connection.destroy(); - } + } else + req.websocket.$close(4004, '404: not found'); } }; @@ -7318,9 +8912,8 @@ F.$websocketcontinue_process = function(route, req, path) { var socket = req.websocket; - if (!socket.prepare(route.flags, route.protocols, route.allow, route.length, F.version_header)) { - socket.close(); - req.connection.destroy(); + if (!socket.prepare(route.flags, route.protocols, route.allow, route.length)) { + socket.$close(4001, '401: unauthorized'); return; } @@ -7331,14 +8924,21 @@ F.$websocketcontinue_process = function(route, req, path) { else if (route.isJSON) socket.type = 3; + if (route.isBUFFER) + socket.typebuffer = true; + var next = function() { + if (req.$total_middleware) + req.$total_middleware = null; + if (F.connections[id]) { socket.upgrade(F.connections[id]); return; } var connection = new WebSocket(path, route.controller, id); + connection.encodedecode = CONF.default_websocket_encodedecode === true; connection.route = route; connection.options = route.options; F.connections[id] = connection; @@ -7385,7 +8985,7 @@ global.MODEL = F.model = function(name) { var obj = F.models[name]; if (obj || obj === null) return obj; - var filename = U.combine(F.config['directory-models'], name + '.js'); + var filename = U.combine(CONF.directory_models, name + '.js'); existsSync(filename) && F.install('model', name, filename, undefined, undefined, undefined, true); return F.models[name] || null; }; @@ -7400,7 +9000,7 @@ global.INCLUDE = global.SOURCE = F.source = function(name, options, callback) { var obj = F.sources[name]; if (obj || obj === null) return obj; - var filename = U.combine(F.config['directory-source'], name + '.js'); + var filename = U.combine(CONF.directory_source, name + '.js'); existsSync(filename) && F.install('source', name, filename, options, callback, undefined, true); return F.sources[name] || null; }; @@ -7425,7 +9025,7 @@ F.include = function(name, options, callback) { * @param {String} language Optional. * @return {MailMessage} */ -F.mail = function(address, subject, view, model, callback, language) { +global.MAIL = F.mail = function(address, subject, view, model, callback, language) { if (typeof(callback) === 'string') { var tmp = language; @@ -7439,8 +9039,8 @@ F.mail = function(address, subject, view, model, callback, language) { if (controller.themeName) view = prepare_viewname(view); - else if (this.onTheme) - controller.themeName = this.onTheme(controller); + else if (F.onTheme) + controller.themeName = F.onTheme(controller); else controller.themeName = ''; @@ -7452,7 +9052,12 @@ F.mail = function(address, subject, view, model, callback, language) { controller.language = language; } - return controller.mail(address, subject, view, model, callback, replyTo); + var mail = controller.mail(address, subject, view, model, callback, replyTo); + + if (language != null) + mail.language = language; + + return mail; }; /** @@ -7464,7 +9069,7 @@ F.mail = function(address, subject, view, model, callback, language) { * @param {String} language Optional. * @return {String} */ -F.view = function(name, model, layout, repository, language) { +global.VIEW = function(name, model, layout, repository, language) { var controller = EMPTYCONTROLLER; @@ -7482,14 +9087,19 @@ F.view = function(name, model, layout, repository, language) { if (theme) { controller.themeName = theme; name = prepare_viewname(name); - } else if (this.onTheme) - controller.themeName = this.onTheme(controller); + } else if (F.onTheme) + controller.themeName = F.onTheme(controller); else controller.themeName = undefined; return controller.view(name, model, true); }; +F.view = function(name, model, layout, repository, language) { + OBSOLETE('F.view()', 'Instead of F.view() use VIEW()'); + return VIEW(name, model, layout, repository, language); +}; + /** * Compiles and renders view * @param {String} body HTML body. @@ -7499,7 +9109,7 @@ F.view = function(name, model, layout, repository, language) { * @param {String} language Optional. * @return {String} */ -F.viewCompile = function(body, model, layout, repository, language) { +global.VIEWCOMPILE = function(body, model, layout, repository, language) { var controller = EMPTYCONTROLLER; @@ -7514,7 +9124,12 @@ F.viewCompile = function(body, model, layout, repository, language) { controller.themeName = undefined; controller.repository = typeof(repository) === 'object' && repository ? repository : EMPTYOBJECT; - return controller.viewCompile(body, model, true); + return controller.view_compile(body, model, true); +}; + +F.viewCompile = function(body, model, layout, repository, language) { + OBSOLETE('F.viewCompile()', 'Instead of F.viewCompile() use VIEWCOMPILE()'); + return VIEWCOMPILE(body, model, layout, repository, language); }; /** @@ -7544,26 +9159,27 @@ F.clear = function(callback, isInit) { var plus = F.id ? 'i-' + F.id + '_' : ''; if (isInit) { - if (F.config['disable-clear-temporary-directory']) { - // clears only JS and CSS files - U.ls(dir, function(files) { - F.unlink(files, function() { - callback && callback(); + if (!CONF.allow_clear_temp) { + if (F.$bundling) { + // clears only JS and CSS files + U.ls(dir, function(files) { + F.unlink(files, function() { + callback && callback(); + }); + }, function(filename, folder) { + if (folder || (plus && !filename.substring(dir.length).startsWith(plus))) + return false; + if (filename.indexOf('.package') !== -1) + return true; + var ext = U.getExtension(filename); + return JSFILES[ext] || ext === 'css' || ext === 'tmp' || ext === 'upload' || ext === 'html' || ext === 'htm'; }); - }, function(filename, folder) { - if (folder || (plus && !filename.substring(dir.length).startsWith(plus))) - return false; - if (filename.indexOf('.package') !== -1) - return true; - var ext = U.getExtension(filename); - return ext === 'js' || ext === 'css' || ext === 'tmp' || ext === 'upload' || ext === 'html' || ext === 'htm'; - }); - + } return F; } } - if (!existsSync(dir)) { + if (!existsSync(dir) || !F.$bundling) { callback && callback(); return F; } @@ -7578,8 +9194,19 @@ F.clear = function(callback, isInit) { continue; (filename.indexOf('/') === -1 || filename.indexOf('.package/') !== -1) && !filename.endsWith('.jsoncache') && arr.push(files[i]); } + files = arr; - directories = directories.remove(n => n.indexOf('.package') === -1); + directories = directories.remove(function(name) { + name = U.getName(name); + + if (name[0] === '~') + return false; + + if (name.endsWith('.package')) + return false; + + return true; + }); } F.unlink(files, () => F.rmdir(directories, callback)); @@ -7659,9 +9286,9 @@ F.rmdir = F.path.rmdir = function(arr, callback) { * @param {Boolean} isUnique Optional, default true. * @return {String} */ -F.encrypt = function(value, key, isUnique) { +global.ENCRYPT = F.encrypt = function(value, key, isUnique) { - if (value === undefined) + if (value == null) return ''; var type = typeof(value); @@ -7679,7 +9306,25 @@ F.encrypt = function(value, key, isUnique) { else if (type === 'object') value = JSON.stringify(value); - return value.encrypt(F.config.secret + '=' + key, isUnique); + if (CONF.default_crypto) { + key = (key || '') + CONF.secret; + + if (key.length < 32) + key += ''.padLeft(32 - key.length, '0'); + + if (key.length > 32) + key = key.substring(0, 32); + + if (!F.temporary.keys[key]) + F.temporary.keys[key] = Buffer.from(key); + + var cipher = Crypto.createCipheriv(CONF.default_crypto, F.temporary.keys[key], CONF.default_crypto_iv); + CONCAT[0] = cipher.update(value); + CONCAT[1] = cipher.final(); + return Buffer.concat(CONCAT).toString('hex'); + } + + return value.encrypt(CONF.secret + '=' + key, isUnique); }; /** @@ -7689,7 +9334,7 @@ F.encrypt = function(value, key, isUnique) { * @param {Boolean} jsonConvert Optional, default true. * @return {Object or String} */ -F.decrypt = function(value, key, jsonConvert) { +global.DECRYPT = F.decrypt = function(value, key, jsonConvert) { if (typeof(key) === 'boolean') { var tmp = jsonConvert; @@ -7700,20 +9345,57 @@ F.decrypt = function(value, key, jsonConvert) { if (typeof(jsonConvert) !== 'boolean') jsonConvert = true; - var response = (value || '').decrypt(F.config.secret + '=' + key); - if (!response) - return null; + var response; - if (jsonConvert) { - if (response.isJSON()) { - try { - return response.parseJSON(true); - } catch (ex) {} + if (CONF.default_crypto) { + + key = (key || '') + CONF.secret; + + if (key.length < 32) + key += ''.padLeft(32 - key.length, '0'); + + if (key.length > 32) + key = key.substring(0, 32); + + if (!F.temporary.keys[key]) + F.temporary.keys[key] = Buffer.from(key); + + var decipher = Crypto.createDecipheriv(CONF.default_crypto, F.temporary.keys[key], CONF.default_crypto_iv); + try { + CONCAT[0] = decipher.update(Buffer.from(value || '', 'hex')); + CONCAT[1] = decipher.final(); + response = Buffer.concat(CONCAT).toString('utf8'); + } catch (e) { + response = null; } - return null; - } + } else + response = (value || '').decrypt(CONF.secret + '=' + key); - return response; + return response ? (jsonConvert ? (response.isJSON() ? response.parseJSON(true) : null) : response) : null; +}; + +global.ENCRYPTREQ = function(req, val, key, strict) { + + if (req instanceof Controller) + req = req.req; + + var obj = {}; + obj.ua = req.ua; + if (strict) + obj.ip = req.ip; + obj.data = val; + return F.encrypt(obj, key); +}; + +global.DECRYPTREQ = function(req, val, key) { + if (!val) + return; + if (req instanceof Controller) + req = req.req; + var obj = F.decrypt(val, key || '', true); + if (!obj || (obj.ip && obj.ip !== req.ip) || (obj.ua !== req.ua)) + return; + return obj.data; }; /** @@ -7724,13 +9406,16 @@ F.decrypt = function(value, key, jsonConvert) { * @return {String} */ F.hash = function(type, value, salt) { + + OBSOLETE('F.hash()', 'Use String.prototype.hash()'); + var hash = Crypto.createHash(type); var plus = ''; if (typeof(salt) === 'string') plus = salt; else if (salt !== false) - plus = (F.config.secret || ''); + plus = (CONF.secret || ''); hash.update(value.toString() + plus, ENCODING); return hash.digest('hex'); @@ -7742,7 +9427,10 @@ F.hash = function(type, value, salt) { * @param {String} key Resource key. * @return {String} String */ -F.resource = function(name, key) { + +const DEFNAME = 'default'; + +global.RESOURCE = F.resource = function(name, key) { if (!key) { key = name; @@ -7750,11 +9438,14 @@ F.resource = function(name, key) { } if (!name) - name = 'default'; + name = DEFNAME; var res = F.resources[name]; - if (res) + if (res) { + if (res.$empty && res[key] == null && name !== DEFNAME) + return res[key] = F.resource(DEFNAME, key); // tries to load a value from "default.resource" return res[key] == null ? '' : res[key]; + } var routes = F.routes.resources[name]; var body = ''; @@ -7767,7 +9458,7 @@ F.resource = function(name, key) { } } - var filename = U.combine(F.config['directory-resources'], name + '.resource'); + var filename = U.combine(CONF.directory_resources, name + '.resource'); var empty = false; if (existsSync(filename)) body += (body ? '\n' : '') + Fs.readFileSync(filename).toString(ENCODING); @@ -7777,7 +9468,7 @@ F.resource = function(name, key) { var obj = body.parseConfig(); F.resources[name] = obj; obj.$empty = empty; - return obj[key] == null ? '' : obj[key]; + return obj[key] == null ? name == DEFNAME ? '' : obj[key] = F.resource(DEFNAME, key) : obj[key]; }; /** @@ -7786,7 +9477,9 @@ F.resource = function(name, key) { * @param {String} text * @return {String} */ -F.translate = function(language, text) { + +// var obsolete_translate = false; +global.TRANSLATE = F.translate = function(language, text) { if (!text) { text = language; @@ -7796,6 +9489,15 @@ F.translate = function(language, text) { if (text[0] === '#' && text[1] !== ' ') return F.resource(language, text.substring(1)); + /* + var value = F.resource(language, 'T' + text.hash(true).toString(16)); + if (!value) { + value = F.resources[language]['T' + text.hash()]; + if (value && !obsolete_translate) { + obsolete_translate = true; + OBSOLETE(language + '.resource', 'A resource file contains older keys with localization, please regenerate localization.'); + } + }*/ var value = F.resource(language, 'T' + text.hash()); return value ? value : text; }; @@ -7806,7 +9508,7 @@ F.translate = function(language, text) { * @param {String} text * @return {String} */ -F.translator = function(language, text) { +global.TRANSLATOR = F.translator = function(language, text) { return framework_internal.parseLocalization(text, language); }; @@ -7870,10 +9572,10 @@ F.$configure_sitemap = function(arr, clean) { return F; }; -F.sitemap = function(name, me, language) { +global.SITEMAP = F.sitemap = function(name, me, language) { if (!F.routes.sitemap) - return EMPTYARRAY; + return me ? null : EMPTYARRAY; if (typeof(me) === 'string') { language = me; @@ -8034,9 +9736,11 @@ F.$configure_dependencies = function(arr, callback) { arr = null; } - if (!arr) + if (!arr || !arr.length) return F; + OBSOLETE('/dependencies', 'File "/dependencies" are deprecated and they will be removed in v4.'); + var type; var options; var interval; @@ -8174,6 +9878,8 @@ F.$configure_workflows = function(arr, clean) { if (!arr || !arr.length) return F; + OBSOLETE('/workflows', 'File "/workflows" are deprecated and they will be removed in v4.'); + arr.forEach(function(line) { line = line.trim(); if (line.startsWith('//')) @@ -8274,10 +9980,124 @@ F.$configure_versions = function(arr, clean) { var len = ismap ? 3 : 2; var key = str.substring(0, index).trim(); var filename = str.substring(index + len).trim(); - F.versions[key] = filename; - ismap && F.map(filename, F.path.public(key)); + + if (CONF.default_root) + key = U.join(CONF.default_root, key); + + if (filename === 'auto') { + + if (ismap) + throw new Error('/versions: "auto" value can\'t be used with mapping'); + + F.versions[key] = filename; + + (function(key, filename) { + ON('ready', function() { + F.consoledebug('"versions" is getting checksum of ' + key); + makehash(key, function(hash) { + + F.consoledebug('"versions" is getting checksum of ' + key + ' (done)'); + + if (hash) { + var index = key.lastIndexOf('.'); + filename = key.substring(0, index) + '-' + hash + key.substring(index); + + F.versions[key] = filename; + + if (!F.routes.merge[key] && !F.temporary.other['merge_' + key]) { + var index = key.indexOf('/', 1); + var theme = index === -1 ? null : key.substring(1, index); + if (theme) { + if (F.themes[theme]) + key = F.themes[theme] + 'public' + key.substring(index); + else + key = F.path.public(key); + } else + key = F.path.public(key); + F.map(filename, key); + } + + F.temporary.views = {}; + F.temporary.other = {}; + global.$VIEWCACHE = []; + } + }); + }); + })(key, filename); + + } else { + F.versions[key] = filename; + ismap && F.map(filename, F.path.public(key)); + } + } + + return F; +}; + +function makehash(url, callback, count) { + var target = 'http://' + (F.ip === 'auto' ? '0.0.0.0' : F.ip) + ':' + F.port + url; + U.download(target, ['get'], function(err, stream, status) { + + // Maybe F.wait() + if (status === 503) { + // Unhandled problem + if (count > 60) + callback(''); + else + setTimeout((url, callback, count) => makehash(url, callback, (count || 1) + 1), 1000, url, callback, count); + return; + } + + if (status !== 200) { + callback(''); + return; + } + + var hash = Crypto.createHash('md5'); + hash.setEncoding('hex'); + stream.pipe(hash); + stream.on('end', function() { + hash.end(); + callback(hash.read().crc32(true)); + }); + + stream.on('error', () => callback('')); + }); +} + +F.$configure_env = function(filename) { + + var data; + + if (filename) { + filename = prepare_filename(filename); + if (!existsSync(filename, true)) + return F; + data = Fs.readFileSync(filename).toString(ENCODING); + } + + var filename2 = null; + + if (!filename) { + filename = U.combine('/', '.env'); + filename2 = '.env-' + (DEBUG ? 'debug' : 'release'); + if (!existsSync(filename, true)) { + F.$configure_env(filename2); + return F; + } + data = Fs.readFileSync(filename).toString(ENCODING); } + data = data.parseENV(); + var keys = Object.keys(data); + + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + if (!process.env.hasOwnProperty(key)) + process.env[key] = data[key]; + } + + filename2 && F.$configure_env(filename2); return F; }; @@ -8294,12 +10114,11 @@ F.$configure_configs = function(arr, rewrite) { if (!arr) { var filenameA = U.combine('/', 'config'); - var filenameB = U.combine('/', 'config-' + (F.config.debug ? 'debug' : 'release')); - + var filenameB = U.combine('/', 'config-' + (DEBUG ? 'debug' : 'release')); arr = []; // read all files from "configs" directory - var configs = F.path.configs(); + var configs = PATH.configs(); if (existsSync(configs)) { var tmp = Fs.readdirSync(configs); for (var i = 0, length = tmp.length; i < length; i++) { @@ -8325,8 +10144,8 @@ F.$configure_configs = function(arr, rewrite) { } var done = function() { - process.title = 'total: ' + F.config.name.removeDiacritics().toLowerCase().replace(REG_EMPTY, '-').substring(0, 8); - F.isVirtualDirectory = existsSync(U.combine(F.config['directory-public-virtual'])); + process.title = 'total: ' + CONF.name.removeDiacritics().toLowerCase().replace(REG_EMPTY, '-').substring(0, 8); + F.isVirtualDirectory = existsSync(U.combine(CONF.directory_public_virtual)); }; if (!(arr instanceof Array) || !arr.length) { @@ -8343,6 +10162,7 @@ F.$configure_configs = function(arr, rewrite) { var tmp; var subtype; var value; + var generated = []; for (var i = 0; i < length; i++) { var str = arr[i]; @@ -8361,6 +10181,11 @@ F.$configure_configs = function(arr, rewrite) { value = str.substring(index + 1).trim(); index = name.indexOf('('); + if (value.substring(0, 7) === 'base64 ' && value.length > 8) + value = Buffer.from(value.substring(7).trim(), 'base64').toString('utf8'); + else if (value.substring(0, 4) === 'hex ' && value.length > 6) + value = Buffer.from(value.substring(4).trim(), 'hex').toString('utf8'); + if (index !== -1) { subtype = name.substring(index + 1, name.indexOf(')')).trim().toLowerCase(); name = name.substring(0, index).trim(); @@ -8368,35 +10193,82 @@ F.$configure_configs = function(arr, rewrite) { subtype = ''; switch (name) { - case 'default-cors-maxage': + case 'secret': + case 'secret-uid': + case 'secret_uid': + name = name.replace(REG_OLDCONF, '_'); + obj[name] = value; + break; case 'default-request-length': + OBSOLETE(name, 'You need to use "default_request_maxlength"'); + obj.default_request_maxlength = U.parseInt(value); + break; case 'default-websocket-request-length': - case 'default-request-timeout': - case 'default-interval-clear-cache': - case 'default-interval-clear-resources': - case 'default-interval-precompile-views': - case 'default-interval-uptodate': - case 'default-interval-websocket-ping': + OBSOLETE(name, 'You need to use "default_websocket_maxlength"'); + obj.default_websocket_maxlength = U.parseInt(value); + break; case 'default-maximum-file-descriptors': - case 'default-interval-clear-dnscache': - case 'default-dependency-timeout': + OBSOLETE(name, 'You need to use "default_maxopenfiles"'); + obj.default_maxopenfiles = U.parseInt(value); + break; + case 'default-cors-maxage': // old + case 'default-request-timeout': // old + case 'default-request-maxlength': // old + case 'default-request-maxkeys': // old + case 'default-websocket-maxlength': // old + case 'default-interval-clear-cache': // old + case 'default-interval-clear-resources': // old + case 'default-interval-precompile-views': // old + case 'default-interval-uptodate': // old + case 'default-interval-websocket-ping': // old + case 'default-interval-clear-dnscache': // old + case 'default-dependency-timeout': // old + case 'default-restbuilder-timeout': // old + case 'nosql-cleaner': // old + case 'default-errorbuilder-status': // old + case 'default-maxopenfiles': // old + case 'default_maxopenfiles': + case 'default_errorbuilder_status': + case 'default_cors_maxage': + case 'default_request_timeout': + case 'default_request_maxlength': + case 'default_request_maxkeys': + case 'default_websocket_maxlength': + case 'default_interval_clear_cache': + case 'default_interval_clear_resources': + case 'default_interval_precompile_views': + case 'default_interval_uptodate': + case 'default_interval_websocket_ping': + case 'default_interval_clear_dnscache': + case 'default_dependency_timeout': + case 'default_restbuilder_timeout': + case 'nosql_cleaner': + name = obsolete_config(name); obj[name] = U.parseInt(value); break; - case 'default-image-consumption': - case 'default-image-quality': + case 'default-image-consumption': // old + case 'default-image-quality': // old + case 'default_image_consumption': + case 'default_image_quality': + name = obsolete_config(name); obj[name] = U.parseInt(value.replace(/%|\s/g, '')); break; - case 'static-accepts-custom': + case 'static-accepts-custom': // old + case 'static_accepts_custom': accepts = value.replace(REG_ACCEPTCLEANER, '').split(','); break; - case 'default-root': + case 'default-root': // old + case 'default_root': + name = obsolete_config(name); if (value) obj[name] = U.path(value); break; - case 'static-accepts': + case 'static-accepts': // old + case 'static_accepts': + name = obsolete_config(name); obj[name] = {}; tmp = value.replace(REG_ACCEPTCLEANER, '').split(','); for (var j = 0; j < tmp.length; j++) @@ -8411,7 +10283,7 @@ F.$configure_configs = function(arr, rewrite) { case 'mail.address.reply': if (name === 'mail.address.bcc') - tmp = 'mail-address-copy'; + tmp = 'mail_address_copy'; else tmp = name.replace(/\./g, '-'); @@ -8419,42 +10291,145 @@ F.$configure_configs = function(arr, rewrite) { obj[tmp] = value; break; + case 'default-cors': // old + case 'default_cors': + name = obsolete_config(name); + value = value.replace(/,/g, ' ').split(' '); + tmp = []; + for (var j = 0; j < value.length; j++) { + var co = (value[j] || '').trim(); + if (co) { + co = co.toLowerCase(); + if (co.substring(0, 2) === '//') { + tmp.push(co); + } else + tmp.push(co.substring(co.indexOf('/') + 2)); + } + } + obj[name] = tmp.length ? tmp : null; + break; + case 'allow-handle-static-files': - OBSOLETE('config["allow-handle-static-files"]', 'The key has been renamed to "allow-static-files"'); - obj['allow-static-files'] = true; + OBSOLETE('config["allow-handle-static-files"]', 'The key has been renamed to "allow_static_files"'); + obj.allow_static_files = true; break; - case 'allow-compile-html': - case 'allow-compile-script': - case 'allow-compile-style': - case 'allow-debug': - case 'allow-gzip': - case 'allow-performance': - case 'allow-static-files': - case 'allow-websocket': - case 'disable-strict-server-certificate-validation': case 'disable-clear-temporary-directory': + OBSOLETE('disable-clear-temporary-directory', 'You need to use "allow_clear_temp : true|false"'); + obj.allow_clear_temp = !(value.toLowerCase() === 'true' || value === '1' || value === 'on'); + break; + + case 'disable-strict-server-certificate-validation': + OBSOLETE('disable-strict-server-certificate-validation', 'You need to use "allow_ssc_validation : true|false"'); + obj.allow_ssc_validation = !(value.toLowerCase() === 'true' || value === '1' || value === 'on'); + break; + + case 'allow-compile': // old + case 'allow-compile-html': // old + case 'allow-compile-script': // old + case 'allow-compile-style': // old + case 'allow-ssc-validation': // old + case 'allow-debug': // old + case 'allow-gzip': // old + case 'allow-head': // old + case 'allow-performance': // old + case 'allow-static-files': // old + case 'allow-websocket': // old + case 'allow-websocket-compression': // old + case 'allow-clear-temp': // old + case 'allow-cache-snapshot': // old + case 'allow-cache-cluster': // old + case 'allow-custom-titles': // old + case 'nosql-worker': // old + case 'nosql-logger': // old + case 'allow-filter-errors': // old + case 'default-websocket-encodedecode': // old + case 'allow_compile': + case 'allow_compile_html': + case 'allow_compile_script': + case 'allow_compile_style': + case 'allow_ssc_validation': + case 'allow_debug': + case 'allow_gzip': + case 'allow_head': + case 'allow_performance': + case 'allow_static_files': + case 'allow_websocket': + case 'allow_websocket_compression': + case 'allow_clear_temp': + case 'allow_cache_snapshot': + case 'allow_cache_cluster': + case 'allow_filter_errors': + case 'allow_custom_titles': case 'trace': - case 'allow-cache-snapshot': + case 'nosql_worker': + case 'nosql_logger': + case 'default_websocket_encodedecode': + name = obsolete_config(name); obj[name] = value.toLowerCase() === 'true' || value === '1' || value === 'on'; break; + case 'nosql-inmemory': // old + case 'nosql_inmemory': + name = obsolete_config(name); + obj[name] = typeof(value) === 'string' ? value.split(',').trim() : value instanceof Array ? value : null; + break; + case 'allow-compress-html': - obj['allow-compile-html'] = value.toLowerCase() === 'true' || value === '1' || value === 'on'; + obj.allow_compile_html = value.toLowerCase() === 'true' || value === '1' || value === 'on'; break; case 'version': obj[name] = value; break; - default: + case 'security.txt': + obj[name] = value ? value.split(',').trim().join('\n') : ''; + break; - if (subtype === 'string') - obj[name] = value; - else if (subtype === 'number' || subtype === 'currency' || subtype === 'float' || subtype === 'double') - obj[name] = value.isNumber(true) ? value.parseFloat() : value.parseInt(); - else if (subtype === 'boolean' || subtype === 'bool') - obj[name] = value.parseBoolean(); + case 'default_crypto_iv': + obj[name] = typeof(value) === 'string' ? Buffer.from(value, 'hex') : value; + break; + case 'allow_workers_silent': + obj[name] = HEADERS.workers.silent = value; + break; + + // backward compatibility + case 'mail-smtp': // old + case 'mail-smtp-options': // old + case 'mail-address-from': // old + case 'mail-address-copy': // old + case 'mail-address-bcc': // old + case 'mail-address-reply': // old + case 'default-image-converter': // old + case 'static-url': // old + case 'static-url-script': // old + case 'static-url-style': // old + case 'static-url-image': // old + case 'static-url-video': // old + case 'static-url-font': // old + case 'static-url-download': // old + case 'static-url-components': // old + case 'default-xpoweredby': // old + case 'default-layout': // old + case 'default-theme': // old + case 'default-proxy': // old + case 'default-timezone': // old + case 'default-response-maxage': // old + case 'default-errorbuilder-resource-name': // old + case 'default-errorbuilder-resource-prefix': // old + name = obsolete_config(name); + obj[name] = value; + break; + + default: + + if (subtype === 'string') + obj[name] = value; + else if (subtype === 'number' || subtype === 'currency' || subtype === 'float' || subtype === 'double') + obj[name] = value.isNumber(true) ? value.parseFloat2() : value.parseInt2(); + else if (subtype === 'boolean' || subtype === 'bool') + obj[name] = (/true|on|1|enabled/i).test(value); else if (subtype === 'eval' || subtype === 'object' || subtype === 'array') { try { obj[name] = new Function('return ' + value)(); @@ -8467,43 +10442,81 @@ F.$configure_configs = function(arr, rewrite) { obj[name] = value.parseDate(); else if (subtype === 'env' || subtype === 'environment') obj[name] = process.env[value]; - else - obj[name] = value.isNumber() ? U.parseInt(value) : value.isNumber(true) ? U.parseFloat(value) : value.isBoolean() ? value.toLowerCase() === 'true' : value; + else if (subtype === 'random') + obj[name] = GUID(value || 10); + else if (subtype === 'generate') { + obj[name] = GUID(value || 10); + generated.push(name); + } else { + if (value.isNumber()) { + obj[name] = value[0] !== '0' ? U.parseInt(value) : value; + } else if (value.isNumber(true)) + obj[name] = value.indexOf(',') === -1 && !(/^0{2,}/).test(value) ? U.parseFloat(value) : value; + else + obj[name] = value.isBoolean() ? value.toLowerCase() === 'true' : value; + } break; } } - U.extend(F.config, obj, rewrite); + // Cache for generated passwords + if (generated && generated.length) { + var filenameC = U.combine('/databases/', 'config{0}.json'.format(global.THREAD ? ('_' + global.THREAD) : '')); + var gdata; - var tmp = F.config['mail-smtp-options']; + if (existsSync(filenameC)) { + gdata = Fs.readFileSync(filenameC).toString('utf8').parseJSON(true); + for (var i = 0; i < generated.length; i++) { + if (gdata[generated[i]] != null) + obj[generated[i]] = gdata[generated[i]]; + } + } + + tmp = {}; + for (var i = 0; i < generated.length; i++) + tmp[generated[i]] = obj[generated[i]]; + + PATH.verify('databases'); + Fs.writeFileSync(filenameC, JSON.stringify(tmp), NOOP); + } + + U.extend(CONF, obj, rewrite); + + if (!CONF.secret_uid) + CONF.secret_uid = (CONF.name).crc32(true).toString(); + + tmp = CONF.mail_smtp_options; if (typeof(tmp) === 'string' && tmp) { tmp = new Function('return ' + tmp)(); - F.config['mail-smtp-options'] = tmp; + CONF.mail_smtp_options = tmp; } - if (!F.config['directory-temp']) - F.config['directory-temp'] = '~' + U.path(Path.join(Os.tmpdir(), 'totaljs' + F.directory.hash())); + process.env.NODE_TLS_REJECT_UNAUTHORIZED = CONF.allow_ssc_validation === false ? '0' : '1'; - if (!F.config['etag-version']) - F.config['etag-version'] = F.config.version.replace(/\.|\s/g, ''); + if (!CONF.directory_temp) + CONF.directory_temp = '~' + U.path(Path.join(Os.tmpdir(), 'totaljs' + F.directory.hash())); - if (F.config['default-timezone']) - process.env.TZ = F.config['default-timezone']; + if (!CONF.etag_version) + CONF.etag_version = CONF.version.replace(/\.|\s/g, ''); - accepts && accepts.length && accepts.forEach(accept => F.config['static-accepts'][accept] = true); + if (CONF.default_timezone) + process.env.TZ = CONF.default_timezone; - if (F.config['disable-strict-server-certificate-validation'] === true) - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + CONF.nosql_worker && framework_nosql.worker(); + CONF.nosql_inmemory && CONF.nosql_inmemory.forEach(framework_nosql.inmemory); + accepts && accepts.length && accepts.forEach(accept => CONF.static_accepts[accept] = true); - if (F.config['allow-performance']) + if (CONF.allow_performance) http.globalAgent.maxSockets = 9999; - var xpowered = F.config['default-xpoweredby']; + QUERYPARSEROPTIONS.maxKeys = CONF.default_request_maxkeys || 33; + + var xpowered = CONF.default_xpoweredby; Object.keys(HEADERS).forEach(function(key) { Object.keys(HEADERS[key]).forEach(function(subkey) { if (RELEASE && subkey === 'Cache-Control') - HEADERS[key][subkey] = HEADERS[key][subkey].replace(/max-age=\d+/, 'max-age=' + F.config['default-response-maxage']); + HEADERS[key][subkey] = HEADERS[key][subkey].replace(/max-age=\d+/, 'max-age=' + CONF.default_response_maxage); if (subkey === 'X-Powered-By') { if (xpowered) HEADERS[key][subkey] = xpowered; @@ -8513,19 +10526,27 @@ F.$configure_configs = function(arr, rewrite) { }); }); - IMAGEMAGICK = F.config['default-image-converter'] === 'im'; done(); - F.emit('configure', F.config); + EMIT('configure', CONF); return F; }; +function obsolete_config(name) { + if (name.indexOf('-') === -1) + return name; + var n = name.replace(REG_OLDCONF, '_'); + OBSOLETE('config[\'' + name + '\']', 'Replace key "{0}" to "{1}" in your config file'.format(name, n)); + return n; +} + /** * Create URL: JavaScript (according to config['static-url-script']) * @param {String} name * @return {String} */ F.routeScript = function(name, theme) { - return F.$routeStatic(name, F.config['static-url-script'], theme); + OBSOLETE('F.routeScript()', 'Renamed to F.public_js'); + return F.$public(name, CONF.static_url_script, theme); }; /** @@ -8534,30 +10555,64 @@ F.routeScript = function(name, theme) { * @return {String} */ F.routeStyle = function(name, theme) { - return F.$routeStatic(name, F.config['static-url-style'], theme); + OBSOLETE('F.routeStyle()', 'Renamed to F.public_css'); + return F.$public(name, CONF.static_url_style, theme); }; F.routeImage = function(name, theme) { - return F.$routeStatic(name, F.config['static-url-image'], theme); + OBSOLETE('F.routeImage()', 'Renamed to F.public_image'); + return F.$public(name, CONF.static_url_image, theme); }; F.routeVideo = function(name, theme) { - return F.$routeStatic(name, F.config['static-url-video'], theme); + OBSOLETE('F.routeVideo()', 'Renamed to F.public_video'); + return F.$public(name, CONF.static_url_video, theme); }; F.routeFont = function(name, theme) { - return F.$routeStatic(name, F.config['static-url-font'], theme); + OBSOLETE('F.routeFont()', 'Renamed to F.public_font'); + return F.$public(name, CONF.static_url_font, theme); }; F.routeDownload = function(name, theme) { - return F.$routeStatic(name, F.config['static-url-download'], theme); + OBSOLETE('F.routeDownload()', 'Renamed to F.public_download'); + return F.$public(name, CONF.static_url_download, theme); }; F.routeStatic = function(name, theme) { - return F.$routeStatic(name, F.config['static-url'], theme); + OBSOLETE('F.routeStatic()', 'Renamed to F.public'); + return F.$public(name, CONF.static_url, theme); +}; + +F.public_js = function(name, theme) { + return F.$public(name, CONF.static_url_script, theme); +}; + +F.public_css = function(name, theme) { + return F.$public(name, CONF.static_url_style, theme); +}; + +F.public_image = function(name, theme) { + return F.$public(name, CONF.static_url_image, theme); +}; + +F.public_video = function(name, theme) { + return F.$public(name, CONF.static_url_video, theme); +}; + +F.public_font = function(name, theme) { + return F.$public(name, CONF.static_url_font, theme); +}; + +F.public_download = function(name, theme) { + return F.$public(name, CONF.static_url_download, theme); +}; + +F.public = function(name, theme) { + return F.$public(name, CONF.static_url, theme); }; -F.$routeStatic = function(name, directory, theme) { +F.$public = function(name, directory, theme) { var key = name + directory + '$' + theme; var val = F.temporary.other[key]; if (RELEASE && val) @@ -8573,7 +10628,7 @@ F.$routeStatic = function(name, directory, theme) { if (index !== -1) { theme = name.substring(1, index); if (theme === '?') { - theme = F.config['default-theme']; + theme = CONF.default_theme; name = name.substring(index); } else name = name.substring(index + 1); @@ -8585,22 +10640,26 @@ F.$routeStatic = function(name, directory, theme) { if (REG_ROUTESTATIC.test(name)) filename = name; else if (name[0] === '/') - filename = U.join(theme, F.$version(name)); + filename = U.join(theme, F.$version(name, true)); else { - filename = U.join(theme, directory, F.$version(name)); + filename = U.join(theme, directory, F.$version(name, true)); if (REG_HTTPHTTPS.test(filename) && filename[0] === '/') filename = filename.substring(1); } - return F.temporary.other[key] = framework_internal.preparePath(F.$version(filename)); + return F.temporary.other[key] = F.$version(framework_internal.preparePath(filename), true); }; -F.$version = function(name) { +F.$version = function(name, def) { + var tmp; + if (F.versions) - name = F.versions[name] || name; + tmp = F.versions[name] || name; + if (F.onVersion) - name = F.onVersion(name) || name; - return name; + tmp = F.onVersion(name) || name; + + return tmp === 'auto' && def ? name : (tmp || name); }; F.$versionprepare = function(html) { @@ -8618,7 +10677,7 @@ F.$versionprepare = function(html) { end = 6; var name = src.substring(end, src.length - 1); - html = html.replace(match[i], src.substring(0, end) + F.$version(name) + '"'); + html = html.replace(match[i], src.substring(0, end) + F.$version(name, true) + '"'); } return html; @@ -8649,15 +10708,14 @@ F.lookup = function(req, url, flags, membertype) { req.$isAuthorized = true; if (!isSystem) { - key = '1' + url + '$' + membertype + req.$flags + (subdomain ? '$' + subdomain : ''); + key = '1' + url + '$' + membertype + req.$flags + (subdomain ? '$' + subdomain : '') + (req.$roles ? 'R' : ''); if (F.temporary.other[key]) return F.temporary.other[key]; } - var length = F.routes.web.length; - for (var i = 0; i < length; i++) { - + for (var i = 0; i < F.routes.web.length; i++) { var route = F.routes.web[i]; + if (route.CUSTOM) { if (!route.CUSTOM(url, req, flags)) continue; @@ -8707,7 +10765,7 @@ F.lookup = function(req, url, flags, membertype) { continue; } - if (key && route.isCACHE && req.$isAuthorized) + if (key && route.isCACHE && (req.$isAuthorized || membertype === 1)) F.temporary.other[key] = route; return route; @@ -8716,6 +10774,55 @@ F.lookup = function(req, url, flags, membertype) { return null; }; +F.lookupaction = function(req, url) { + + var isSystem = url[0] === '#'; + if (isSystem) + return F.routes.system[url]; + + var length = F.routes.web.length; + for (var i = 0; i < length; i++) { + + var route = F.routes.web[i]; + if (route.method !== req.method) + continue; + + if (route.CUSTOM) { + if (!route.CUSTOM(url, req)) + continue; + } else { + if (route.isWILDCARD) { + if (!framework_internal.routeCompare(req.path, route.url, isSystem, true)) + continue; + } else { + if (!framework_internal.routeCompare(req.path, route.url, isSystem)) + continue; + } + } + + if (isSystem) { + if (route.isSYSTEM) + return route; + continue; + } + + if (route.isPARAM && route.regexp) { + var skip = false; + for (var j = 0, l = route.regexpIndexer.length; j < l; j++) { + var p = req.path[route.regexpIndexer[j]]; + if (p === undefined || !route.regexp[route.regexpIndexer[j]].test(p)) { + skip = true; + break; + } + } + if (skip) + continue; + } + return route; + } +}; + + F.lookup_websocket = function(req, url, membertype) { var subdomain = F._length_subdomain_websocket && req.subdomain ? req.subdomain.join('.') : null; @@ -8786,11 +10893,15 @@ F.lookup_websocket = function(req, url, membertype) { F.accept = function(extension, contentType) { if (extension[0] === '.') extension = extension.substring(1); - F.config['static-accepts'][extension] = true; + CONF.static_accepts[extension] = true; contentType && U.setContentType(extension, contentType); return F; }; +// A temporary variable for generating Worker ID +// It's faster than Date.now() +var WORKERID = 0; + /** * Run worker * @param {String} name @@ -8799,7 +10910,7 @@ F.accept = function(extension, contentType) { * @param {Array} args Additional arguments, optional. * @return {ChildProcess} */ -F.worker = function(name, id, timeout, args) { +global.WORKER = F.worker = function(name, id, timeout, args, special) { var fork = null; var type = typeof(id); @@ -8827,15 +10938,15 @@ F.worker = function(name, id, timeout, args) { if (fork) return fork; - var filename = name[0] === '@' ? F.path.package(name.substring(1)) : U.combine(F.config['directory-workers'], name); + var filename = name[0] === '@' ? F.path.package(name.substring(1)) : U.combine(CONF.directory_workers, name); if (!args) - args = []; + args = EMPTYARRAY; - fork = Child.fork(filename[filename.length - 3] === '.' ? filename : filename + '.js', args, HEADERS.workers); + fork = Child.fork(filename[filename.length - 3] === '.' ? filename : filename + '.js', args, special ? HEADERS.workers2 : HEADERS.workers); if (!id) - id = name + '_' + new Date().getTime(); + id = name + '_' + (WORKERID++); fork.__id = id; F.workers[id] = fork; @@ -8860,7 +10971,7 @@ F.worker = function(name, id, timeout, args) { return fork; }; -F.worker2 = function(name, args, callback, timeout) { +global.WORKER2 = F.worker2 = function(name, args, callback, timeout) { if (typeof(args) === 'function') { timeout = callback; @@ -8875,18 +10986,26 @@ F.worker2 = function(name, args, callback, timeout) { if (args && !(args instanceof Array)) args = [args]; - var fork = F.worker(name, name, timeout, args); + var fork = F.worker(name, null, timeout, args, true); if (fork.__worker2) return fork; + var output = Buffer.alloc(0); + fork.__worker2 = true; fork.on('error', function(e) { - callback && callback(e); + callback && callback(e, output); callback = null; }); + fork.stdout.on('data', function(data) { + CONCAT[0] = output; + CONCAT[1] = data; + output = Buffer.concat(CONCAT); + }); + fork.on('exit', function() { - callback && callback(); + callback && callback(null, output); callback = null; }); @@ -8899,7 +11018,7 @@ F.worker2 = function(name, args, callback, timeout) { * @param {Boolean} enable Enable waiting (optional, default: by the current state). * @return {Boolean} */ -F.wait = function(name, enable) { +global.PAUSESERVER = F.wait = function(name, enable) { if (!F.waits) F.waits = {}; @@ -8924,6 +11043,90 @@ F.wait = function(name, enable) { return enable === true; }; +global.UPDATE = function(versions, callback, pauseserver, noarchive) { + + if (typeof(version) === 'function') { + callback = versions; + versions = CONF.version; + } + + if (typeof(callback) === 'string') { + pauseserver = callback; + callback = null; + } + + if (!(versions instanceof Array)) + versions = [versions]; + + pauseserver && PAUSESERVER(pauseserver); + + if (F.id && F.id !== '0') { + if (callback || pauseserver) { + ONCE('update', function() { + callback && callback(); + pauseserver && PAUSESERVER(pauseserver); + }); + } + return; + } + + var errorbuilder = new ErrorBuilder(); + + versions.wait(function(version, next) { + + var filename = PATH.updates(version + '.js'); + var response; + + try { + response = Fs.readFileSync(filename); + } catch (e) { + next(); + return; + } + + var opt = {}; + opt.version = version; + opt.callback = function(err) { + err && errorbuilder.push(err); + + if (!noarchive) + Fs.renameSync(filename, filename + '_bk'); + + next(); + }; + + opt.done = function(arg) { + return function(err) { + if (err) { + opt.callback(err); + } else if (arg) + opt.callback(); + else + opt.callback(); + }; + }; + + opt.success = function() { + opt.callback(null); + }; + + opt.invalid = function(err) { + opt.callback(err); + }; + + var fn = new Function('$', response); + fn(opt, response.toString('utf8')); + + }, function() { + var err = errorbuilder.length ? errorbuilder : null; + callback && callback(err); + if (F.isCluster && F.id && F.id !== '0') + process.send('total:update'); + pauseserver && PAUSESERVER(pauseserver); + EMIT('update', err); + }); +}; + // ================================================================================= // Framework route // ================================================================================= @@ -8962,35 +11165,43 @@ FrameworkRoute.prototype = { }, get flags() { return this.route.flags || EMPTYARRAY; + }, + set groups(value) { + this.route.groups = value; + }, + get groups() { + return this.route.groups; } }; -FrameworkRoute.prototype.make = function(fn) { +const FrameworkRouteProto = FrameworkRoute.prototype; + +FrameworkRouteProto.make = function(fn) { fn && fn.call(this, this); return this; }; -FrameworkRoute.prototype.setId = function(value) { +FrameworkRouteProto.setId = function(value) { this.route.id = value; return this; }; -FrameworkRoute.prototype.setDecription = function(value) { +FrameworkRouteProto.setDecription = function(value) { this.route.description = value; return this; }; -FrameworkRoute.prototype.setTimeout = function(value) { +FrameworkRouteProto.setTimeout = function(value) { this.route.timeout = value; return this; }; -FrameworkRoute.prototype.setMaxLength = function(value) { +FrameworkRouteProto.setMaxLength = function(value) { this.route.length = value; return this; }; -FrameworkRoute.prototype.setOptions = function(value) { +FrameworkRouteProto.setOptions = function(value) { this.route.options = value; return this; }; @@ -9000,151 +11211,193 @@ FrameworkRoute.prototype.setOptions = function(value) { // ================================================================================= function FrameworkPath() {} +const FrameworkPathProto = FrameworkPath.prototype; -FrameworkPath.prototype.verify = function(name) { +FrameworkPathProto.verify = function(name) { var prop = '$directory-' + name; if (F.temporary.path[prop]) return F; - var directory = F.config['directory-' + name] || name; + var directory = CONF['directory_' + name] || name; var dir = U.combine(directory); - !existsSync(dir) && Fs.mkdirSync(dir); + try { + !existsSync(dir) && Fs.mkdirSync(dir); + } catch (e) {} F.temporary.path[prop] = true; return F; }; -FrameworkPath.prototype.mkdir = function(p) { +FrameworkPathProto.mkdir = function(p, cache) { - if (p[0] === '/') - p = p.substring(1); + var key = '$directory-' + p; + + if (cache && F.temporary.path[key]) + return F; + + F.temporary.path[key] = true; var is = F.isWindows; + var s = ''; + + if (p[0] === '/') { + s = is ? '\\' : '/'; + p = p.substring(1); + } + var l = p.length - 1; + var beg = 0; if (is) { if (p[l] === '\\') p = p.substring(0, l); + + if (p[1] === ':') + beg = 1; + } else { if (p[l] === '/') p = p.substring(0, l); } + if (existsSync(p)) + return F; + var arr = is ? p.replace(/\//g, '\\').split('\\') : p.split('/'); - var directory = is ? '' : '/'; + var directory = s; for (var i = 0, length = arr.length; i < length; i++) { var name = arr[i]; if (is) - directory += (directory ? '\\' : '') + name; + directory += (i && directory ? '\\' : '') + name; else - directory += (directory ? '/' : '') + name; - !existsSync(directory) && Fs.mkdirSync(directory); + directory += (i && directory ? '/' : '') + name; + + if (i >= beg && !existsSync(directory)) + Fs.mkdirSync(directory); } + + return F; }; -FrameworkPath.prototype.exists = function(path, callback) { +FrameworkPathProto.exists = function(path, callback) { Fs.lstat(path, (err, stats) => callback(err ? false : true, stats ? stats.size : 0, stats ? stats.isFile() : false)); return F; }; -FrameworkPath.prototype.public = function(filename) { - return U.combine(F.config['directory-public'], filename); +FrameworkPathProto.public = function(filename) { + return U.combine(CONF.directory_public, filename); }; -FrameworkPath.prototype.public_cache = function(filename) { +FrameworkPathProto.public_cache = function(filename) { var key = 'public_' + filename; var item = F.temporary.other[key]; - return item ? item : F.temporary.other[key] = U.combine(F.config['directory-public'], filename); + return item ? item : F.temporary.other[key] = U.combine(CONF.directory_public, filename); }; -FrameworkPath.prototype.private = function(filename) { - return U.combine(F.config['directory-private'], filename); +FrameworkPathProto.private = function(filename) { + return U.combine(CONF.directory_private, filename); }; -FrameworkPath.prototype.isomorphic = function(filename) { - return U.combine(F.config['directory-isomorphic'], filename); +FrameworkPathProto.isomorphic = function(filename) { + return U.combine(CONF.directory_isomorphic, filename); }; -FrameworkPath.prototype.configs = function(filename) { - return U.combine(F.config['directory-configs'], filename); +FrameworkPathProto.configs = function(filename) { + return U.combine(CONF.directory_configs, filename); }; -FrameworkPath.prototype.virtual = function(filename) { - return U.combine(F.config['directory-public-virtual'], filename); +FrameworkPathProto.virtual = function(filename) { + return U.combine(CONF.directory_public_virtual, filename); }; -FrameworkPath.prototype.logs = function(filename) { +FrameworkPathProto.logs = function(filename) { this.verify('logs'); - return U.combine(F.config['directory-logs'], filename); + return U.combine(CONF.directory_logs, filename); }; -FrameworkPath.prototype.models = function(filename) { - return U.combine(F.config['directory-models'], filename); +FrameworkPathProto.models = function(filename) { + return U.combine(CONF.directory_models, filename); }; -FrameworkPath.prototype.temp = function(filename) { +FrameworkPathProto.temp = function(filename) { this.verify('temp'); - return U.combine(F.config['directory-temp'], filename); + return U.combine(CONF.directory_temp, filename); }; -FrameworkPath.prototype.temporary = function(filename) { +FrameworkPathProto.temporary = function(filename) { return this.temp(filename); }; -FrameworkPath.prototype.views = function(filename) { - return U.combine(F.config['directory-views'], filename); +FrameworkPathProto.views = function(filename) { + return U.combine(CONF.directory_views, filename); +}; + +FrameworkPathProto.updates = function(filename) { + return U.combine(CONF.directory_updates, filename); }; -FrameworkPath.prototype.workers = function(filename) { - return U.combine(F.config['directory-workers'], filename); +FrameworkPathProto.workers = function(filename) { + return U.combine(CONF.directory_workers, filename); }; -FrameworkPath.prototype.databases = function(filename) { +FrameworkPathProto.databases = function(filename) { this.verify('databases'); - return U.combine(F.config['directory-databases'], filename); + return U.combine(CONF.directory_databases, filename); +}; + +FrameworkPathProto.modules = function(filename) { + return U.combine(CONF.directory_modules, filename); +}; + +FrameworkPathProto.schemas = function(filename) { + return U.combine(CONF.directory_schemas, filename); }; -FrameworkPath.prototype.modules = function(filename) { - return U.combine(F.config['directory-modules'], filename); +FrameworkPathProto.operations = function(filename) { + return U.combine(CONF.directory_operations, filename); }; -FrameworkPath.prototype.controllers = function(filename) { - return U.combine(F.config['directory-controllers'], filename); +FrameworkPathProto.tasks = function(filename) { + return U.combine(CONF.directory_tasks, filename); }; -FrameworkPath.prototype.definitions = function(filename) { - return U.combine(F.config['directory-definitions'], filename); +FrameworkPathProto.controllers = function(filename) { + return U.combine(CONF.directory_controllers, filename); }; -FrameworkPath.prototype.tests = function(filename) { - return U.combine(F.config['directory-tests'], filename); +FrameworkPathProto.definitions = function(filename) { + return U.combine(CONF.directory_definitions, filename); }; -FrameworkPath.prototype.resources = function(filename) { - return U.combine(F.config['directory-resources'], filename); +FrameworkPathProto.tests = function(filename) { + return U.combine(CONF.directory_tests, filename); }; -FrameworkPath.prototype.services = function(filename) { - return U.combine(F.config['directory-services'], filename); +FrameworkPathProto.resources = function(filename) { + return U.combine(CONF.directory_resources, filename); }; -FrameworkPath.prototype.packages = function(filename) { - return U.combine(F.config['directory-packages'], filename); +FrameworkPathProto.services = function(filename) { + return U.combine(CONF.directory_services, filename); }; -FrameworkPath.prototype.themes = function(filename) { - return U.combine(F.config['directory-themes'], filename); +FrameworkPathProto.packages = function(filename) { + return U.combine(CONF.directory_packages, filename); }; -FrameworkPath.prototype.components = function(filename) { - return U.combine(F.config['directory-components'], filename); +FrameworkPathProto.themes = function(filename) { + return U.combine(CONF.directory_themes, filename); }; -FrameworkPath.prototype.root = function(filename) { +FrameworkPathProto.components = function(filename) { + return U.combine(CONF.directory_components, filename); +}; + +FrameworkPathProto.root = function(filename) { var p = Path.join(directory, filename || ''); return F.isWindows ? p.replace(/\\/g, '/') : p; }; -FrameworkPath.prototype.package = function(name, filename) { +FrameworkPathProto.package = function(name, filename) { if (filename === undefined) { var index = name.indexOf('/'); @@ -9154,7 +11407,7 @@ FrameworkPath.prototype.package = function(name, filename) { } } - var tmp = F.config['directory-temp']; + var tmp = CONF.directory_temp; var p = tmp[0] === '~' ? Path.join(tmp.substring(1), name + '.package', filename || '') : Path.join(directory, tmp, name + '.package', filename || ''); return F.isWindows ? p.replace(REG_WINDOWSPATH, '/') : p; }; @@ -9167,25 +11420,38 @@ function FrameworkCache() { this.items = {}; this.count = 1; this.interval; + this.$sync = true; } -FrameworkCache.prototype.init = function() { +const FrameworkCacheProto = FrameworkCache.prototype; + +FrameworkCacheProto.init = function(notimer) { var self = this; - clearInterval(self.interval); - self.interval = setInterval(() => F.cache.recycle(), 1000 * 60); - if (F.config['allow-cache-snapshot']) - self.load(() => self.loadPersist()); + + if (!notimer) + self.init_timer(); + + if (CONF.allow_cache_snapshot) + self.load(() => self.loadpersistent()); else - self.loadPersist(); + self.loadpersistent(); + + return self; +}; + +FrameworkCacheProto.init_timer = function() { + var self = this; + self.interval && clearInterval(self.interval); + self.interval = setInterval(() => F.cache.recycle(), 1000 * 60); return self; }; -FrameworkCache.prototype.save = function() { +FrameworkCacheProto.save = function() { Fs.writeFile(F.path.temp((F.id ? 'i-' + F.id + '_' : '') + 'framework_cachesnapshot.jsoncache'), JSON.stringify(this.items), NOOP); return this; }; -FrameworkCache.prototype.load = function(callback) { +FrameworkCacheProto.load = function(callback) { var self = this; Fs.readFile(F.path.temp((F.id ? 'i-' + F.id + '_' : '') + 'framework_cachesnapshot.jsoncache'), function(err, data) { if (!err) { @@ -9199,7 +11465,7 @@ FrameworkCache.prototype.load = function(callback) { return self; }; -FrameworkCache.prototype.savePersist = function() { +FrameworkCacheProto.savepersistent = function() { setTimeout2('framework_cachepersist', function(self) { var keys = Object.keys(self.items); var obj = {}; @@ -9207,7 +11473,7 @@ FrameworkCache.prototype.savePersist = function() { for (var i = 0, length = keys.length; i < length; i++) { var key = keys[i]; var item = self.items[key]; - if (item.persist) + if (item && item.persist) obj[key] = item; } @@ -9216,7 +11482,7 @@ FrameworkCache.prototype.savePersist = function() { return this; }; -FrameworkCache.prototype.loadPersist = function(callback) { +FrameworkCacheProto.loadpersistent = function(callback) { var self = this; Fs.readFile(F.path.temp((F.id ? 'i-' + F.id + '_' : '') + 'framework_cachepersist.jsoncache'), function(err, data) { if (!err) { @@ -9226,7 +11492,7 @@ FrameworkCache.prototype.loadPersist = function(callback) { for (var i = 0, length = keys.length; i < length; i++) { var key = keys[i]; var item = data[key]; - if (item.expire >= F.datetime) + if (item.expire >= NOW) self.items[key] = item; } } catch (e) {} @@ -9236,64 +11502,67 @@ FrameworkCache.prototype.loadPersist = function(callback) { return self; }; -FrameworkCache.prototype.stop = function() { +FrameworkCacheProto.stop = function() { clearInterval(this.interval); return this; }; -FrameworkCache.prototype.clear = function(sync) { +FrameworkCacheProto.clear = function() { this.items = {}; - F.isCluster && sync !== false && process.send(CLUSTER_CACHE_CLEAR); - this.savePersist(); + F.isCluster && CONF.allow_cache_cluster && process.send(CLUSTER_CACHE_CLEAR); + this.savepersistent(); return this; }; -FrameworkCache.prototype.recycle = function() { +FrameworkCacheProto.recycle = function() { var items = this.items; - var isPersist = false; - F.datetime = new Date(); + var persistent = false; + NOW = new Date(); this.count++; for (var o in items) { var value = items[o]; if (!value) delete items[o]; - else if (value.expire < F.datetime) { + else if (value.expire < NOW) { if (value.persist) - isPersist = true; - F.emit('cache-expire', o, value.value); + persistent = true; + F.$events['cache-expire'] && EMIT('cache-expire', o, value.value); + F.$events.cache_expired && EMIT('cache_expired', o, value.value); delete items[o]; } } - isPersist && this.savePersist(); - F.config['allow-cache-snapshot'] && this.save(); + persistent && this.savepersistent(); + CONF.allow_cache_snapshot && this.save(); F.service(this.count); + CONF.allow_stats_snapshot && F.snapshotstats && F.snapshotstats(); + F.temporary.service.usage = 0; + measure_usage(); return this; }; -FrameworkCache.prototype.set2 = function(name, value, expire, sync) { - return this.set(name, value, expire, sync, true); +FrameworkCacheProto.set2 = function(name, value, expire) { + return this.set(name, value, expire, true); }; -FrameworkCache.prototype.set = FrameworkCache.prototype.add = function(name, value, expire, sync, persist) { - var type = typeof(expire); +FrameworkCacheProto.set = FrameworkCacheProto.add = function(name, value, expire, persist) { - if (F.isCluster && sync !== false) { - CLUSTER_CACHE_SET.key = name; + if (F.isCluster && CONF.allow_cache_cluster && this.$sync) { + CLUSTER_CACHE_SET.name = name; CLUSTER_CACHE_SET.value = value; CLUSTER_CACHE_SET.expire = expire; process.send(CLUSTER_CACHE_SET); } - switch (type) { + switch (typeof(expire)) { case 'string': expire = expire.parseDateExpiration(); break; case 'undefined': - expire = F.datetime.add('m', 5); + expire = NOW.add('m', 5); break; } @@ -9301,72 +11570,75 @@ FrameworkCache.prototype.set = FrameworkCache.prototype.add = function(name, val if (persist) { obj.persist = true; - this.savePersist(); + this.savepersistent(); } this.items[name] = obj; - F.$events['cache-set'] && F.emit('cache-set', name, value, expire, sync !== false); + F.$events['cache-set'] && EMIT('cache-set', name, value, expire, this.$sync); + F.$events.cache_set && EMIT('cache_set', name, value, expire, this.$sync); return value; }; -FrameworkCache.prototype.read = FrameworkCache.prototype.get = function(key, def) { +FrameworkCacheProto.read = FrameworkCacheProto.get = function(key, def) { var value = this.items[key]; if (!value) return def; - F.datetime = new Date(); + NOW = new Date(); - if (value.expire < F.datetime) { + if (value.expire < NOW) { this.items[key] = undefined; - F.$events['cache-expire'] && F.emit('cache-expire', key, value.value); + F.$events['cache-expire'] && EMIT('cache-expire', key, value.value); + F.$events.cache_expired && EMIT('cache_expired', key, value.value); return def; } return value.value; }; -FrameworkCache.prototype.read2 = FrameworkCache.prototype.get2 = function(key, def) { +FrameworkCacheProto.read2 = FrameworkCacheProto.get2 = function(key, def) { var value = this.items[key]; if (!value) return def; - if (value.expire < F.datetime) { + if (value.expire < NOW) { this.items[key] = undefined; - F.$events['cache-expire'] && F.emit('cache-expire', key, value.value); + F.$events['cache-expire'] && EMIT('cache-expire', key, value.value); + F.$events.cache_expired && EMIT('cache_expired', key, value.value); return def; } return value.value; }; -FrameworkCache.prototype.setExpire = function(name, expire) { +FrameworkCacheProto.setExpire = function(name, expire) { var obj = this.items[name]; if (obj) obj.expire = typeof(expire) === 'string' ? expire.parseDateExpiration() : expire; return this; }; -FrameworkCache.prototype.remove = function(name, sync) { +FrameworkCacheProto.remove = function(name) { var value = this.items[name]; if (value) { - this.items[name].persist && this.savePersist(); + this.items[name].persist && this.savepersistent(); this.items[name] = undefined; } - if (F.isCluster && sync !== false) { - CLUSTER_CACHE_REMOVE.key = name; + if (F.isCluster && CONF.allow_cache_cluster && this.$sync) { + CLUSTER_CACHE_REMOVE.name = name; process.send(CLUSTER_CACHE_REMOVE); } return value; }; -FrameworkCache.prototype.removeAll = function(search, sync) { +FrameworkCacheProto.removeAll = function(search) { var count = 0; - var isReg = U.isRegExp(search); + var isReg = typeof(search) === 'object'; for (var key in this.items) { @@ -9382,28 +11654,28 @@ FrameworkCache.prototype.removeAll = function(search, sync) { count++; } - if (F.isCluster && sync !== false) { - CLUSTER_CACHE_REMOVEALL.key = search; + if (F.isCluster && CONF.allow_cache_cluster && this.$sync) { + CLUSTER_CACHE_REMOVEALL.search = search; process.send(CLUSTER_CACHE_REMOVEALL); } return count; }; -FrameworkCache.prototype.fn = function(name, fnCache, fnCallback) { +FrameworkCacheProto.fn = function(name, fnCache, fnCallback, options) { var self = this; var value = self.read2(name); if (value) { - fnCallback && fnCallback(value, true); + fnCallback && fnCallback(value, true, options); return self; } fnCache(function(value, expire) { self.add(name, value, expire); - fnCallback && fnCallback(value, false); - }); + fnCallback && fnCallback(value, false, options); + }, options); return self; }; @@ -9414,6 +11686,8 @@ function subscribe_timeout(req) { } function subscribe_timeout_middleware(req) { + if (req.$total_middleware) + req.$total_middleware = null; req.$total_execute2(); } @@ -9438,6 +11712,7 @@ function Controller(name, req, res, currentView) { if (req) { this.language = req.$language; this.req = req; + this.route = req.$total_route; } else this.req = EMPTYREQUEST; @@ -9445,8 +11720,8 @@ function Controller(name, req, res, currentView) { // controller.type === 1 - server sent events // this.type = 0; - // this.layoutName = F.config['default-layout']; - // this.themeName = F.config['default-theme']; + // this.layoutName =CONF.default_layout; + // this.themeName =CONF.default_theme; // this.status = 200; // this.isLayout = false; @@ -9473,6 +11748,10 @@ function Controller(name, req, res, currentView) { Controller.prototype = { + get breadcrumb() { + return this.repository[REPOSITORY_SITEMAP]; + }, + get repository() { if (this.$repository) return this.$repository; @@ -9485,30 +11764,31 @@ Controller.prototype = { }, get schema() { - return this.req.$total_route.schema[0] === 'default' ? this.req.$total_route.schema[1] : this.req.$total_route.schema.join('/'); + return this.route.schema ? this.route.schema[0] === 'default' ? this.route.schema[1] : this.route.schema.join('/') : ''; }, get workflow() { - return this.req.$total_route.schema_workflow; + return this.route.schema_workflow; }, get sseID() { return this.req.headers['last-event-id'] || null; }, - get route() { - return this.req.$total_route; + get options() { + return this.route.options; }, - get options() { - return this.req.$total_route.options; + get split() { + return this.req.split; }, get flags() { - return this.req.$total_route.flags; + return this.route.flags; }, get path() { + OBSOLETE('controller.path', 'Use: PATH'); return F.path; }, @@ -9516,10 +11796,18 @@ Controller.prototype = { return this.req.query; }, + set query(val) { + this.req.query = val; + }, + get body() { return this.req.body; }, + set body(val) { + this.req.body = val; + }, + get files() { return this.req.files; }, @@ -9536,6 +11824,10 @@ Controller.prototype = { return this.req.xhr; }, + set xhr(val) { + this.req.xhr = val; + }, + get url() { return U.path(this.req.uri.pathname); }, @@ -9544,31 +11836,32 @@ Controller.prototype = { return this.req.uri; }, + get headers() { + return this.req.headers; + }, + get cache() { + OBSOLETE('controller.cache', 'Use: F.cache or CACHE()'); return F.cache; }, get config() { - return F.config; + OBSOLETE('controller.config', 'Use: CONF'); + return CONF; }, get controllers() { + OBSOLETE('controller.controllers', 'This property will be removed in v4.'); return F.controllers; }, - get isProxy() { - return this.req.isProxy === true; - }, - - get isDebug() { - return F.config.debug; - }, - get isTest() { + OBSOLETE('controller.isTest', 'Use: F.isTest'); return this.req.headers['x-assertion-testing'] === '1'; }, get isSecure() { + OBSOLETE('controller.isSecure', 'Use: controller.secured'); return this.req.isSecure; }, @@ -9600,10 +11893,22 @@ Controller.prototype = { return this.req.mobile; }, + set mobile(val) { + this.req.mobile = val; + }, + get robot() { return this.req.robot; }, + get sessionid() { + return this.req.sessionid; + }, + + set sessionid(val) { + this.req.sessionid = val; + }, + get viewname() { var name = this.req.path[this.req.path.length - 1]; return !name || name === '/' ? 'index' : name; @@ -9616,7 +11921,7 @@ Controller.prototype = { get params() { if (this.$params) return this.$params; - var route = this.req.$total_route; + var route = this.route; var names = route.paramnames; if (names) { var obj = {}; @@ -9625,9 +11930,17 @@ Controller.prototype = { this.$params = obj; return obj; } else { - this.$params = EMPTYOBJECT; - return EMPTYOBJECT; + // Because in some cases are overwritten + return this.$params = {}; } + }, + + set params(val) { + this.$params = val; + }, + + get ua() { + return this.req ? this.req.ua : null; } }; @@ -9635,21 +11948,39 @@ Controller.prototype = { // PROTOTYPES // ====================================================== -// Schema operations +const ControllerProto = Controller.prototype; + +ControllerProto.$get = ControllerProto.$read = function(helper, callback) { + + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } -Controller.prototype.$get = Controller.prototype.$read = function(helper, callback) { this.getSchema().get(helper, callback, this); return this; }; -Controller.prototype.$query = function(helper, callback) { +ControllerProto.$query = function(helper, callback) { + + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } + this.getSchema().query(helper, callback, this); return this; }; -Controller.prototype.$save = function(helper, callback) { +ControllerProto.$save = function(helper, callback) { + + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } + var self = this; - if (framework_builders.isSchema(self.body)) { + if (self.body && self.body.$$schema) { self.body.$$controller = self; self.body.$save(helper, callback); } else { @@ -9660,47 +11991,141 @@ Controller.prototype.$save = function(helper, callback) { return self; }; -Controller.prototype.$remove = function(helper, callback) { - var self = this; - self.getSchema().remove(helper, callback, self); - return this; -}; +ControllerProto.$insert = function(helper, callback) { + + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } -Controller.prototype.$workflow = function(name, helper, callback) { var self = this; - if (framework_builders.isSchema(self.body)) { + if (self.body && self.body.$$schema) { self.body.$$controller = self; - self.body.$workflow(name, helper, callback); - } else - self.getSchema().workflow2(name, helper, callback, self); + self.body.$insert(helper, callback); + } else { + var model = self.getSchema().default(); + model.$$controller = self; + model.$insert(helper, callback); + } return self; }; -Controller.prototype.$workflow2 = function(name, helper, callback) { - var self = this; - self.getSchema().workflow2(name, helper, callback, self); - return self; -}; +ControllerProto.$update = function(helper, callback) { + + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } -Controller.prototype.$hook = function(name, helper, callback) { var self = this; - if (framework_builders.isSchema(self.body)) { + if (self.body && self.body.$$schema) { self.body.$$controller = self; - self.body.$hook(name, helper, callback); - } else - self.getSchema().hook2(name, helper, callback, self); + self.body.$update(helper, callback); + } else { + var model = self.getSchema().default(); + model.$$controller = self; + model.$update(helper, callback); + } return self; }; -Controller.prototype.$hook2 = function(name, helper, callback) { - var self = this; - self.getSchema().hook2(name, helper, callback, self); - return self; -}; +ControllerProto.$patch = function(helper, callback) { -Controller.prototype.$transform = function(name, helper, callback) { - var self = this; - if (framework_builders.isSchema(self.body)) { + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } + + var self = this; + if (self.body && self.body.$$schema) { + self.body.$$controller = self; + self.body.$patch(helper, callback); + } else { + var model = self.getSchema().default(); + model.$$controller = self; + model.$patch(helper, callback); + } + return self; +}; + +ControllerProto.$remove = function(helper, callback) { + + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } + + var self = this; + self.getSchema().remove(helper, callback, self); + return this; +}; + +ControllerProto.$workflow = function(name, helper, callback) { + var self = this; + + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } + + if (self.body && self.body.$$schema) { + self.body.$$controller = self; + self.body.$workflow(name, helper, callback); + } else + self.getSchema().workflow2(name, helper, callback, self); + return self; +}; + +ControllerProto.$workflow2 = function(name, helper, callback) { + + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } + + var self = this; + self.getSchema().workflow2(name, helper, callback, self); + return self; +}; + +ControllerProto.$hook = function(name, helper, callback) { + var self = this; + + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = EMPTYOBJECT; + } + + if (self.body && self.body.$$schema) { + self.body.$$controller = self; + self.body.$hook(name, helper, callback); + } else + self.getSchema().hook2(name, helper, callback, self); + + return self; +}; + +ControllerProto.$hook2 = function(name, helper, callback) { + + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = EMPTYOBJECT; + } + + var self = this; + self.getSchema().hook2(name, helper, callback, self); + return self; +}; + +ControllerProto.$transform = function(name, helper, callback) { + + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = EMPTYOBJECT; + } + + var self = this; + if (self.body && self.body.$$schema) { self.body.$$controller = self; self.body.$transform(name, helper, callback); } else @@ -9708,15 +12133,27 @@ Controller.prototype.$transform = function(name, helper, callback) { return self; }; -Controller.prototype.$transform2 = function(name, helper, callback) { +ControllerProto.$transform2 = function(name, helper, callback) { + + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = EMPTYOBJECT; + } + var self = this; self.getSchema().transform2(name, helper, callback, self); return self; }; -Controller.prototype.$operation = function(name, helper, callback) { +ControllerProto.$operation = function(name, helper, callback) { + + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = EMPTYOBJECT; + } + var self = this; - if (framework_builders.isSchema(self.body)) { + if (self.body && self.body.$$schema) { self.body.$$controller = self; self.body.$operation(name, helper, callback); } else @@ -9724,16 +12161,33 @@ Controller.prototype.$operation = function(name, helper, callback) { return self; }; -Controller.prototype.$operation2 = function(name, helper, callback) { +ControllerProto.operation = function(name, value, callback, options) { + OPERATION(name, value, callback, options, this); + return this; +}; + +ControllerProto.tasks = function() { + var tb = new TaskBuilder(this); + // tb.callback(this.callback()); + return tb; +}; + +ControllerProto.$operation2 = function(name, helper, callback) { + + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = EMPTYOBJECT; + } + var self = this; self.getSchema().operation2(name, helper, callback, self); return self; }; -Controller.prototype.$exec = function(name, helper, callback) { +ControllerProto.$exec = function(name, helper, callback) { var self = this; - if (typeof(helper) === 'function') { + if (callback == null && typeof(helper) === 'function') { callback = helper; helper = EMPTYOBJECT; } @@ -9741,7 +12195,7 @@ Controller.prototype.$exec = function(name, helper, callback) { if (callback == null) callback = self.callback(); - if (framework_builders.isSchema(self.body)) { + if (self.body && self.body.$$schema) { self.body.$$controller = self; self.body.$exec(name, helper, callback); return self; @@ -9753,10 +12207,10 @@ Controller.prototype.$exec = function(name, helper, callback) { return self; }; -Controller.prototype.$async = function(callback, index) { +ControllerProto.$async = function(callback, index) { var self = this; - if (framework_builders.isSchema(self.body)) { + if (self.body && self.body.$$schema) { self.body.$$controller = self; return self.body.$async(callback, index); } @@ -9766,11 +12220,11 @@ Controller.prototype.$async = function(callback, index) { return model.$async(callback, index); }; -Controller.prototype.getSchema = function() { - var route = this.req.$total_route; +ControllerProto.getSchema = function() { + var route = this.route; if (!route.schema || !route.schema[1]) throw new Error('The controller\'s route does not define any schema.'); - var schema = GETSCHEMA(route.schema[0], route.schema[1]); + var schema = route.isDYNAMICSCHEMA ? framework_builders.findschema(route.schema[0] + '/' + this.params[route.schema[1]]) : GETSCHEMA(route.schema[0], route.schema[1]); if (schema) return schema; throw new Error('Schema "{0}" does not exist.'.format(route.schema[1])); @@ -9780,19 +12234,28 @@ Controller.prototype.getSchema = function() { * Renders component * @param {String} name A component name * @param {Object} settings Optional, settings. + * @model {Object} settings Optional, model for the component. * @return {String} */ -Controller.prototype.component = function(name, settings) { +ControllerProto.component = function(name, settings, model) { var filename = F.components.views[name]; if (filename) { - var generator = framework_internal.viewEngine(name, filename, this); - if (generator) - return generator.call(this, this, this.repository, this.$model, this.session, this.query, this.body, this.url, F.global, F.helpers, this.user, this.config, F.functions, 0, this.outputPartial, this.req.cookie, this.req.files, this.req.mobile, settings || EMPTYOBJECT); + var self = this; + var generator = framework_internal.viewEngine(name, filename, self, true); + if (generator) { + if (generator.components.length) { + if (!self.repository[REPOSITORY_COMPONENTS]) + self.repository[REPOSITORY_COMPONENTS] = {}; + for (var i = 0; i < generator.components.length; i++) + self.repository[REPOSITORY_COMPONENTS][generator.components[i]] = 1; + } + return generator.call(self, self, self.repository, model || self.$model, self.session, self.query, self.body, self.url, F.global, F.helpers, self.user, CONF, F.functions, 0, self.outputPartial, self.req.files, self.req.mobile, settings || EMPTYOBJECT); + } } return ''; }; -Controller.prototype.$components = function(group, settings) { +ControllerProto.$components = function(group, settings) { if (group) { var keys = Object.keys(F.components.instances); @@ -9800,8 +12263,16 @@ Controller.prototype.$components = function(group, settings) { for (var i = 0, length = keys.length; i < length; i++) { var component = F.components.instances[keys[i]]; if (component.group === group) { - var tmp = this.component(keys[i], settings); - tmp && output.push(tmp); + if (component.render) { + !this.$viewasync && (this.$viewasync = []); + $VIEWASYNC++; + var name = '@{-' + $VIEWASYNC + '-}'; + this.$viewasync.push({ replace: name, name: component.name, settings: settings }); + output.push(name); + } else { + var tmp = this.component(keys[i], settings); + tmp && output.push(tmp); + } } } return output.join('\n'); @@ -9818,7 +12289,7 @@ Controller.prototype.$components = function(group, settings) { * @param {Object} options * @return {String/Controller} */ -Controller.prototype.cookie = function(name, value, expires, options) { +ControllerProto.cookie = function(name, value, expires, options) { var self = this; if (value === undefined) return self.req.cookie(name); @@ -9830,7 +12301,7 @@ Controller.prototype.cookie = function(name, value, expires, options) { * Clears uploaded files * @return {Controller} */ -Controller.prototype.clear = function() { +ControllerProto.clear = function() { var self = this; self.req.clear(); return self; @@ -9841,7 +12312,7 @@ Controller.prototype.clear = function() { * @param {String} text * @return {String} */ -Controller.prototype.translate = function(language, text) { +ControllerProto.translate = function(language, text) { if (!text) { text = language; @@ -9858,7 +12329,7 @@ Controller.prototype.translate = function(language, text) { * @param {Function} callback * @return {Controller} */ -Controller.prototype.middleware = function(names, options, callback) { +ControllerProto.middleware = function(names, options, callback) { if (typeof(names) === 'string') names = [names]; @@ -9873,10 +12344,19 @@ Controller.prototype.middleware = function(names, options, callback) { options = EMPTYOBJECT; var self = this; + + if (self.req.$total_middleware) + self.req.$total_middleware = null; + async_middleware(0, self.req, self.res, names, () => callback && callback(), options, self); return self; }; +ControllerProto.nocache = function() { + this.req.nocache(); + return this; +}; + /** * Creates a pipe between the current request and target URL * @param {String} url @@ -9884,16 +12364,16 @@ Controller.prototype.middleware = function(names, options, callback) { * @param {Function(err)} callback Optional. * @return {Controller} */ -Controller.prototype.pipe = function(url, headers, callback) { +ControllerProto.pipe = function(url, headers, callback) { this.res.proxy(url, headers, null, callback); return this; }; -Controller.prototype.encrypt = function() { +ControllerProto.encrypt = function() { return F.encrypt.apply(framework, arguments); }; -Controller.prototype.decrypt = function() { +ControllerProto.decrypt = function() { return F.decrypt.apply(framework, arguments); }; @@ -9901,7 +12381,8 @@ Controller.prototype.decrypt = function() { * Creates a hash (alias for F.hash()) * @return {Controller} */ -Controller.prototype.hash = function() { +ControllerProto.hash = function() { + OBSOLETE('controller.hash()', 'Use String.prototype.hash()'); return F.hash.apply(framework, arguments); }; @@ -9911,7 +12392,7 @@ Controller.prototype.hash = function() { * @param {String} value * @return {Controller} */ -Controller.prototype.header = function(name, value) { +ControllerProto.header = function(name, value) { this.res.setHeader(name, value); return this; }; @@ -9921,15 +12402,15 @@ Controller.prototype.header = function(name, value) { * @param {String} path * @return {Controller} */ -Controller.prototype.host = function(path) { +ControllerProto.host = function(path) { return this.req.hostname(path); }; -Controller.prototype.hostname = function(path) { +ControllerProto.hostname = function(path) { return this.req.hostname(path); }; -Controller.prototype.resource = function(name, key) { +ControllerProto.resource = function(name, key) { return F.resource(name, key); }; @@ -9938,7 +12419,7 @@ Controller.prototype.resource = function(name, key) { * @param {Error/String} err * @return {Controller/Function} */ -Controller.prototype.error = function(err) { +ControllerProto.error = function(err) { var self = this; // Custom errors @@ -9956,13 +12437,27 @@ Controller.prototype.error = function(err) { return self; }; -Controller.prototype.invalid = function(status) { +ControllerProto.invalid = function(status) { + var self = this; - if (status) + if (status instanceof ErrorBuilder) { + setImmediate(next_controller_invalid, self, status); + return status; + } + + var type = typeof(status); + + if (type === 'number') self.status = status; var builder = new ErrorBuilder(); + + if (type === 'string') + builder.push(status); + else if (status instanceof Error) + builder.push(status); + setImmediate(next_controller_invalid, self, builder); return builder; }; @@ -9976,7 +12471,7 @@ function next_controller_invalid(self, builder) { * @param {String} message * @return {Controller} */ -Controller.prototype.wtf = Controller.prototype.problem = function(message) { +ControllerProto.wtf = ControllerProto.problem = function(message) { F.problem(message, this.name, this.uri, this.ip); return this; }; @@ -9986,7 +12481,7 @@ Controller.prototype.wtf = Controller.prototype.problem = function(message) { * @param {String} message * @return {Controller} */ -Controller.prototype.change = function(message) { +ControllerProto.change = function(message) { F.change(message, this.name, this.uri, this.ip); return this; }; @@ -9996,7 +12491,7 @@ Controller.prototype.change = function(message) { * @param {String} message * @return {Controller} */ -Controller.prototype.trace = function(message) { +ControllerProto.trace = function(message) { F.trace(message, this.name, this.uri, this.ip); return this; }; @@ -10007,7 +12502,7 @@ Controller.prototype.trace = function(message) { * @param {String Array} flags Route flags (optional). * @return {Boolean} */ -Controller.prototype.transfer = function(url, flags) { +ControllerProto.transfer = function(url, flags) { var self = this; var length = F.routes.web.length; @@ -10048,7 +12543,6 @@ Controller.prototype.transfer = function(url, flags) { break; } - if (!selected) return false; @@ -10061,37 +12555,37 @@ Controller.prototype.transfer = function(url, flags) { // Hidden variable self.req.$path = framework_internal.routeSplit(url, true); - self.req.$total_route = selected; + self.route = self.req.$total_route = selected; self.req.$total_execute(404); return true; }; -Controller.prototype.cancel = function() { +ControllerProto.cancel = function() { this.isCanceled = true; return this; }; -Controller.prototype.log = function() { - F.log.apply(framework, arguments); +ControllerProto.log = function() { + F.log.apply(F, arguments); return this; }; -Controller.prototype.logger = function() { - F.logger.apply(framework, arguments); +ControllerProto.logger = function() { + F.logger.apply(F, arguments); return this; }; -Controller.prototype.meta = function() { +ControllerProto.meta = function() { var self = this; if (arguments[0]) - self.repository[REPOSITORY_META_TITLE] = arguments[0]; + self.repository[REPOSITORY_META_TITLE] = arguments[0].encode(); if (arguments[1]) - self.repository[REPOSITORY_META_DESCRIPTION] = arguments[1]; + self.repository[REPOSITORY_META_DESCRIPTION] = arguments[1].encode(); if (arguments[2] && arguments[2].length) - self.repository[REPOSITORY_META_KEYWORDS] = arguments[2] instanceof Array ? arguments[2].join(', ') : arguments[2]; + self.repository[REPOSITORY_META_KEYWORDS] = (arguments[2] instanceof Array ? arguments[2].join(', ') : arguments[2]); if (arguments[3]) self.repository[REPOSITORY_META_IMAGE] = arguments[3]; @@ -10099,7 +12593,7 @@ Controller.prototype.meta = function() { return self; }; -Controller.prototype.$dns = function() { +ControllerProto.$dns = function() { var builder = ''; var length = arguments.length; @@ -10111,7 +12605,7 @@ Controller.prototype.$dns = function() { return ''; }; -Controller.prototype.$prefetch = function() { +ControllerProto.$prefetch = function() { var builder = ''; var length = arguments.length; @@ -10123,7 +12617,7 @@ Controller.prototype.$prefetch = function() { return ''; }; -Controller.prototype.$prerender = function() { +ControllerProto.$prerender = function() { var builder = ''; var length = arguments.length; @@ -10135,22 +12629,22 @@ Controller.prototype.$prerender = function() { return ''; }; -Controller.prototype.$next = function(value) { +ControllerProto.$next = function(value) { this.head(''); return ''; }; -Controller.prototype.$prev = function(value) { +ControllerProto.$prev = function(value) { this.head(''); return ''; }; -Controller.prototype.$canonical = function(value) { +ControllerProto.$canonical = function(value) { this.head(''); return ''; }; -Controller.prototype.$meta = function() { +ControllerProto.$meta = function() { var self = this; if (arguments.length) { @@ -10158,77 +12652,88 @@ Controller.prototype.$meta = function() { return ''; } - F.$events['controller-render-meta'] && F.emit('controller-render-meta', self); + F.$events['controller-render-meta'] && EMIT('controller-render-meta', self); + F.$events.controller_render_meta && EMIT('controller_render_meta', self); var repository = self.repository; return F.onMeta.call(self, repository[REPOSITORY_META_TITLE], repository[REPOSITORY_META_DESCRIPTION], repository[REPOSITORY_META_KEYWORDS], repository[REPOSITORY_META_IMAGE]); }; -Controller.prototype.title = function(value) { +ControllerProto.title = function(value) { this.$title(value); return this; }; -Controller.prototype.description = function(value) { +ControllerProto.description = function(value) { this.$description(value); return this; }; -Controller.prototype.keywords = function(value) { +ControllerProto.keywords = function(value) { this.$keywords(value); return this; }; -Controller.prototype.author = function(value) { +ControllerProto.author = function(value) { this.$author(value); return this; }; -Controller.prototype.$title = function(value) { +ControllerProto.$title = function(value) { if (value) - this.repository[REPOSITORY_META_TITLE] = value; + this.repository[REPOSITORY_META_TITLE] = value.encode(); return ''; }; -Controller.prototype.$title2 = function(value) { +ControllerProto.$title2 = function(value) { var current = this.repository[REPOSITORY_META_TITLE]; if (value) - this.repository[REPOSITORY_META_TITLE] = (current ? current : '') + value; + this.repository[REPOSITORY_META_TITLE] = (current ? current : '') + value.encode(); return ''; }; -Controller.prototype.$description = function(value) { +ControllerProto.$description = function(value) { if (value) - this.repository[REPOSITORY_META_DESCRIPTION] = value; + this.repository[REPOSITORY_META_DESCRIPTION] = value.encode(); return ''; }; -Controller.prototype.$keywords = function(value) { +ControllerProto.$keywords = function(value) { if (value && value.length) - this.repository[REPOSITORY_META_KEYWORDS] = value instanceof Array ? value.join(', ') : value; + this.repository[REPOSITORY_META_KEYWORDS] = (value instanceof Array ? value.join(', ') : value).encode(); return ''; }; -Controller.prototype.$author = function(value) { +ControllerProto.$author = function(value) { if (value) - this.repository[REPOSITORY_META_AUTHOR] = value; + this.repository[REPOSITORY_META_AUTHOR] = value.encode(); return ''; }; -Controller.prototype.sitemap_navigation = function(name, language) { +ControllerProto.sitemap_navigation = function(name, language) { return F.sitemap_navigation(name || this.sitemapid, language || this.language); }; -Controller.prototype.sitemap_url = function(name, a, b, c, d, e, f) { +ControllerProto.sitemap_url = function(name, a, b, c, d, e, f) { var item = F.sitemap(name || this.sitemapid, true, this.language); return item ? item.url.format(a, b, c, d, e, f) : ''; }; -Controller.prototype.sitemap_name = function(name, a, b, c, d, e, f) { +ControllerProto.sitemap_name = function(name, a, b, c, d, e, f) { var item = F.sitemap(name || this.sitemapid, true, this.language); return item ? item.name.format(a, b, c, d, e, f) : ''; }; -Controller.prototype.sitemap_add = function(parent, name, url) { +ControllerProto.sitemap_url2 = function(language, name, a, b, c, d, e, f) { + var item = F.sitemap(name || this.sitemapid, true, language); + return item ? item.url.format(a, b, c, d, e, f) : ''; +}; + +ControllerProto.sitemap_name2 = function(language, name, a, b, c, d, e, f) { + var item = F.sitemap(name || this.sitemapid, true, language); + return item ? item.name.format(a, b, c, d, e, f) : ''; +}; + +ControllerProto.sitemap_add = function(parent, name, url) { var self = this; var sitemap = self.repository[REPOSITORY_SITEMAP]; @@ -10256,7 +12761,7 @@ Controller.prototype.sitemap_add = function(parent, name, url) { return sitemap; }; -Controller.prototype.sitemap_change = function(name, type, a, b, c, d, e, f) { +ControllerProto.sitemap_change = function(name, type, a, b, c, d, e, f) { var self = this; var sitemap = self.repository[REPOSITORY_SITEMAP]; @@ -10301,7 +12806,7 @@ Controller.prototype.sitemap_change = function(name, type, a, b, c, d, e, f) { return sitemap; }; -Controller.prototype.sitemap_replace = function(name, title, url) { +ControllerProto.sitemap_replace = function(name, title, url) { var self = this; var sitemap = self.repository[REPOSITORY_SITEMAP]; @@ -10341,24 +12846,24 @@ Controller.prototype.sitemap_replace = function(name, title, url) { }; // Arguments: parent, name, url -Controller.prototype.$sitemap_add = function(parent, name, url) { +ControllerProto.$sitemap_add = function(parent, name, url) { this.sitemap_add(parent, name, url); return ''; }; // Arguments: name, type, value, format -Controller.prototype.$sitemap_change = function(a, b, c, d, e, f, g, h) { +ControllerProto.$sitemap_change = function(a, b, c, d, e, f, g, h) { this.sitemap_change(a, b, c, d, e, f, g, h); return ''; }; // Arguments: name, title, url -Controller.prototype.$sitemap_replace =function(a, b, c) { +ControllerProto.$sitemap_replace =function(a, b, c) { this.sitemap_replace(a, b, c); return ''; }; -Controller.prototype.sitemap = function(name) { +ControllerProto.sitemap = function(name) { var self = this; var sitemap; @@ -10381,7 +12886,7 @@ Controller.prototype.sitemap = function(name) { self.repository[REPOSITORY_SITEMAP] = sitemap; if (!self.repository[REPOSITORY_META_TITLE]) { - sitemap = sitemap.last(); + sitemap = sitemap[sitemap.length - 1]; if (sitemap) self.repository[REPOSITORY_META_TITLE] = sitemap.name; } @@ -10390,23 +12895,23 @@ Controller.prototype.sitemap = function(name) { }; // Arguments: name -Controller.prototype.$sitemap = function(name) { +ControllerProto.$sitemap = function(name) { var self = this; self.sitemap(name); return ''; }; -Controller.prototype.module = function(name) { +ControllerProto.module = function(name) { return F.module(name); }; -Controller.prototype.layout = function(name) { +ControllerProto.layout = function(name) { var self = this; self.layoutName = name; return self; }; -Controller.prototype.theme = function(name) { +ControllerProto.theme = function(name) { var self = this; self.themeName = name; return self; @@ -10417,13 +12922,13 @@ Controller.prototype.theme = function(name) { * @param {String} name Layout name * @return {String} */ -Controller.prototype.$layout = function(name) { +ControllerProto.$layout = function(name) { var self = this; self.layoutName = name; return ''; }; -Controller.prototype.model = function(name) { +ControllerProto.model = function(name) { return F.model(name); }; @@ -10436,7 +12941,7 @@ Controller.prototype.model = function(name) { * @param {Function(err)} callback Optional. * @return {MailMessage} */ -Controller.prototype.mail = function(address, subject, view, model, callback) { +ControllerProto.mail = function(address, subject, view, model, callback) { if (typeof(model) === 'function') { callback = model; @@ -10469,91 +12974,55 @@ Controller.prototype.mail = function(address, subject, view, model, callback) { return message; }; -/* - Check if ETag or Last Modified has modified - @compare {String or Date} - @strict {Boolean} :: if strict then use equal date else use great than date (default: false) - - if @compare === {String} compare if-none-match - if @compare === {Date} compare if-modified-since - - return {Boolean}; -*/ -Controller.prototype.notModified = function(compare, strict) { - return F.notModified(this.req, this.res, compare, strict); -}; - -/* - Set last modified header or Etag - @value {String or Date} - - if @value === {String} set ETag - if @value === {Date} set LastModified - - return {Controller}; -*/ -Controller.prototype.setModified = function(value) { - F.setModified(this.req, this.res, value); - return this; -}; - -/** - * Sets expire headers - * @param {Date} date - */ -Controller.prototype.setExpires = function(date) { - date && this.res.setHeader('Expires', date.toUTCString()); - return this; -}; - -Controller.prototype.$template = function(name, model, expire, key) { +ControllerProto.$template = function(name, model, expire, key) { + OBSOLETE('@{template()}', 'The method will be removed in v4'); return this.$viewToggle(true, name, model, expire, key); }; -Controller.prototype.$templateToggle = function(visible, name, model, expire, key) { +ControllerProto.$templateToggle = function(visible, name, model, expire, key) { + OBSOLETE('@{templateToggle()}', 'The method will be removed in v4'); return this.$viewToggle(visible, name, model, expire, key); }; -Controller.prototype.$view = function(name, model, expire, key) { - return this.$viewToggle(true, name, model, expire, key); -}; - -Controller.prototype.$viewCompile = function(body, model, key) { - var self = this; - var layout = self.layoutName; - self.layoutName = ''; - var value = self.viewCompile(body, model, null, true, key); - self.layoutName = layout; - return value || ''; -}; - -Controller.prototype.$viewToggle = function(visible, name, model, expire, key) { - - if (!visible) - return ''; +ControllerProto.$view = function(name, model, expire, key) { var self = this; var cache; if (expire) { cache = '$view.' + name + '.' + (key || ''); - var output = self.cache.read2(cache); + var output = F.cache.read2(cache); if (output) - return output; + return output.body; } - var layout = self.layoutName; - self.layoutName = ''; - var value = self.view(name, model, null, true); - self.layoutName = layout; - + var value = self.view(name, model, null, true, true, cache); if (!value) return ''; - expire && self.cache.add(cache, value, expire, false); + expire && F.cache.add(cache, { components: value instanceof Function, body: value instanceof Function ? '' : value }, expire, false); return value; }; +ControllerProto.$viewCompile = function(body, model, key) { + OBSOLETE('@{viewCompile()}', 'Was renamed to @{view_compile()}.'); + return this.$view_compile(body, model, key); +}; + +ControllerProto.$view_compile = function(body, model, key) { + var self = this; + var layout = self.layoutName; + self.layoutName = ''; + var value = self.view_compile(body, model, null, true, key); + self.layoutName = layout; + return value || ''; +}; + +ControllerProto.$viewToggle = function(visible, name, model, expire, key, async) { + OBSOLETE('@{viewToggle()}', 'The method will be removed in v4'); + return visible ? this.$view(name, model, expire, key, async) : ''; +}; + /** * Adds a place into the places. * @param {String} name A place name. @@ -10562,7 +13031,7 @@ Controller.prototype.$viewToggle = function(visible, name, model, expire, key) { * @param {String} argN A content 2, optional * @return {String/Controller} String is returned when the method contains only `name` argument */ -Controller.prototype.place = function(name) { +ControllerProto.place = function(name) { var key = REPOSITORY_PLACE + '_' + name; var length = arguments.length; @@ -10581,6 +13050,7 @@ Controller.prototype.place = function(name) { switch (U.getExtension(val)) { case 'js': + case 'mjs': val = ''; break; case 'css': @@ -10602,7 +13072,7 @@ Controller.prototype.place = function(name) { * @param {Boolean} replace Optional, default `false` otherwise concats contents. * @return {String/Controller} String is returned when the method contains only `name` argument */ -Controller.prototype.section = function(name, value, replace) { +ControllerProto.section = function(name, value, replace) { var key = '$section_' + name; @@ -10622,7 +13092,7 @@ Controller.prototype.section = function(name, value, replace) { return this; }; -Controller.prototype.$place = function() { +ControllerProto.$place = function() { var self = this; if (arguments.length === 1) return self.place.apply(self, arguments); @@ -10630,22 +13100,30 @@ Controller.prototype.$place = function() { return ''; }; -Controller.prototype.$url = function(host) { +ControllerProto.$url = function(host) { return host ? this.req.hostname(this.url) : this.url; }; // Argument: name -Controller.prototype.$helper = function() { +ControllerProto.$helper = function() { return this.helper.apply(this, arguments); }; -function querystring_encode(value, def) { - return value != null ? value instanceof Date ? encodeURIComponent(value.format()) : typeof(value) === 'string' ? encodeURIComponent(value) : value.toString() : def || ''; +function querystring_encode(value, def, key) { + + if (value instanceof Array) { + var tmp = ''; + for (var i = 1; i < value.length; i++) + tmp += (tmp ? '&' : '') + key + '=' + querystring_encode(value[i], def); + return querystring_encode(value[0], def) + (tmp ? tmp : ''); + } + + return value != null ? value instanceof Date ? encodeURIComponent(value.format()) : typeof(value) === 'string' ? encodeURIComponent(value) : (value + '') : def || ''; } // @{href({ key1: 1, key2: 2 })} // @{href('key', 'value')} -Controller.prototype.href = function(key, value) { +ControllerProto.href = function(key, value) { var self = this; if (!arguments.length) { @@ -10664,21 +13142,26 @@ Controller.prototype.href = function(key, value) { if (!str) { obj = U.copy(self.query); + for (var i = 2; i < arguments.length; i++) obj[arguments[i]] = undefined; obj[key] = '\0'; - var arr = Object.keys(obj); - for (var i = 0, length = arr.length; i < length; i++) { - var val = obj[arr[i]]; - if (val !== undefined) - str += (str ? '&' : '') + arr[i] + '=' + (key === arr[i] ? '\0' : querystring_encode(val)); + for (var k in obj) { + var val = obj[k]; + if (val !== undefined) { + if (val instanceof Array) { + for (var j = 0; j < val.length; j++) + str += (str ? '&' : '') + k + '=' + (key === k ? '\0' : querystring_encode(val[j])); + } else + str += (str ? '&' : '') + k + '=' + (key === k ? '\0' : querystring_encode(val)); + } } self[cachekey] = str; } - str = str.replace('\0', querystring_encode(value, self.query[key])); + str = str.replace('\0', querystring_encode(value, self.query[key], key)); for (var i = 2; i < arguments.length; i++) { var beg = str.indexOf(arguments[i] + '='); @@ -10707,15 +13190,15 @@ Controller.prototype.href = function(key, value) { return self.url + (obj ? '?' + obj : ''); }; -Controller.prototype.$checked = function(bool, charBeg, charEnd) { +ControllerProto.$checked = function(bool, charBeg, charEnd) { return this.$isValue(bool, charBeg, charEnd, 'checked="checked"'); }; -Controller.prototype.$disabled = function(bool, charBeg, charEnd) { +ControllerProto.$disabled = function(bool, charBeg, charEnd) { return this.$isValue(bool, charBeg, charEnd, 'disabled="disabled"'); }; -Controller.prototype.$selected = function(bool, charBeg, charEnd) { +ControllerProto.$selected = function(bool, charBeg, charEnd) { return this.$isValue(bool, charBeg, charEnd, 'selected="selected"'); }; @@ -10726,32 +13209,32 @@ Controller.prototype.$selected = function(bool, charBeg, charEnd) { * return {String} Returns empty string. */ // Argument: value -Controller.prototype.$set = function() { +ControllerProto.$set = function() { return ''; }; -Controller.prototype.$readonly = function(bool, charBeg, charEnd) { +ControllerProto.$readonly = function(bool, charBeg, charEnd) { return this.$isValue(bool, charBeg, charEnd, 'readonly="readonly"'); }; -Controller.prototype.$header = function(name, value) { +ControllerProto.$header = function(name, value) { this.header(name, value); return ''; }; -Controller.prototype.$text = function(model, name, attr) { +ControllerProto.$text = function(model, name, attr) { return this.$input(model, 'text', name, attr); }; -Controller.prototype.$password = function(model, name, attr) { +ControllerProto.$password = function(model, name, attr) { return this.$input(model, 'password', name, attr); }; -Controller.prototype.$hidden = function(model, name, attr) { +ControllerProto.$hidden = function(model, name, attr) { return this.$input(model, 'hidden', name, attr); }; -Controller.prototype.$radio = function(model, name, value, attr) { +ControllerProto.$radio = function(model, name, value, attr) { if (typeof(attr) === 'string') { var label = attr; @@ -10763,7 +13246,7 @@ Controller.prototype.$radio = function(model, name, value, attr) { return this.$input(model, 'radio', name, attr); }; -Controller.prototype.$checkbox = function(model, name, attr) { +ControllerProto.$checkbox = function(model, name, attr) { if (typeof(attr) === 'string') { var label = attr; @@ -10774,7 +13257,7 @@ Controller.prototype.$checkbox = function(model, name, attr) { return this.$input(model, 'checkbox', name, attr); }; -Controller.prototype.$textarea = function(model, name, attr) { +ControllerProto.$textarea = function(model, name, attr) { var builder = '' + ((model[name] || attr.value) || '') + ''; }; -Controller.prototype.$input = function(model, type, name, attr) { +ControllerProto.$input = function(model, type, name, attr) { var builder = ['' + builder + ' ' + attr.label + '') : builder; }; -Controller.prototype._preparehostname = function(value) { +ControllerProto._preparehostname = function(value) { if (!value) return value; var tmp = value.substring(0, 5); return tmp !== 'http:' && tmp !== 'https' && (tmp[0] !== '/' || tmp[1] !== '/') ? this.host(value) : value; }; -Controller.prototype.head = function() { +ControllerProto.head = function() { var self = this; if (!arguments.length) { - // OBSOLETE: this is useless - // F.emit('controller-render-head', self); - var author = self.repository[REPOSITORY_META_AUTHOR] || self.config.author; - return (author ? '' : '') + (self.repository[REPOSITORY_HEAD] || ''); + var author = self.repository[REPOSITORY_META_AUTHOR] || CONF.author; + var plus = ''; + var components = self.repository[REPOSITORY_COMPONENTS]; + if (components) { + var keys = Object.keys(components); + for (var i = 0; i < keys.length; i++) { + var com = F.components.groups[keys[i]]; + if (com) + plus += com.links; + } + // Cleans cache + self.repository[REPOSITORY_COMPONENTS] = null; + } + return (author ? '' : '') + (self.repository[REPOSITORY_HEAD] || '') + plus; } var header = (self.repository[REPOSITORY_HEAD] || ''); @@ -10921,21 +13414,21 @@ Controller.prototype.head = function() { var is = (tmp[0] !== '/' && tmp[1] !== '/') && tmp !== 'http://' && tmp !== 'https:/'; var ext = U.getExtension(val); if (ext === 'css') - header += ''; - else if (ext === 'js') - header += ''; + header += ''; + else if (JSFILES[ext]) + header += ''; } self.repository[REPOSITORY_HEAD] = header; return self; }; -Controller.prototype.$head = function() { +ControllerProto.$head = function() { this.head.apply(this, arguments); return ''; }; -Controller.prototype.$isValue = function(bool, charBeg, charEnd, value) { +ControllerProto.$isValue = function(bool, charBeg, charEnd, value) { if (!bool) return ''; charBeg = charBeg || ' '; @@ -10943,49 +13436,7 @@ Controller.prototype.$isValue = function(bool, charBeg, charEnd, value) { return charBeg + value + charEnd; }; -Controller.prototype.$modified = function(value) { - - var self = this; - var type = typeof(value); - var date; - - if (type === 'number') { - date = new Date(value); - } else if (type === 'string') { - - var d = value.split(' '); - - date = d[0].split('-'); - var time = (d[1] || '').split(':'); - - var year = U.parseInt(date[0] || ''); - var month = U.parseInt(date[1] || '') - 1; - var day = U.parseInt(date[2] || '') - 1; - - if (month < 0) - month = 0; - - if (day < 0) - day = 0; - - var hour = U.parseInt(time[0] || ''); - var minute = U.parseInt(time[1] || ''); - var second = U.parseInt(time[2] || ''); - - date = new Date(year, month, day, hour, minute, second, 0); - } else if (U.isDate(value)) - date = value; - - date && self.setModified(date); - return ''; -}; - -Controller.prototype.$etag = function(value) { - this.setModified(value); - return ''; -}; - -Controller.prototype.$options = function(arr, selected, name, value) { +ControllerProto.$options = function(arr, selected, name, value, disabled) { var type = typeof(arr); if (!arr) @@ -11000,7 +13451,7 @@ Controller.prototype.$options = function(arr, selected, name, value) { arr = Object.keys(arr); } - if (!U.isArray(arr)) + if (!(arr instanceof Array)) arr = [arr]; selected = selected || ''; @@ -11008,10 +13459,12 @@ Controller.prototype.$options = function(arr, selected, name, value) { var options = ''; if (!isObject) { - if (value === undefined) + if (value == null) value = value || name || 'value'; - if (name === undefined) - name = name || 'name'; + if (name == null) + name = 'name'; + if (disabled == null) + disabled = 'disabled'; } var isSelected = false; @@ -11026,6 +13479,7 @@ Controller.prototype.$options = function(arr, selected, name, value) { var text = ''; var val = ''; var sel = false; + var dis = false; if (isObject) { if (name === true) { @@ -11051,6 +13505,13 @@ Controller.prototype.$options = function(arr, selected, name, value) { if (typeof(val) === 'function') val = val(i, text); + dis = o[disabled]; + + if (typeof(disabled) === 'function') + dis = disabled(i, val, text); + else + dis = dis ? true : false; + } else { text = o; val = o; @@ -11061,7 +13522,7 @@ Controller.prototype.$options = function(arr, selected, name, value) { isSelected = sel; } - options += ''; + options += ''; } return options; @@ -11072,7 +13533,7 @@ Controller.prototype.$options = function(arr, selected, name, value) { * @private * @return {String} */ -Controller.prototype.$script = function() { +ControllerProto.$script = function() { return arguments.length === 1 ? this.$js(arguments[0]) : this.$js.apply(this, arguments); }; @@ -11081,11 +13542,11 @@ Controller.prototype.$script = function() { * @private * @return {String} */ -Controller.prototype.$js = function() { +ControllerProto.$js = function() { var self = this; var builder = ''; for (var i = 0; i < arguments.length; i++) - builder += self.routeScript(arguments[i], true); + builder += self.public_js(arguments[i], true); return builder; }; @@ -11094,7 +13555,7 @@ Controller.prototype.$js = function() { * @private * @return {String} */ -Controller.prototype.$absolute = function(files, base) { +ControllerProto.$absolute = function(files, base) { var self = this; var builder; @@ -11111,13 +13572,14 @@ Controller.prototype.$absolute = function(files, base) { for (var i = 0, length = files.length; i < length; i++) { switch (ftype) { case 'js': - builder += self.routeScript(files[i], true, base); + case 'mjs': + builder += self.public_js(files[i], true, base); break; case 'css': - builder += self.routeStyle(files[i], true, base); + builder += self.public_css(files[i], true, base); break; default: - builder += self.routeStatic(files[i], base); + builder += self.public(files[i], base); break; } } @@ -11129,15 +13591,18 @@ Controller.prototype.$absolute = function(files, base) { switch (ftype) { case 'js': - return self.routeScript(files, true, base); + case 'mjs': + return self.public_js(files, true, base); case 'css': - return self.routeStyle(files, true, base); + return self.public_css(files, true, base); } - return self.routeStatic(files, base); + return self.public(files, base); }; -Controller.prototype.$import = function() { +var $importmergecache = {}; + +ControllerProto.$import = function() { var self = this; var builder = ''; @@ -11156,7 +13621,12 @@ Controller.prototype.$import = function() { } if (filename === 'components' && F.components.has) { - builder += F.components.links; + // Generated in controller.head() + continue; + } + + if (filename === 'manifest' || filename === 'manifest.json') { + builder += ''; continue; } @@ -11165,30 +13635,88 @@ Controller.prototype.$import = function() { continue; } - var extension = filename.substring(filename.lastIndexOf('.')); + if (filename[0] === 'l' && filename[9] === 'd' && filename.substring(0, 10) === 'livereload') { + if (DEBUG) { + var url = filename.substring(11).trim(); + builder += ''; + } + continue; + } + + var k = 'import#' + (self.themeName || '') + filename; + + if (F.temporary.other[k]) { + builder += F.temporary.other[k]; + continue; + } + + var ext; + + if (filename.indexOf('+') !== -1) { + + // MERGE + var merge = filename.split('+'); + var hash = 'merge' + filename.hash(true); + + if ($importmergecache[hash]) { + builder += F.temporary.other[k] = $importmergecache[hash]; + continue; + } + + merge[0] = merge[0].trim(); + var index = merge[0].lastIndexOf('.'); + var mergename = merge[0]; + var crc = 0; + + ext = U.getExtension(merge[0]); + merge[0] = ext === 'css' ? self.public_css(merge[0]) : self.public_js(merge[0]); + + for (var j = 1; j < merge.length; j++) { + merge[j] = merge[j].trim(); + merge[j] = ext === 'css' ? self.public_css(merge[j]) : self.public_js(merge[j]); + crc += merge[j].crc32(true); + } + + var outputname = mergename.substring(0, index) + crc + mergename.substring(index); + outputname = ext === 'css' ? self.public_css(outputname) : self.public_js(outputname); + + var tmp = ext === 'css' ? self.public_css(outputname, true) : self.public_js(outputname, true); + $importmergecache[hash] = F.temporary.other[k] = tmp; + + merge.unshift(outputname); + MERGE.apply(global, merge); + builder += tmp; + continue; + } + + ext = filename.substring(filename.lastIndexOf('.')); var tag = filename[0] !== '!'; if (!tag) filename = filename.substring(1); if (filename[0] === '#') - extension = '.js'; + ext = '.js'; - switch (extension) { + switch (ext) { case '.js': - builder += self.routeScript(filename, tag); + builder += F.temporary.other[k] = self.public_js(filename, tag); break; case '.css': - builder += self.routeStyle(filename, tag); + builder += F.temporary.other[k] = self.public_css(filename, tag); break; case '.ico': - builder += self.$favicon(filename); + builder += F.temporary.other[k] = self.$favicon(filename); break; case '.jpg': case '.gif': case '.svg': case '.png': case '.jpeg': - builder += self.routeImage(filename); + case '.heif': + case '.webp': + case '.heic': + case '.apng': + builder += F.temporary.other[k] = self.public_image(filename); break; case '.mp4': case '.avi': @@ -11199,10 +13727,10 @@ Controller.prototype.$import = function() { case '.mpe': case '.mpeg': case '.m4v': - builder += self.routeVideo(filename); + builder += F.temporary.other[k] = self.public_video(filename); break; default: - builder += self.routeStatic(filename); + builder += F.temporary.other[k] = self.public(filename); break; } } @@ -11215,18 +13743,18 @@ Controller.prototype.$import = function() { * @private * @return {String} */ -Controller.prototype.$css = function() { +ControllerProto.$css = function() { var self = this; var builder = ''; for (var i = 0; i < arguments.length; i++) - builder += self.routeStyle(arguments[i], true); + builder += self.public_css(arguments[i], true); return builder; }; -Controller.prototype.$image = function(name, width, height, alt, className) { +ControllerProto.$image = function(name, width, height, alt, className) { var style = ''; @@ -11238,7 +13766,7 @@ Controller.prototype.$image = function(name, width, height, alt, className) { width = width.width; } - var builder = ' 0) builder += ' width="' + width + ATTR_END; @@ -11267,8 +13795,8 @@ Controller.prototype.$image = function(name, width, height, alt, className) { * @param {String} className Optional. * @return {String} */ -Controller.prototype.$download = function(filename, innerHTML, downloadName, className) { - var builder = '' + value + '') : value; }; +/** + * Serialize object into the JSON + * @private + * @param {Object} obj + * @param {String} id Optional. + * @param {Boolean} beautify Optional. + * @return {String} + */ +ControllerProto.$json2 = function(obj, id) { + + if (obj && obj.$$schema) + obj = obj.$clean(); + + var data = {}; + + for (var i = 2; i < arguments.length; i++) { + var key = arguments[i]; + data[key] = obj[key]; + } + + return ''; +}; + /** * Append FAVICON tag * @private * @param {String} name * @return {String} */ -Controller.prototype.$favicon = function(name) { +ControllerProto.$favicon = function(name) { var contentType = 'image/x-icon'; - if (name == null) + if (!name) name = 'favicon.ico'; var key = 'favicon#' + name; @@ -11326,7 +13880,7 @@ Controller.prototype.$favicon = function(name) { else if (name.lastIndexOf('.gif') !== -1) contentType = 'image/gif'; - return F.temporary.other[key] = ''; + return F.temporary.other[key] = ''; }; /** @@ -11337,18 +13891,16 @@ Controller.prototype.$favicon = function(name) { * @param {Function} fn * @return {String} */ -Controller.prototype._routeHelper = function(name, fn) { +ControllerProto.$static = function(name, fn) { return fn.call(framework, prepare_staticurl(name, false), this.themeName); }; -/** - * Create URL: JavaScript - * @param {String} name - * @param {Boolean} tag Optional, default "false" - * @param {String} path Optional, default undefined - * @return {String} - */ -Controller.prototype.routeScript = function(name, tag, path) { +ControllerProto.routeScript = function(name, tag, path) { + OBSOLETE('controller.routeScript()', 'Was renamed to "controller.public_js()"'); + return this.public_js(name, tag, path); +}; + +ControllerProto.public_js = function(name, tag, path) { if (name === undefined) name = 'default.js'; @@ -11372,7 +13924,7 @@ Controller.prototype.routeScript = function(name, tag, path) { return ''; } } else { - url = this._routeHelper(name, F.routeScript); + url = this.$static(name, F.public_js); if (path && U.isRelative(url)) url = F.isWindows ? U.join(path, url) : U.join(path, url).substring(1); } @@ -11380,68 +13932,68 @@ Controller.prototype.routeScript = function(name, tag, path) { return tag ? ('') : url; }; -/** - * Create URL: CSS - * @param {String} name - * @param {Boolean} tag Append tag? - * @return {String} - */ -Controller.prototype.routeStyle = function(name, tag, path) { +ControllerProto.routeStyle = function(name, tag, path) { + OBSOLETE('controller.routeStyle()', 'Was renamed to "controller.public_css()"'); + return this.public_css(name, tag, path); +}; + +ControllerProto.public_css = function(name, tag, path) { + var self = this; if (name === undefined) name = 'default.css'; - var url = self._routeHelper(name, F.routeStyle); + var url = self.$static(name, F.public_css); if (path && U.isRelative(url)) url = F.isWindows ? U.join(path, url) : U.join(path, url).substring(1); return tag ? '' : url; }; -/** - * Create URL: IMG - * @param {String} name - * @return {String} - */ -Controller.prototype.routeImage = function(name) { - return this._routeHelper(name, F.routeImage); +ControllerProto.routeImage = function(name) { + OBSOLETE('controller.routeImage()', 'Was renamed to "controller.public_image()"'); + return this.public_image(name); }; -/** - * Create URL: VIDEO - * @param {String} name - * @return {String} - */ -Controller.prototype.routeVideo = function(name) { - return this._routeHelper(name, F.routeVideo); +ControllerProto.public_image = function(name) { + return this.$static(name, F.public_image); }; -/** - * Create URL: FONT - * @param {String} name - * @return {String} - */ -Controller.prototype.routeFont = function(name) { - return F.routeFont(name); +ControllerProto.routeVideo = function(name) { + OBSOLETE('controller.routeVideo()', 'Was renamed to "controller.public_video()"'); + return this.public_video(name); }; -/** - * Create URL: DOWNLOAD - * @param {String} name - * @return {String} - */ -Controller.prototype.routeDownload = function(name) { - return this._routeHelper(name, F.routeDownload); +ControllerProto.public_video = function(name) { + return this.$static(name, F.public_video); }; -/** - * Create URL: static files (by the config['static-url']) - * @param {String} name - * @return {String} - */ -Controller.prototype.routeStatic = function(name, path) { - var url = this._routeHelper(name, F.routeStatic); +ControllerProto.routeFont = function(name) { + OBSOLETE('controller.routeFont()', 'Was renamed to "controller.public_font()"'); + return this.public_font(name); +}; + +ControllerProto.public_font = function(name) { + return F.public_font(name); +}; + +ControllerProto.routeDownload = function(name) { + OBSOLETE('controller.routeDownload()', 'Was renamed to "controller.public_download()"'); + return this.public_download(name); +}; + +ControllerProto.public_download = function(name) { + return this.$static(name, F.public_download); +}; + +ControllerProto.routeStatic = function(name, path) { + OBSOLETE('controller.routeStatic()', 'Was renamed to "controller.public()"'); + return this.public(name, path); +}; + +ControllerProto.public = function(name, path) { + var url = this.$static(name, F.public); if (path && U.isRelative(url)) return F.isWindows ? U.join(path, url) : U.join(path, url).substring(1); return url; @@ -11453,7 +14005,8 @@ Controller.prototype.routeStatic = function(name, path) { * @param {Object} model A model, optional. * @return {String} */ -Controller.prototype.template = function(name, model) { +ControllerProto.template = function(name, model) { + OBSOLETE('controller.template()', 'This method will be removed in v4.'); return this.view(name, model, true); }; @@ -11462,7 +14015,7 @@ Controller.prototype.template = function(name, model) { * @param {String} name A helper name. * @return {String} */ -Controller.prototype.helper = function(name) { +ControllerProto.helper = function(name) { var helper = F.helpers[name]; if (!helper) return ''; @@ -11482,7 +14035,7 @@ Controller.prototype.helper = function(name) { * @param {Function(key, value)} replacer JSON replacer. * @return {Controller} */ -Controller.prototype.json = function(obj, headers, beautify, replacer) { +ControllerProto.json = function(obj, headers, beautify, replacer) { var self = this; var res = self.res; @@ -11505,6 +14058,12 @@ Controller.prototype.json = function(obj, headers, beautify, replacer) { return self; } + if (self.$evalroutecallback) { + var err = obj instanceof framework_builders.ErrorBuilder ? obj : null; + self.$evalroutecallback(err, err ? null : obj); + return self; + } + if (obj instanceof framework_builders.ErrorBuilder) { self.req.$language && !obj.isResourceCustom && obj.setResource(self.req.$language); @@ -11522,7 +14081,7 @@ Controller.prototype.json = function(obj, headers, beautify, replacer) { F.stats.response.errorBuilder++; } else { - if (framework_builders.isSchema(obj)) + if (obj && obj.$$schema) obj = obj.$clean(); if (beautify) @@ -11533,15 +14092,47 @@ Controller.prototype.json = function(obj, headers, beautify, replacer) { F.stats.response.json++; res.options.body = obj; + res.options.compress = obj.length > 4096; res.$text(); self.precache && self.precache(obj, res.options.type, headers); return self; }; -Controller.prototype.success = function(is, value) { - if (is === undefined) - is = true; - return this.json(SUCCESS(is, value)); +ControllerProto.success = function(is, value) { + var t = typeof(is); + if (value === undefined && (is == null || t === 'boolean')) { + F.stats.response.json++; + var res = this.res; + res.options.body = '{"success":' + (is == null ? 'true' : is) + '}'; + res.options.type = CT_JSON; + res.options.compress = false; + res.$text(); + } else { + if (t && t !== 'boolean') { + value = t; + t = true; + } + this.json(SUCCESS(is == null ? true : is, value)); + } + + return this; +}; + +ControllerProto.done = function(arg) { + var self = this; + return function(err, response) { + if (err) { + self.invalid(err); + } else if (arg) + self.json(SUCCESS(err == null, arg === true ? response : arg)); + else { + var res = self.res; + res.options.body = '{"success":' + (err == null) + '}'; + res.options.type = CT_JSON; + res.options.compress = false; + res.$text(); + } + }; }; /** @@ -11553,7 +14144,7 @@ Controller.prototype.success = function(is, value) { * @param {Function} replacer Optional, the JSON replacer. * @return {Controller} */ -Controller.prototype.jsonp = function(name, obj, headers, beautify, replacer) { +ControllerProto.jsonp = function(name, obj, headers, beautify, replacer) { var self = this; var res = self.res; @@ -11585,7 +14176,7 @@ Controller.prototype.jsonp = function(name, obj, headers, beautify, replacer) { F.stats.response.errorBuilder++; } else { - if (framework_builders.isSchema(obj)) + if (obj && obj.$$schema) obj = obj.$clean(); if (beautify) @@ -11607,14 +14198,19 @@ Controller.prototype.jsonp = function(name, obj, headers, beautify, replacer) { * @param {String} view Optional, undefined or null returns JSON. * @return {Function} */ -Controller.prototype.callback = function(view) { +ControllerProto.callback = function(view) { var self = this; return function(err, data) { + CONF.logger && self.req.$logger && F.ilogger(null, self.req); + + if (self.res && self.res.success) + return; + var is = err instanceof framework_builders.ErrorBuilder; // NoSQL embedded database - if (data === undefined && !U.isError(err) && !is) { + if (data === undefined && (err && !err.stack) && !is) { data = err; err = null; } @@ -11627,14 +14223,29 @@ Controller.prototype.callback = function(view) { return is && err.unexpected ? self.view500(err) : self.view404(err); } + // Hack for schemas + if (data instanceof F.callback_redirect) + return self.redirect(data.url); + if (typeof(view) === 'string') self.view(view, data); - else + else if (data === SUCCESSHELPER && data.value === undefined) { + if (self.$evalroutecallback) { + self.$evalroutecallback(null, data); + } else { + F.stats.response.json++; + var res = self.res; + res.options.compress = false; + res.options.body = '{"success":' + (data.success == null ? 'true' : data.success) + '}'; + res.options.type = CT_JSON; + res.$text(); + } + } else self.json(data); }; }; -Controller.prototype.custom = function() { +ControllerProto.custom = function() { if (this.res.success) return false; this.res.$custom(); @@ -11646,16 +14257,22 @@ Controller.prototype.custom = function() { * @param {Boolean} enable Optional, default `true`. * @return {Controller} */ -Controller.prototype.noClear = function(enable) { +ControllerProto.noClear = function(enable) { + OBSOLETE('controller.noClear()', 'You need to use controller.autoclear(false)'); this.req._manual = enable === undefined ? true : enable; return this; }; -Controller.prototype.html = function(body, headers) { +ControllerProto.autoclear = function(enable) { + this.req._manual = enable === false; + return this; +}; + +ControllerProto.html = function(body, headers) { return this.content(body, 'text/html', headers); }; -Controller.prototype.content = function(body, type, headers) { +ControllerProto.content = function(body, type, headers) { var self = this; var res = self.res; @@ -11663,6 +14280,12 @@ Controller.prototype.content = function(body, type, headers) { res.options.headers = headers; res.options.code = self.status || 200; + if (self.$evalroutecallback) { + var err = body instanceof ErrorBuilder ? body : null; + self.$evalroutecallback(err, err ? null : body); + return self; + } + if (body instanceof ErrorBuilder) { if (self.language && !body.resourceName) @@ -11683,6 +14306,7 @@ Controller.prototype.content = function(body, type, headers) { res.options.type = type || CT_TEXT; res.options.body = body; + res.options.compress = body.length > 4096; res.$text(); if (self.precache && (!self.status || self.status === 200)) { @@ -11699,7 +14323,7 @@ Controller.prototype.content = function(body, type, headers) { * @param {Boolean} headers A custom headers. * @return {Controller} */ -Controller.prototype.plain = function(body, headers) { +ControllerProto.plain = function(body, headers) { var self = this; var res = self.res; @@ -11721,7 +14345,7 @@ Controller.prototype.plain = function(body, headers) { if (body == null) body = ''; else if (type === 'object') { - if (framework_builders.isSchema(body)) + if (body && body.$$schema) body = body.$clean(); body = body ? JSON.stringify(body, null, 4) : ''; } else @@ -11739,7 +14363,7 @@ Controller.prototype.plain = function(body, headers) { * @param {Object/Number} headers A custom headers or a custom HTTP status. * @return {Controller} */ -Controller.prototype.empty = function(headers) { +ControllerProto.empty = function(headers) { var self = this; var res = self.res; @@ -11759,12 +14383,27 @@ Controller.prototype.empty = function(headers) { return self; }; +/** + * Creates an empty response with 204 + * @param {Object/Number} headers A custom headers or a custom HTTP status. + * @return {Controller} + */ +ControllerProto.nocontent = function(headers) { + var self = this; + var res = self.res; + res.writeHead(204, headers); + res.end(); + F.stats.response.empty++; + response_end(res); + return self; +}; + /** * Destroys a request (closes it) * @param {String} problem Optional. * @return {Controller} */ -Controller.prototype.destroy = function(problem) { +ControllerProto.destroy = function(problem) { var self = this; problem && self.problem(problem); @@ -11785,7 +14424,7 @@ Controller.prototype.destroy = function(problem) { * @param {Function} done Optinoal, callback. * @return {Controller} */ -Controller.prototype.file = function(filename, download, headers, done) { +ControllerProto.file = function(filename, download, headers, done) { if (filename[0] === '~') filename = filename.substring(1); @@ -11797,10 +14436,57 @@ Controller.prototype.file = function(filename, download, headers, done) { res.options.download = download; res.options.headers = headers; res.options.callback = done; + res.$file(); return this; }; +ControllerProto.filefs = function(name, id, download, headers, callback, checkmeta) { + var self = this; + var options = {}; + options.id = id; + options.download = download; + options.headers = headers; + options.done = callback; + FILESTORAGE(name).res(self.res, options, checkmeta, $file_notmodified); + return self; +}; + +ControllerProto.filenosql = function(name, id, download, headers, callback, checkmeta) { + var self = this; + var options = {}; + options.id = id; + options.download = download; + options.headers = headers; + options.done = callback; + NOSQL(name).binary.res(self.res, options, checkmeta, $file_notmodified); + return self; +}; + +ControllerProto.imagefs = function(name, id, make, headers, callback, checkmeta) { + var self = this; + var options = {}; + options.id = id; + options.image = true; + options.make = make; + options.headers = headers; + options.done = callback; + FILESTORAGE(name).res(self.res, options, checkmeta, $file_notmodified); + return self; +}; + +ControllerProto.imagenosql = function(name, id, make, headers, callback, checkmeta) { + var self = this; + var options = {}; + options.id = id; + options.image = true; + options.make = make; + options.headers = headers; + options.done = callback; + NOSQL(name).binary.res(self.res, options, checkmeta, $file_notmodified); + return self; +}; + /** * Responds with an image * @param {String or Stream} filename @@ -11809,7 +14495,7 @@ Controller.prototype.file = function(filename, download, headers, done) { * @param {Function} done Optional, callback. * @return {Controller} */ -Controller.prototype.image = function(filename, make, headers, done) { +ControllerProto.image = function(filename, make, headers, done) { var res = this.res; @@ -11839,7 +14525,7 @@ Controller.prototype.image = function(filename, make, headers, done) { * @param {Function} done Optinoal, callback. * @return {Controller} */ -Controller.prototype.stream = function(type, stream, download, headers, done, nocompress) { +ControllerProto.stream = function(type, stream, download, headers, done, nocompress) { var res = this.res; res.options.type = type; res.options.stream = stream; @@ -11856,7 +14542,7 @@ Controller.prototype.stream = function(type, stream, download, headers, done, no * @param {String} problem Description of problem (optional) * @return {Controller} */ -Controller.prototype.throw400 = Controller.prototype.view400 = function(problem) { +ControllerProto.throw400 = ControllerProto.view400 = function(problem) { return controller_error_status(this, 400, problem); }; @@ -11865,7 +14551,7 @@ Controller.prototype.throw400 = Controller.prototype.view400 = function(problem) * @param {String} problem Description of problem (optional) * @return {Controller} */ -Controller.prototype.throw401 = Controller.prototype.view401 = function(problem) { +ControllerProto.throw401 = ControllerProto.view401 = function(problem) { return controller_error_status(this, 401, problem); }; @@ -11874,7 +14560,7 @@ Controller.prototype.throw401 = Controller.prototype.view401 = function(problem) * @param {String} problem Description of problem (optional) * @return {Controller} */ -Controller.prototype.throw403 = Controller.prototype.view403 = function(problem) { +ControllerProto.throw403 = ControllerProto.view403 = function(problem) { return controller_error_status(this, 403, problem); }; @@ -11883,7 +14569,7 @@ Controller.prototype.throw403 = Controller.prototype.view403 = function(problem) * @param {String} problem Description of problem (optional) * @return {Controller} */ -Controller.prototype.throw404 = Controller.prototype.view404 = function(problem) { +ControllerProto.throw404 = ControllerProto.view404 = function(problem) { return controller_error_status(this, 404, problem); }; @@ -11892,7 +14578,7 @@ Controller.prototype.throw404 = Controller.prototype.view404 = function(problem) * @param {String} problem Description of problem (optional) * @return {Controller} */ -Controller.prototype.throw409 = Controller.prototype.view409 = function(problem) { +ControllerProto.throw409 = ControllerProto.view409 = function(problem) { return controller_error_status(this, 409, problem); }; @@ -11901,7 +14587,7 @@ Controller.prototype.throw409 = Controller.prototype.view409 = function(problem) * @param {Error} error * @return {Controller} */ -Controller.prototype.throw500 = Controller.prototype.view500 = function(error) { +ControllerProto.throw500 = ControllerProto.view500 = function(error) { var self = this; F.error(error instanceof Error ? error : new Error((error || '').toString()), self.name, self.req.uri); return controller_error_status(self, 500, error); @@ -11912,17 +14598,26 @@ Controller.prototype.throw500 = Controller.prototype.view500 = function(error) { * @param {String} problem Description of the problem (optional) * @return {Controller} */ -Controller.prototype.throw501 = Controller.prototype.view501 = function(problem) { +ControllerProto.throw501 = ControllerProto.view501 = function(problem) { return controller_error_status(this, 501, problem); }; +/** + * Throw 503 - Service unavailable + * @param {String} problem Description of the problem (optional) + * @return {Controller} + */ +ControllerProto.throw503 = ControllerProto.view503 = function(problem) { + return controller_error_status(this, 503, problem); +}; + /** * Creates a redirect * @param {String} url * @param {Boolean} permanent Is permanent? Default: `false` * @return {Controller} */ -Controller.prototype.redirect = function(url, permanent) { +ControllerProto.redirect = function(url, permanent) { this.precache && this.precache(null, null, null); var res = this.res; res.options.url = url; @@ -11940,7 +14635,7 @@ Controller.prototype.redirect = function(url, permanent) { * @param {Object} headers Optional, additional headers. * @return {Controller} */ -Controller.prototype.binary = function(buffer, type, encoding, download, headers) { +ControllerProto.binary = function(buffer, type, encoding, download, headers) { var res = this.res; @@ -11970,7 +14665,7 @@ Controller.prototype.binary = function(buffer, type, encoding, download, headers * @param {String} label * @return {Object} */ -Controller.prototype.baa = function(label) { +ControllerProto.baa = function(label) { var self = this; self.precache && self.precache(null, null, null); @@ -12001,7 +14696,7 @@ Controller.prototype.baa = function(label) { * @param {Number} retry A reconnection timeout in milliseconds when is an unexpected problem. * @return {Controller} */ -Controller.prototype.sse = function(data, eventname, id, retry) { +ControllerProto.sse = function(data, eventname, id, retry) { var self = this; var res = self.res; @@ -12020,7 +14715,7 @@ Controller.prototype.sse = function(data, eventname, id, retry) { self.type = 1; if (retry === undefined) - retry = self.req.$total_route.timeout; + retry = self.route.timeout; self.req.$total_success(); self.req.on('close', () => self.close()); @@ -12053,51 +14748,12 @@ Controller.prototype.sse = function(data, eventname, id, retry) { return self; }; -Controller.prototype.mmr = function(name, stream, callback) { - - var self = this; - var res = self.res; - - if (typeof(stream) === 'function') { - callback = stream; - stream = name; - } - - if (!stream) - stream = name; - - if (!self.isConnected || (!self.type && res.success) || (self.type && self.type !== 2)) { - callback = null; - return self; - } - - if (!self.type) { - self.type = 2; - self.boundary = '----totaljs' + U.GUID(10); - self.req.$total_success(); - self.req.on('close', () => self.close()); - res.success = true; - HEADERS.mmr[HEADER_TYPE] = 'multipart/x-mixed-replace; boundary=' + self.boundary; - res.writeHead(self.status || 200, HEADERS.mmr); - } - - res.write('--' + self.boundary + NEWLINE + HEADER_TYPE + ': ' + U.getContentType(U.getExtension(name)) + NEWLINE + NEWLINE); - F.stats.response.mmr++; - - if (typeof(stream) === 'string') - stream = Fs.createReadStream(stream); - - stream.pipe(res, HEADERS.mmrpipe); - CLEANUP(stream, () => callback && callback()); - return self; -}; - /** * Close a response * @param {Boolean} end * @return {Controller} */ -Controller.prototype.close = function(end) { +ControllerProto.close = function(end) { var self = this; if (end === undefined) @@ -12110,7 +14766,8 @@ Controller.prototype.close = function(end) { self.isConnected = false; self.res.success = true; F.reqstats(false, false); - F.$events['request-end'] && F.emit('request-end', self.req, self.res); + F.$events['request-end'] && EMIT('request-end', self.req, self.res); + F.$events.request_end && EMIT('request_end', self.req, self.res); self.type = 0; end && self.res.end(); self.req.clear(true); @@ -12124,46 +14781,13 @@ Controller.prototype.close = function(end) { self.res.success = true; F.reqstats(false, false); - F.$events['request-end'] && F.emit('request-end', self.req, self.res); + F.$events['request-end'] && EMIT('request-end', self.req, self.res); + F.$events.request_end && EMIT('request_end', self.req, self.res); end && self.res.end(); self.req.clear(true); return self; }; -/** - * Sends an object to another total.js application (POST + JSON) - * @param {String} url - * @param {Object} obj - * @param {Function(err, data, code, headers)} callback - * @param {Number} timeout Timeout, optional default 10 seconds. - * @return {EventEmitter} - */ -Controller.prototype.proxy = function(url, obj, callback, timeout) { - - var self = this; - var tmp; - - if (typeof(callback) === 'number') { - tmp = timeout; - timeout = callback; - callback = tmp; - } - - if (typeof(obj) === 'function') { - tmp = callback; - callback = obj; - obj = tmp; - } - - return U.request(url, FLAGS_PROXY, obj, function(err, data, code, headers) { - if (!callback) - return; - if ((headers['content-type'] || '').lastIndexOf('/json') !== -1) - data = F.onParseJSON(data); - callback.call(self, err, data, code, headers); - }, null, HEADERS.proxy, ENCODING, timeout || 10000); -}; - /** * Creates a proxy between current request and new URL * @param {String} url @@ -12172,7 +14796,7 @@ Controller.prototype.proxy = function(url, obj, callback, timeout) { * @param {Number} timeout Optional, timeout (default: 10000) * @return {EventEmitter} */ -Controller.prototype.proxy2 = function(url, callback, headers, timeout) { +ControllerProto.proxy = ControllerProto.proxy2 = function(url, callback, headers, timeout) { if (typeof(callback) === 'object') { timeout = headers; @@ -12189,22 +14813,18 @@ Controller.prototype.proxy2 = function(url, callback, headers, timeout) { flags.push(req.method); flags.push('dnscache'); - if (type === CT_JSON) + if ((/\/json/i).test(type)) flags.push('json'); - var c = req.method[0]; var tmp; - var keys; - if (c === 'G' || c === 'H' || c === 'O') { - if (url.indexOf('?') === -1) { - tmp = Qs.stringify(self.query); - if (tmp) - url += '?' + tmp; - } + if (url.indexOf('?') === -1) { + tmp = Qs.stringify(self.query); + if (tmp) + url += '?' + tmp; } - keys = Object.keys(req.headers); + var keys = Object.keys(req.headers); for (var i = 0, length = keys.length; i < length; i++) { switch (keys[i]) { case 'x-forwarded-for': @@ -12222,9 +14842,20 @@ Controller.prototype.proxy2 = function(url, callback, headers, timeout) { } if (headers) { + + if (headers.flags) { + if (typeof(headers.flags) === 'string') + headers.flags = headers.flags.split(','); + for (var i = 0; i < headers.flags.length; i++) + flags.push(headers.flags[i]); + headers.flags = undefined; + } + keys = Object.keys(headers); - for (var i = 0, length = keys.length; i < length; i++) - h[keys[i]] = headers[keys[i]]; + for (var i = 0, length = keys.length; i < length; i++) { + if (headers[keys[i]]) + h[keys[i]] = headers[keys[i]]; + } } return U.request(url, flags, self.body, function(err, data, code, headers) { @@ -12232,12 +14863,16 @@ Controller.prototype.proxy2 = function(url, callback, headers, timeout) { if (err) { callback && callback(err); self.invalid().push(err); - return; + } else { + self.status = code; + callback && callback(err, data, code, headers); + var ct = (headers['content-type'] || 'text/plain').replace(REG_ENCODINGCLEANER, ''); + if (data instanceof Buffer) + self.binary(data, ct); + else + self.content(data, ct); } - self.status = code; - callback && callback(err, data, code, headers); - self.content(data, (headers['content-type'] || 'text/plain').replace(REG_ENCODINGCLEANER, '')); }, null, h, ENCODING, timeout || 10000); }; @@ -12249,7 +14884,7 @@ Controller.prototype.proxy2 = function(url, callback, headers, timeout) { * @param {Boolean} isPartial When is `true` the method returns rendered HTML as `String` * @return {Controller/String} */ -Controller.prototype.view = function(name, model, headers, partial) { +ControllerProto.view = function(name, model, headers, partial, noasync, cachekey) { var self = this; @@ -12267,9 +14902,9 @@ Controller.prototype.view = function(name, model, headers, partial) { return self; if (self.layoutName === undefined) - self.layoutName = F.config['default-layout']; + self.layoutName = CONF.default_layout; if (self.themeName === undefined) - self.themeName = F.config['default-theme']; + self.themeName = CONF.default_theme; // theme root `~some_view` // views root `~~some_view` @@ -12312,7 +14947,7 @@ Controller.prototype.view = function(name, model, headers, partial) { skip = 2; } - if (!isTheme && !isLayout && !skip) + if (!isTheme && !isLayout && !skip && self._currentView) filename = self._currentView + name; if (!isTheme && (skip === 2 || skip === 3)) @@ -12360,10 +14995,15 @@ Controller.prototype.view = function(name, model, headers, partial) { } } - return self.$viewrender(filename, framework_internal.viewEngine(name, filename, self), model, headers, partial, isLayout); + return self.$viewrender(filename, framework_internal.viewEngine(name, filename, self), model, headers, partial, isLayout, noasync, cachekey); }; -Controller.prototype.viewCompile = function(body, model, headers, partial, key) { +ControllerProto.viewCompile = function(body, model, headers, partial, key) { + OBSOLETE('controller.viewCompile()', 'Was renamed to `controller.view_compile()`.'); + return this.view_compile(body, model, headers, partial, key); +}; + +ControllerProto.view_compile = function(body, model, headers, partial, key) { if (headers === true) { key = partial; @@ -12380,7 +15020,7 @@ Controller.prototype.viewCompile = function(body, model, headers, partial, key) return this.$viewrender('[dynamic view]', framework_internal.viewEngineCompile(body, this.language, this, key), model, headers, partial); }; -Controller.prototype.$viewrender = function(filename, generator, model, headers, partial, isLayout) { +ControllerProto.$viewrender = function(filename, generator, model, headers, partial, isLayout, noasync, cachekey) { var self = this; var err; @@ -12412,7 +15052,16 @@ Controller.prototype.$viewrender = function(filename, generator, model, headers, var helpers = F.helpers; try { - value = generator.call(self, self, self.repository, model, self.session, self.query, self.body, self.url, F.global, helpers, self.user, self.config, F.functions, 0, partial ? self.outputPartial : self.output, self.req.cookie, self.req.files, self.req.mobile, EMPTYOBJECT); + + if (generator.components.length) { + if (!self.repository[REPOSITORY_COMPONENTS]) + self.repository[REPOSITORY_COMPONENTS] = {}; + for (var i = 0; i < generator.components.length; i++) + self.repository[REPOSITORY_COMPONENTS][generator.components[i]] = 1; + } + + value = generator.call(self, self, self.repository, model, self.session, self.query, self.body, self.url, F.global, helpers, self.user, CONF, F.functions, 0, partial ? self.outputPartial : self.output, self.req.files, self.req.mobile, EMPTYOBJECT); + } catch (ex) { err = new Error('View "' + filename + '": ' + ex.message); @@ -12433,7 +15082,107 @@ Controller.prototype.$viewrender = function(filename, generator, model, headers, return value; } - if (!isLayout && self.precache && (!self.status || self.status === 200) && !partial) + // noasync = true --> rendered inline view in view + + if (self.$viewasync && self.$viewasync.length) { + + var can = ((isLayout || !self.layoutName) && noasync !== true) || !!cachekey; + if (can) { + var done = {}; + var obj = {}; + + obj.repository = self.repository; + obj.model = self.$model; + obj.user = self.user; + obj.session = self.session; + obj.controller = self; + obj.query = self.query; + obj.body = self.body; + obj.files = self.files; + + self.$viewasync.waitFor(function(item, next) { + + if (item.value) { + value = value.replace(item.replace, item.value); + if (isLayout && self.precache) + self.output = self.output.replace(item.replace, item.value); + return next(); + } + + obj.options = obj.settings = item.settings; + obj.next = obj.callback = function(model) { + if (arguments.length > 1) + model = arguments[1]; + item.value = self.component(item.name, item.settings, model); + value = value.replace(item.replace, item.value); + if (isLayout && self.precache) + self.output = self.output.replace(item.replace, item.value); + next(); + }; + + F.components.instances[item.name].render(obj); + + }, function() { + + if (cachekey && F.cache.items[cachekey]) { + var cache = F.cache.items[cachekey].value; + cache.body = value; + cache.components = true; + } + + if (isLayout && self.precache && (!self.status || self.status === 200) && !partial) + self.precache(self.output, CT_HTML, headers, true); + + if (isLayout || !self.layoutName) { + + self.outputPartial = ''; + self.output = ''; + isLayout = false; + + if (partial) { + done.callback && done.callback(null, value); + return; + } + + self.req.$total_success(); + + if (!self.isConnected) + return self; + + var res = self.res; + res.options.body = value; + res.options.code = self.status || 200; + res.options.type = CT_HTML; + res.options.headers = headers; + res.$text(); + F.stats.response.view++; + return self; + } + + if (partial) + self.outputPartial = value; + else + self.output = value; + + if (!cachekey && !noasync) { + self.isLayout = true; + value = self.view(self.layoutName, self.$model, headers, partial); + } + + // Async + if (partial) { + self.outputPartial = ''; + self.isLayout = false; + done.callback && done.callback(null, value); + } + + }); + + return cachekey ? value : (partial ? (fn => done.callback = fn) : self); + } + } + + if (!isLayout && self.precache && (!self.status || self.status === 200) && !partial && !self.$viewasync) self.precache(value, CT_HTML, headers, true); if (isLayout || !self.layoutName) { @@ -12450,6 +15199,20 @@ Controller.prototype.$viewrender = function(filename, generator, model, headers, if (!self.isConnected) return self; + var components = self.repository[REPOSITORY_COMPONENTS]; + if (components) { + var keys = Object.keys(components); + var plus = ''; + for (var i = 0; i < keys.length; i++) { + var com = F.components.groups[keys[i]]; + if (com) + plus += com.links; + } + // Cleans cache + self.repository[REPOSITORY_COMPONENTS] = null; + value = value.replace('', plus + ''); + } + var res = self.res; res.options.body = value; res.options.code = self.status || 200; @@ -12465,9 +15228,12 @@ Controller.prototype.$viewrender = function(filename, generator, model, headers, else self.output = value; - self.isLayout = true; - value = self.view(self.layoutName, self.$model, headers, partial); + if (!cachekey && !noasync) { + self.isLayout = true; + value = self.view(self.layoutName, self.$model, headers, partial); + } + // Async if (partial) { self.outputPartial = ''; self.isLayout = false; @@ -12481,12 +15247,12 @@ Controller.prototype.$viewrender = function(filename, generator, model, headers, * Creates a cache for the response without caching layout * @param {String} key * @param {String} expires Expiration, e.g. `1 minute` - * @param {Boolean} disabled Disables a caching, optinoal (e.g. for debug mode you can disable a cache), default: `false` + * @param {Boolean} disabled Disables a caching, optional (e.g. for debug mode you can disable a cache), default: `false` * @param {Function()} fnTo This method is executed when the content is prepared for the cache. * @param {Function()} fnFrom This method is executed when the content is readed from the cache. * @return {Controller} */ -Controller.prototype.memorize = function(key, expires, disabled, fnTo, fnFrom) { +ControllerProto.memorize = function(key, expires, disabled, fnTo, fnFrom) { var self = this; @@ -12497,7 +15263,7 @@ Controller.prototype.memorize = function(key, expires, disabled, fnTo, fnFrom) { self.themeName && (key += '#' + self.themeName); - var output = self.cache.read2(key); + var output = F.cache.read2(key); if (!output) return self.$memorize_prepare(key, expires, disabled, fnTo, fnFrom); @@ -12545,7 +15311,7 @@ Controller.prototype.memorize = function(key, expires, disabled, fnTo, fnFrom) { fnFrom && fnFrom.call(self); if (self.layoutName) { - self.output = U.createBuffer(output.content); + self.output = Buffer.from(output.content); self.isLayout = true; self.view(self.layoutName, null); } else { @@ -12556,7 +15322,7 @@ Controller.prototype.memorize = function(key, expires, disabled, fnTo, fnFrom) { return self; }; -Controller.prototype.$memorize_prepare = function(key, expires, disabled, fnTo, fnFrom) { +ControllerProto.$memorize_prepare = function(key, expires, disabled, fnTo, fnFrom) { var self = this; var pk = '$memorize' + key; @@ -12588,7 +15354,7 @@ Controller.prototype.$memorize_prepare = function(key, expires, disabled, fnTo, } } - self.cache.add(key, options, expires, false); + F.cache.add(key, options, expires, false); self.precache = null; delete F.temporary.processing[pk]; }; @@ -12643,30 +15409,32 @@ WebSocket.prototype = { }, get global() { + OBSOLETE('controller.global', 'Use: G'); return F.global; }, get config() { - return F.config; + OBSOLETE('controller.config', 'Use: CONF'); + return CONF; }, get cache() { + OBSOLETE('controller.cache', 'Use: F.cache or CACHE()'); return F.cache; }, get isDebug() { - return F.config.debug; + OBSOLETE('controller.isDebug', 'Use: DEBUG'); + return DEBUG; }, get path() { + OBSOLETE('controller.path', 'Use: PATH'); return F.path; }, - get fs() { - return F.fs; - }, - get isSecure() { + OBSOLETE('controller.isSecure', 'Use: controller.secured'); return this.req.isSecure; }, @@ -12689,10 +15457,22 @@ WebSocket.prototype = { this.$params = EMPTYOBJECT; return EMPTYOBJECT; } + }, + + get keys() { + return this._keys; } }; -WebSocket.prototype.emit = function(name, a, b, c, d, e, f, g) { + +const WebSocketProto = WebSocket.prototype; + +WebSocketProto.operation = function(name, value, callback, options) { + OPERATION(name, value, callback, options, this); + return this; +}; + +WebSocketProto.emit = function(name, a, b, c, d, e, f, g) { var evt = this.$events[name]; if (evt) { var clean = false; @@ -12712,7 +15492,7 @@ WebSocket.prototype.emit = function(name, a, b, c, d, e, f, g) { return this; }; -WebSocket.prototype.on = function(name, fn) { +WebSocketProto.on = function(name, fn) { if (this.$events[name]) this.$events[name].push(fn); else @@ -12720,12 +15500,12 @@ WebSocket.prototype.on = function(name, fn) { return this; }; -WebSocket.prototype.once = function(name, fn) { +WebSocketProto.once = function(name, fn) { fn.$once = true; return this.on(name, fn); }; -WebSocket.prototype.removeListener = function(name, fn) { +WebSocketProto.removeListener = function(name, fn) { var evt = this.$events[name]; if (evt) { evt = evt.remove(n => n === fn); @@ -12737,7 +15517,7 @@ WebSocket.prototype.removeListener = function(name, fn) { return this; }; -WebSocket.prototype.removeAllListeners = function(name) { +WebSocketProto.removeAllListeners = function(name) { if (name === true) this.$events = EMPTYOBJECT; else if (name) @@ -12755,25 +15535,27 @@ WebSocket.prototype.removeAllListeners = function(name) { * @param {Function(key, value)} replacer for JSON (optional) * @return {WebSocket} */ -WebSocket.prototype.send = function(message, id, blacklist, replacer) { +WebSocketProto.send = function(message, id, blacklist, replacer) { - var keys = this._keys; - if (!keys || !keys.length) - return this; + var self = this; + var keys = self._keys; + + if (!keys || !keys.length || message === undefined) + return self; var data; var raw = false; for (var i = 0, length = keys.length; i < length; i++) { - var conn = this.connections[keys[i]]; + var conn = self.connections[keys[i]]; if (id) { if (id instanceof Array) { if (!websocket_valid_array(conn.id, id)) continue; } else if (id instanceof Function) { - if (!websocket_valid_fn(conn.id, conn, id)) + if (!websocket_valid_fn(conn.id, conn, id, message)) continue; } else throw new Error('Invalid "id" argument.'); @@ -12784,7 +15566,7 @@ WebSocket.prototype.send = function(message, id, blacklist, replacer) { if (websocket_valid_array(conn.id, blacklist)) continue; } else if (blacklist instanceof Function) { - if (websocket_valid_fn(conn.id, conn, blacklist)) + if (websocket_valid_fn(conn.id, conn, blacklist, message)) continue; } else throw new Error('Invalid "blacklist" argument.'); @@ -12802,22 +15584,59 @@ WebSocket.prototype.send = function(message, id, blacklist, replacer) { F.stats.response.websocket++; } - return this; + return self; +}; + +WebSocketProto.send2 = function(message, comparer, replacer, params) { + + var self = this; + var keys = self._keys; + if (!keys || !keys.length || message === undefined) + return self; + + if (!params && replacer != null && typeof(replacer) !== 'function') { + params = replacer; + replacer = null; + } + + var data; + var raw = false; + + for (var i = 0, length = keys.length; i < length; i++) { + + var conn = self.connections[keys[i]]; + + if (data === undefined) { + if (conn.type === 3) { + raw = true; + data = JSON.stringify(message, replacer); + } else + data = message; + } + + if (comparer && !comparer(conn, message, params)) + continue; + + conn.send(data, raw); + F.stats.response.websocket++; + } + + return self; }; function websocket_valid_array(id, arr) { return arr.indexOf(id) !== -1; } -function websocket_valid_fn(id, client, fn) { - return fn && fn(id, client) ? true : false; +function websocket_valid_fn(id, client, fn, msg) { + return fn && fn(id, client, msg) ? true : false; } /** * Sends a ping message * @return {WebSocket} */ -WebSocket.prototype.ping = function() { +WebSocketProto.ping = function() { var keys = this._keys; if (!keys) @@ -12841,7 +15660,7 @@ WebSocket.prototype.ping = function() { * @param {Number} code Optional default 1000. * @return {Websocket} */ -WebSocket.prototype.close = function(id, message, code) { +WebSocketProto.close = function(id, message, code) { var keys = this._keys; @@ -12894,7 +15713,7 @@ WebSocket.prototype.close = function(id, message, code) { * @param {Error/String} err * @return {WebSocket/Function} */ -WebSocket.prototype.error = function(err) { +WebSocketProto.error = function(err) { var result = F.error(typeof(err) === 'string' ? new Error(err) : err, this.name, this.path); return err ? this : result; }; @@ -12904,7 +15723,7 @@ WebSocket.prototype.error = function(err) { * @param {String} message * @return {WebSocket} */ -WebSocket.prototype.wtf = WebSocket.prototype.problem = function(message) { +WebSocketProto.wtf = WebSocketProto.problem = function(message) { F.problem(message, this.name, this.uri); return this; }; @@ -12914,7 +15733,7 @@ WebSocket.prototype.wtf = WebSocket.prototype.problem = function(message) { * @param {String} message * @return {WebSocket} */ -WebSocket.prototype.change = function(message) { +WebSocketProto.change = function(message) { F.change(message, this.name, this.uri, this.ip); return this; }; @@ -12924,12 +15743,18 @@ WebSocket.prototype.change = function(message) { * @param {Function(connection, index)} fn * @return {WebSocket} */ -WebSocket.prototype.all = function(fn) { - if (this._keys) { - for (var i = 0, length = this._keys.length; i < length; i++) - fn(this.connections[this._keys[i]], i); +WebSocketProto.all = function(fn) { + var arr = fn == null || fn == true ? [] : null; + var self = this; + if (self._keys) { + for (var i = 0, length = self._keys.length; i < length; i++) { + if (arr) + arr.push(self.connections[self._keys[i]]); + else + fn(self.connections[self._keys[i]], i); + } } - return this; + return arr ? arr : self; }; /** @@ -12937,7 +15762,7 @@ WebSocket.prototype.all = function(fn) { * @param {String} id * @return {WebSocketClient} */ -WebSocket.prototype.find = function(id) { +WebSocketProto.find = function(id) { var self = this; if (!self._keys) @@ -12948,17 +15773,12 @@ WebSocket.prototype.find = function(id) { for (var i = 0; i < length; i++) { var connection = self.connections[self._keys[i]]; - - if (!isFn) { - if (connection.id === id) + if (isFn) { + if (id(connection, connection.id)) return connection; - continue; - } - - if (id(connection, connection.id)) + } else if (connection.id === id) return connection; } - return null; }; @@ -12967,7 +15787,7 @@ WebSocket.prototype.find = function(id) { * @param {String} problem Optional. * @return {WebSocket} */ -WebSocket.prototype.destroy = function(problem) { +WebSocketProto.destroy = function(problem) { var self = this; problem && self.problem(problem); @@ -12979,13 +15799,14 @@ WebSocket.prototype.destroy = function(problem) { setTimeout(function() { - self._keys.forEach(function(key) { + for (var i = 0; i < self._keys.length; i++) { + var key = self._keys[i]; var conn = self.connections[key]; if (conn) { conn._isClosed = true; conn.socket.removeAllListeners(); } - }); + } self.connections = null; self._keys = null; @@ -12993,6 +15814,7 @@ WebSocket.prototype.destroy = function(problem) { self.buffer = null; delete F.connections[self.id]; self.removeAllListeners(); + }, 1000); return self; @@ -13003,7 +15825,7 @@ WebSocket.prototype.destroy = function(problem) { * @param {Function} callback * @return {WebSocket] */ -WebSocket.prototype.autodestroy = function(callback) { +WebSocketProto.autodestroy = function(callback) { var self = this; var key = 'websocket:' + self.id; self.on('open', () => clearTimeout2(key)); @@ -13020,7 +15842,7 @@ WebSocket.prototype.autodestroy = function(callback) { * Internal function * @return {WebSocket} */ -WebSocket.prototype.$refresh = function() { +WebSocketProto.$refresh = function() { if (this.connections) { this._keys = Object.keys(this.connections); this.online = this._keys.length; @@ -13034,7 +15856,7 @@ WebSocket.prototype.$refresh = function() { * @param {String} id * @return {WebSocket} */ -WebSocket.prototype.$remove = function(id) { +WebSocketProto.$remove = function(id) { if (this.connections) delete this.connections[id]; return this; @@ -13045,7 +15867,7 @@ WebSocket.prototype.$remove = function(id) { * @param {WebSocketClient} client * @return {WebSocket} */ -WebSocket.prototype.$add = function(client) { +WebSocketProto.$add = function(client) { this.connections[client._id] = client; return this; }; @@ -13056,21 +15878,21 @@ WebSocket.prototype.$add = function(client) { * @param {String} key A resource key. * @return {String} */ -WebSocket.prototype.resource = function(name, key) { +WebSocketProto.resource = function(name, key) { return F.resource(name, key); }; -WebSocket.prototype.log = function() { +WebSocketProto.log = function() { F.log.apply(framework, arguments); return this; }; -WebSocket.prototype.logger = function() { +WebSocketProto.logger = function() { F.logger.apply(framework, arguments); return this; }; -WebSocket.prototype.check = function() { +WebSocketProto.check = function() { this.$ping && this.all(websocketcheck_ping); return this; }; @@ -13126,18 +15948,28 @@ WebSocketClient.prototype = { return this.req.query; }, + get headers() { + return this.req.headers; + }, + get uri() { return this.req.uri; }, get config() { + OBSOLETE('controller.config', 'Use: CONF'); return this.container.config; }, get global() { + OBSOLETE('controller.global', 'Use: G'); return this.container.global; }, + get sessionid() { + return this.req.sessionid; + }, + get session() { return this.req.session; }, @@ -13159,13 +15991,37 @@ WebSocketClient.prototype = { } }; -WebSocketClient.prototype.isWebSocket = true; +const WebSocketClientProto = WebSocketClient.prototype; + +WebSocketClientProto.isWebSocket = true; -WebSocketClient.prototype.cookie = function(name) { +WebSocketClientProto.cookie = function(name) { return this.req.cookie(name); }; -WebSocketClient.prototype.prepare = function(flags, protocols, allow, length) { +WebSocketClientProto.$close = function(code, message) { + + var self = this; + + if ((self.req.headers['user-agent'] || '').indexOf('Total.js') !== -1) { + self.close(); + return; + } + + var header = SOCKET_RESPONSE.format(self.$websocket_key(self.req)); + self.socket.write(Buffer.from(header, 'binary')); + self.ready = true; + self.close(message, code); + + setTimeout(function(self) { + self.req = null; + self.socket = null; + }, 1000, self); + + return self; +}; + +WebSocketClientProto.prepare = function(flags, protocols, allow, length) { flags = flags || EMPTYARRAY; protocols = protocols || EMPTYARRAY; @@ -13178,7 +16034,7 @@ WebSocketClient.prototype.prepare = function(flags, protocols, allow, length) { self.length = length; - var origin = self.req.headers['origin'] || ''; + var origin = self.req.headers.origin || ''; var length = allow.length; if (length && allow.indexOf('*') === -1) { @@ -13201,10 +16057,11 @@ WebSocketClient.prototype.prepare = function(flags, protocols, allow, length) { } } - var compress = (F.config['allow-websocket-compression'] && self.req.headers['sec-websocket-extensions'] || '').indexOf('permessage-deflate') !== -1; + var compress = (CONF.allow_websocket_compression && self.req.headers['sec-websocket-extensions'] || '').indexOf('permessage-deflate') !== -1; var header = protocols.length ? (compress ? SOCKET_RESPONSE_PROTOCOL_COMPRESS : SOCKET_RESPONSE_PROTOCOL).format(self.$websocket_key(self.req), protocols.join(', ')) : (compress ? SOCKET_RESPONSE_COMPRESS : SOCKET_RESPONSE).format(self.$websocket_key(self.req)); - self.socket.write(U.createBuffer(header, 'binary')); + self.socket.write(Buffer.from(header, 'binary')); + self.ready = true; if (compress) { self.inflatepending = []; @@ -13212,10 +16069,10 @@ WebSocketClient.prototype.prepare = function(flags, protocols, allow, length) { self.inflate = Zlib.createInflateRaw(WEBSOCKET_COMPRESS_OPTIONS); self.inflate.$websocket = self; self.inflate.on('error', function() { - if (self.$uerror) - return; - self.$uerror = true; - self.close('Unexpected error'); + if (!self.$uerror) { + self.$uerror = true; + self.close('Invalid data', 1003); + } }); self.inflate.on('data', websocket_inflate); @@ -13224,10 +16081,10 @@ WebSocketClient.prototype.prepare = function(flags, protocols, allow, length) { self.deflate = Zlib.createDeflateRaw(WEBSOCKET_COMPRESS_OPTIONS); self.deflate.$websocket = self; self.deflate.on('error', function() { - if (self.$uerror) - return; - self.$uerror = true; - self.close('Unexpected error'); + if (!self.$uerror) { + self.$uerror = true; + self.close('Invalid data', 1003); + } }); self.deflate.on('data', websocket_deflate); } @@ -13238,13 +16095,19 @@ WebSocketClient.prototype.prepare = function(flags, protocols, allow, length) { }; function websocket_inflate(data) { - this.$websocket.inflatechunks.push(data); - this.$websocket.inflatechunkslength += data.length; + var ws = this.$websocket; + if (ws && ws.inflatechunks) { + ws.inflatechunks.push(data); + ws.inflatechunkslength += data.length; + } } function websocket_deflate(data) { - this.$websocket.deflatechunks.push(data); - this.$websocket.deflatechunkslength += data.length; + var ws = this.$websocket; + if (ws && ws.deflatechunks) { + ws.deflatechunks.push(data); + ws.deflatechunkslength += data.length; + } } /** @@ -13252,7 +16115,7 @@ function websocket_deflate(data) { * @param {WebSocket} container * @return {WebSocketClient} */ -WebSocketClient.prototype.upgrade = function(container) { +WebSocketClientProto.upgrade = function(container) { var self = this; self.req.on('error', websocket_onerror); self.container = container; @@ -13263,8 +16126,10 @@ WebSocketClient.prototype.upgrade = function(container) { self.socket.on('end', websocket_close); self.container.$add(self); self.container.$refresh(); - F.$events['websocket-begin'] && F.emit('websocket-begin', self.container, self); + F.$events['websocket-begin'] && EMIT('websocket-begin', self.container, self); + F.$events.websocket_begin && EMIT('websocket_begin', self.container, self); self.container.$events.open && self.container.emit('open', self); + F.stats.performance.online++; return self; }; @@ -13282,12 +16147,14 @@ function websocket_close() { this.$websocket.$onclose(); } -WebSocketClient.prototype.$ondata = function(data) { +WebSocketClientProto.$ondata = function(data) { - if (this.isClosed) + var self = this; + + if (self.isClosed) return; - var current = this.current; + var current = self.current; if (data) { if (current.buffer) { @@ -13298,27 +16165,30 @@ WebSocketClient.prototype.$ondata = function(data) { current.buffer = data; } - if (!this.$parse()) + if (!self.$parse()) return; if (!current.final && current.type !== 0x00) current.type2 = current.type; + var decompress = current.compressed && self.inflate; var tmp; switch (current.type === 0x00 ? current.type2 : current.type) { case 0x01: // text - if (this.inflate) { - current.final && this.parseInflate(); + if (decompress) { + current.final && self.parseInflate(); } else { - tmp = this.$readbody(); - if (current.body) - current.body += tmp; - else + tmp = self.$readbody(); + if (current.body) { + CONCAT[0] = current.body; + CONCAT[1] = tmp; + current.body = Buffer.concat(CONCAT); + } else current.body = tmp; - current.final && this.$decode(); + current.final && self.$decode(); } break; @@ -13326,37 +16196,43 @@ WebSocketClient.prototype.$ondata = function(data) { case 0x02: // binary - if (this.inflate) { - current.final && this.parseInflate(); + if (decompress) { + current.final && self.parseInflate(); } else { - tmp = this.$readbody(); + tmp = self.$readbody(); if (current.body) { CONCAT[0] = current.body; CONCAT[1] = tmp; current.body = Buffer.concat(CONCAT); } else current.body = tmp; - current.final && this.$decode(); + current.final && self.$decode(); } break; case 0x08: // close - this.close(); + self.closemessage = current.buffer.slice(4).toString('utf8'); + self.closecode = current.buffer[2] << 8 | current.buffer[3]; + + if (self.closemessage && self.container.encodedecode) + self.closemessage = $decodeURIComponent(self.closemessage); + + self.close(); break; case 0x09: // ping, response pong - this.socket.write(U.getWebSocketFrame(0, 'PONG', 0x0A)); + self.socket.write(U.getWebSocketFrame(0, 'PONG', 0x0A)); current.buffer = null; current.inflatedata = null; - this.$ping = true; + self.$ping = true; break; case 0x0a: // pong - this.$ping = true; + self.$ping = true; current.buffer = null; current.inflatedata = null; break; @@ -13364,12 +16240,12 @@ WebSocketClient.prototype.$ondata = function(data) { if (current.buffer) { current.buffer = current.buffer.slice(current.length, current.buffer.length); - current.buffer.length && this.$ondata(); + current.buffer.length && self.$ondata(); } }; function buffer_concat(buffers, length) { - var buffer = U.createBufferSize(length); + var buffer = Buffer.alloc(length); var offset = 0; for (var i = 0, n = buffers.length; i < n; i++) { buffers[i].copy(buffer, offset); @@ -13381,17 +16257,21 @@ function buffer_concat(buffers, length) { // MIT // Written by Jozef Gula // Optimized by Peter Sirka -WebSocketClient.prototype.$parse = function() { +WebSocketClientProto.$parse = function() { var self = this; var current = self.current; // check end message - if (!current.buffer || current.buffer.length <= 2 || ((current.buffer[0] & 0x80) >> 7) !== 1) + + // Long messages doesn't work because 0x80 still returns 0 + // if (!current.buffer || current.buffer.length <= 2 || ((current.buffer[0] & 0x80) >> 7) !== 1) + if (!current.buffer || current.buffer.length <= 2) return; - // webSocked - Opcode + // WebSocket - Opcode current.type = current.buffer[0] & 0x0f; + current.compressed = (current.buffer[0] & 0x40) === 0x40; // is final message? current.final = ((current.buffer[0] & 0x80) >> 7) === 0x01; @@ -13403,15 +16283,18 @@ WebSocketClient.prototype.$parse = function() { var length = U.getMessageLength(current.buffer, F.isLE); // index for data + // Solving a problem with The value "-1" is invalid for option "size" + if (length <= 0) + return; + var index = current.buffer[1] & 0x7f; index = ((index === 126) ? 4 : (index === 127 ? 10 : 2)) + (current.isMask ? 4 : 0); // total message length (data + header) var mlength = index + length; - // ??? if (mlength > this.length) { - this.close('Maximum request length exceeded.'); + this.close('Frame is too large', 1009); return; } @@ -13426,13 +16309,13 @@ WebSocketClient.prototype.$parse = function() { // does frame contain mask? if (current.isMask) { - current.mask = U.createBufferSize(4); + current.mask = Buffer.alloc(4); current.buffer.copy(current.mask, 0, index - 4, index); } - if (this.inflate) { + if (current.compressed && this.inflate) { - var buf = U.createBufferSize(length); + var buf = Buffer.alloc(length); current.buffer.copy(buf, 0, index, mlength); // does frame contain mask? @@ -13445,7 +16328,7 @@ WebSocketClient.prototype.$parse = function() { buf.$continue = current.final === false; this.inflatepending.push(buf); } else { - current.data = U.createBufferSize(length); + current.data = Buffer.alloc(length); current.buffer.copy(current.data, 0, index, mlength); } } @@ -13453,65 +16336,64 @@ WebSocketClient.prototype.$parse = function() { return true; }; -WebSocketClient.prototype.$readbody = function() { - +WebSocketClientProto.$readbody = function() { var current = this.current; var length = current.data.length; - var buf; - - if (current.type === 1) { - - buf = U.createBufferSize(length); - for (var i = 0; i < length; i++) { - if (current.isMask) - buf[i] = current.data[i] ^ current.mask[i % 4]; - else - buf[i] = current.data[i]; - } - - return buf.toString('utf8'); - - } else { - - buf = U.createBufferSize(length); - for (var i = 0; i < length; i++) { - // does frame contain mask? - if (current.isMask) - buf[i] = current.data[i] ^ current.mask[i % 4]; - else - buf[i] = current.data[i]; - } - return buf; + var buf = Buffer.alloc(length); + for (var i = 0; i < length; i++) { + // does frame contain mask? + if (current.isMask) + buf[i] = current.data[i] ^ current.mask[i % 4]; + else + buf[i] = current.data[i]; } + return buf; }; -WebSocketClient.prototype.$decode = function() { +WebSocketClientProto.$decode = function() { + var data = this.current.body; + F.stats.performance.message++; + + // Buffer + if (this.typebuffer) { + this.container.emit('message', this, data); + return; + } switch (this.type) { case 1: // BINARY - this.container.emit('message', this, new Uint8Array(data).buffer); + // this.container.emit('message', this, new Uint8Array(data).buffer); + this.container.emit('message', this, data); break; case 3: // JSON + if (data instanceof Buffer) data = data.toString(ENCODING); - F.config['default-websocket-encodedecode'] === true && (data = $decodeURIComponent(data)); - data.isJSON() && this.container.emit('message', this, F.onParseJSON(data, this.req)); + + if (this.container.encodedecode === true) + data = $decodeURIComponent(data); + + if (data.isJSON()) { + var tmp = F.onParseJSON(data, this.req); + if (tmp !== undefined) + this.container.emit('message', this, tmp); + } break; default: // TEXT if (data instanceof Buffer) data = data.toString(ENCODING); - this.container.emit('message', this, F.config['default-websocket-encodedecode'] === true ? $decodeURIComponent(data) : data); + this.container.emit('message', this, this.container.encodedecode === true ? $decodeURIComponent(data) : data); break; } this.current.body = null; }; -WebSocketClient.prototype.parseInflate = function() { +WebSocketClientProto.parseInflate = function() { var self = this; if (self.inflatelock) @@ -13523,7 +16405,7 @@ WebSocketClient.prototype.parseInflate = function() { self.inflatechunkslength = 0; self.inflatelock = true; self.inflate.write(buf); - !buf.$continue && self.inflate.write(U.createBuffer(WEBSOCKET_COMPRESS)); + !buf.$continue && self.inflate.write(Buffer.from(WEBSOCKET_COMPRESS)); self.inflate.flush(function() { if (!self.inflatechunks) @@ -13535,7 +16417,7 @@ WebSocketClient.prototype.parseInflate = function() { self.inflatelock = false; if (data.length > self.length) { - self.close('Maximum request length exceeded.'); + self.close('Frame is too large', 1009); return; } @@ -13552,7 +16434,7 @@ WebSocketClient.prototype.parseInflate = function() { } }; -WebSocketClient.prototype.$onerror = function(err) { +WebSocketClientProto.$onerror = function(err) { if (this.isClosed) return; @@ -13564,10 +16446,12 @@ WebSocketClient.prototype.$onerror = function(err) { this.container.$events.error && this.container.emit('error', err, this); }; -WebSocketClient.prototype.$onclose = function() { +WebSocketClientProto.$onclose = function() { + if (this._isClosed) return; + F.stats.performance.online--; this.isClosed = true; this._isClosed = true; @@ -13585,9 +16469,10 @@ WebSocketClient.prototype.$onclose = function() { this.container.$remove(this._id); this.container.$refresh(); - this.container.$events.close && this.container.emit('close', this); + this.container.$events.close && this.container.emit('close', this, this.closecode, this.closemessage); this.socket.removeAllListeners(); - F.$events['websocket-end'] && F.emit('websocket-end', this.container, this); + F.$events['websocket-end'] && EMIT('websocket-end', this.container, this); + F.$events.websocket_end && EMIT('websocket_end', this.container, this); }; /** @@ -13596,32 +16481,34 @@ WebSocketClient.prototype.$onclose = function() { * @param {Boolean} raw The message won't be converted e.g. to JSON. * @return {WebSocketClient} */ -WebSocketClient.prototype.send = function(message, raw, replacer) { +WebSocketClientProto.send = function(message, raw, replacer) { - if (this.isClosed) - return this; + var self = this; + + if (self.isClosed) + return self; - if (this.type !== 1) { - var data = this.type === 3 ? (raw ? message : JSON.stringify(message, replacer)) : (message || '').toString(); - if (F.config['default-websocket-encodedecode'] === true && data) + if (self.type !== 1) { + var data = self.type === 3 ? (raw ? message : JSON.stringify(message, replacer)) : typeof(message) === 'object' ? JSON.stringify(message, replacer) : message.toString(); + if (self.container.encodedecode === true && data) data = encodeURIComponent(data); - if (this.deflate) { - this.deflatepending.push(U.createBuffer(data)); - this.sendDeflate(); + if (self.deflate) { + self.deflatepending.push(Buffer.from(data)); + self.sendDeflate(); } else - this.socket.write(U.getWebSocketFrame(0, data, 0x01)); + self.socket.write(U.getWebSocketFrame(0, data, 0x01)); } else if (message) { - if (this.deflate) { - this.deflatepending.push(U.createBuffer(message)); - this.sendDeflate(); + if (self.deflate) { + self.deflatepending.push(Buffer.from(message)); + self.sendDeflate(); } else - this.socket.write(U.getWebSocketFrame(0, new Int8Array(message), 0x02)); + self.socket.write(U.getWebSocketFrame(0, new Int8Array(message), 0x02)); } - return this; + return self; }; -WebSocketClient.prototype.sendDeflate = function() { +WebSocketClientProto.sendDeflate = function() { var self = this; if (self.deflatelock) @@ -13634,14 +16521,14 @@ WebSocketClient.prototype.sendDeflate = function() { self.deflatelock = true; self.deflate.write(buf); self.deflate.flush(function() { - if (!self.deflatechunks) - return; - var data = buffer_concat(self.deflatechunks, self.deflatechunkslength); - data = data.slice(0, data.length - 4); - self.deflatelock = false; - self.deflatechunks = null; - self.socket.write(U.getWebSocketFrame(0, data, self.type === 1 ? 0x02 : 0x01, true)); - self.sendDeflate(); + if (self.deflatechunks) { + var data = buffer_concat(self.deflatechunks, self.deflatechunkslength); + data = data.slice(0, data.length - 4); + self.deflatelock = false; + self.deflatechunks = null; + self.socket.write(U.getWebSocketFrame(0, data, self.type === 1 ? 0x02 : 0x01, true)); + self.sendDeflate(); + } }); } }; @@ -13650,7 +16537,7 @@ WebSocketClient.prototype.sendDeflate = function() { * Ping message * @return {WebSocketClient} */ -WebSocketClient.prototype.ping = function() { +WebSocketClientProto.ping = function() { if (!this.isClosed) { this.socket.write(U.getWebSocketFrame(0, 'PING', 0x09)); this.$ping = false; @@ -13664,12 +16551,19 @@ WebSocketClient.prototype.ping = function() { * @param {Number} code WebSocket code. * @return {WebSocketClient} */ -WebSocketClient.prototype.close = function(message, code) { - if (!this.isClosed) { - this.isClosed = true; - this.socket.end(U.getWebSocketFrame(code || 1000, message ? (F.config['default-websocket-encodedecode'] ? encodeURIComponent(message) : message) : '', 0x08)); +WebSocketClientProto.close = function(message, code) { + var self = this; + if (!self.isClosed) { + self.isClosed = true; + if (self.ready) { + if (message && self.container && self.container.encodedecode) + message = encodeURIComponent(message); + self.socket.end(U.getWebSocketFrame(code || 1000, message || '', 0x08)); + } else + self.socket.end(); + self.req.connection.destroy(); } - return this; + return self; }; /** @@ -13677,7 +16571,7 @@ WebSocketClient.prototype.close = function(message, code) { * @param {Request} req * @return {String} */ -WebSocketClient.prototype.$websocket_key = function(req) { +WebSocketClientProto.$websocket_key = function(req) { var sha1 = Crypto.createHash('sha1'); sha1.update((req.headers['sec-websocket-key'] || '') + SOCKET_HASH); return sha1.digest('base64'); @@ -13689,6 +16583,59 @@ WebSocketClient.prototype.$websocket_key = function(req) { // ================================================================================= // ********************************************************************************* +function req_authorizecallback(isAuthorized, user, $) { + + // @isAuthorized "null" for callbacks(err, user) + // @isAuthorized "true" + // @isAuthorized "object" is as user but "user" must be "undefined" + + if (isAuthorized instanceof Error || isAuthorized instanceof ErrorBuilder) { + // Error handling + isAuthorized = false; + } else if (isAuthorized == null && user) { + // A callback error handling + isAuthorized = true; + } else if (user == null && isAuthorized && isAuthorized !== true) { + user = isAuthorized; + isAuthorized = true; + } + + $.req.isAuthorized = isAuthorized; + $.req.authorizecallback(null, user, isAuthorized); + $.req.authorizecallback = null; +} + +function req_authorizetotal(isAuthorized, user, $) { + + // @isAuthorized "null" for callbacks(err, user) + // @isAuthorized "true" + // @isAuthorized "object" is as user but "user" must be "undefined" + + var req = $.req; + var roles = req.flagslength !== req.flags.length; + + if (roles) { + req.$flags += req.flags.slice(req.flagslength).join(''); + req.$roles = true; + } + + req.flagslength = undefined; + + if (isAuthorized instanceof Error || isAuthorized instanceof ErrorBuilder) { + // Error handling + isAuthorized = false; + } else if (isAuthorized == null && user) { + // A callback error handling + isAuthorized = true; + } else if (user == null && isAuthorized && isAuthorized !== true) { + user = isAuthorized; + isAuthorized = true; + } + + req.isAuthorized = isAuthorized; + req.$total_authorize(isAuthorized, user, roles); +} + function extend_request(PROTO) { PROTOREQ = PROTO; @@ -13723,9 +16670,11 @@ function extend_request(PROTO) { get: function() { if (this._subdomain) return this._subdomain; - var subdomain = this.uri.host.toLowerCase().replace(/^www\./i, '').split('.'); - if (subdomain.length > 2) - this._subdomain = subdomain.slice(0, subdomain.length - 2); // example: [subdomain].domain.com + var subdomain = this.uri.hostname.toLowerCase().replace(REG_WWW, '').split('.'); + if (subdomain.length > 2) // example: [subdomain].domain.com + this._subdomain = subdomain.slice(0, subdomain.length - 2); + else if (subdomain.length > 1 && subdomain[subdomain.length - 1] === 'localhost') // example: [subdomain].localhost + this._subdomain = subdomain.slice(0, subdomain.length - 1); else this._subdomain = null; return this._subdomain; @@ -13761,6 +16710,14 @@ function extend_request(PROTO) { } }); + Object.defineProperty(PROTO, 'ua', { + get: function() { + if (this.$ua === undefined) + this.$ua = (this.headers['user-agent'] || '').parseUA(); + return this.$ua; + } + }); + Object.defineProperty(PROTO, 'mobile', { get: function() { if (this.$mobile === undefined) @@ -13795,13 +16752,14 @@ function extend_request(PROTO) { * Disable HTTP cache for current request * @return {Request} */ - PROTO.noCache = function() { + PROTO.noCache = PROTO.nocache = function() { this.res && this.res.noCache(); return this; }; - PROTO.notModified = function(compare, strict) { - return F.notModified(this, this.res, compare, strict); + PROTO.useragent = function(structured) { + var key = structured ? '$ua2' : '$ua'; + return this[key] ? this[key] : this[key] = (this.headers['user-agent'] || '').parseUA(structured); }; /** @@ -13845,7 +16803,7 @@ function extend_request(PROTO) { var result = { user: '', password: '', empty: true }; try { - var arr = U.createBuffer(authorization.replace('Basic ', '').trim(), 'base64').toString(ENCODING).split(':'); + var arr = Buffer.from(authorization.replace('Basic ', '').trim(), 'base64').toString(ENCODING).split(':'); result.user = arr[0] || ''; result.password = arr[1] || ''; result.empty = !result.user || !result.password; @@ -13861,20 +16819,41 @@ function extend_request(PROTO) { */ PROTO.authorize = function(callback) { - var auth = F.onAuthorize; + var req = this; - if (!auth) { + if (!F.onAuthorize) { callback(null, null, false); - return this; + return req; } - var req = this; + if (F.onAuthorize.$newversion) { + req.authorizecallback = callback; + F.onAuthorize(req, req.res, req.flags, req_authorizecallback); + return req; + } - auth(req, req.res, req.flags, function(isAuthorized, user) { - if (typeof(isAuthorized) !== 'boolean') { + F.onAuthorize(req, req.res, req.flags || [], function(isAuthorized, user) { + + if (!F.onAuthorize.isobsolete) { + F.onAuthorize.isobsolete = 1; + OBSOLETE('F.onAuthorize', 'You need to use a new authorization declaration: "AUTH(function($) {})"'); + } + + // @isAuthorized "null" for callbacks(err, user) + // @isAuthorized "true" + // @isAuthorized "object" is as user but "user" must be "undefined" + + if (isAuthorized instanceof Error || isAuthorized instanceof ErrorBuilder) { + // Error handling + isAuthorized = false; + } else if (isAuthorized == null && user) { + // A callback error handling + isAuthorized = true; + } else if (user == null && isAuthorized && isAuthorized !== true) { user = isAuthorized; - isAuthorized = !user; + isAuthorized = true; } + req.isAuthorized = isAuthorized; callback(null, user, isAuthorized); }); @@ -13929,6 +16908,10 @@ function extend_request(PROTO) { return uri.protocol + '//' + uri.hostname + (uri.port && uri.port !== 80 ? ':' + uri.port : '') + (path || ''); }; + PROTO.filecache = function(callback) { + F.exists(this, this.res, 20, callback); + }; + PROTO.$total_success = function() { this.$total_timeout && clearTimeout(this.$total_timeout); this.$total_canceled = true; @@ -13952,7 +16935,7 @@ function extend_request(PROTO) { this.$total_header = header; if (this.$total_route) { F.path.verify('temp'); - framework_internal.parseMULTIPART(this, header, this.$total_route, F.config['directory-temp']); + framework_internal.parseMULTIPART(this, header, this.$total_route, CONF.directory_temp); } else this.$total_status(404); }; @@ -13978,7 +16961,8 @@ function extend_request(PROTO) { F.reqstats(false, false); this.res.writeHead(status); this.res.end(U.httpStatus(status)); - F.$events['request-end'] && F.emit('request-end', this, this.res); + F.$events['request-end'] && EMIT('request-end', this, this.res); + F.$events.request_end && EMIT('request_end', this, this.res); this.clear(true); }; @@ -13999,8 +16983,16 @@ function extend_request(PROTO) { var res = this.res; if (isError || !route) { - F.stats.response['error' + status]++; - status !== 500 && F.$events['error'] && F.emit('error' + status, this, res, this.$total_exception); + var key = 'error' + status; + F.stats.response[key]++; + status !== 500 && F.$events.error && EMIT('error', this, res, this.$total_exception); + + if (status === 408) { + if (F.timeouts.push((NOW = new Date()).toJSON() + ' ' + this.url) > 5) + F.timeouts.shift(); + } + + F.$events[key] && EMIT(key, this, res, this.$total_exception); } if (!route) { @@ -14012,8 +17004,13 @@ function extend_request(PROTO) { res.options.type = this.$total_exception.contentType; res.$text(); } else { - res.options.body = U.httpStatus(status) + prepare_error(this.$total_exception); - res.options.type = CT_TEXT; + + MODELERROR.code = status; + MODELERROR.status = U.httpStatus(status, false); + MODELERROR.error = this.$total_exception ? prepare_error(this.$total_exception) : null; + + res.options.body = VIEW('.' + PATHMODULES + 'error', MODELERROR); + res.options.type = CT_HTML; res.options.code = status || 404; res.$text(); } @@ -14064,21 +17061,23 @@ function extend_request(PROTO) { return; var ctrlname = '@' + name; - F.$events.controller && F.emit('controller', controller, name, this.$total_route.options); - F.$events[ctrlname] && F.emit(ctrlname, controller, name, this.$total_route.options); + F.$events.controller && EMIT('controller', controller, name, this.$total_route.options); + F.$events[ctrlname] && EMIT(ctrlname, controller, name, this.$total_route.options); if (controller.isCanceled) return; - if (this.$total_route.isCACHE && !F.temporary.other[this.uri.pathname]) + if (!controller.isTransfer && this.$total_route.isCACHE && !F.temporary.other[this.uri.pathname]) F.temporary.other[this.uri.pathname] = this.path; if (this.$total_route.isGENERATOR) async.call(controller, this.$total_route.execute, true)(controller, framework_internal.routeParam(this.$total_route.param.length ? this.split : this.path, this.$total_route)); else { - if (this.$total_route.param.length) - this.$total_route.execute.apply(controller, framework_internal.routeParam(this.split, this.$total_route)); - else + if (this.$total_route.param.length) { + var params = framework_internal.routeParam(this.split, this.$total_route); + controller.id = params[0]; + this.$total_route.execute.apply(controller, params); + } else this.$total_route.execute.call(controller); } @@ -14105,7 +17104,7 @@ function extend_request(PROTO) { return; this.buffer_exceeded = true; - this.buffer_data = U.createBuffer(); + this.buffer_data = Buffer.alloc(0); }; PROTO.$total_cancel = function() { @@ -14121,15 +17120,18 @@ function extend_request(PROTO) { PROTO.$total_validate = function(route, next, code) { - this.$total_schema = false; - - if (!this.$total_route.schema || this.method === 'DELETE') - return next(this, code); - var self = this; + self.$total_schema = false; + + if (!self.$total_route.schema) + return next(self, code); - F.onSchema(this, this.$total_route.schema[0], this.$total_route.schema[1], function(err, body) { + if (!self.$total_route.schema[1]) { + F.stats.request.operation++; + return next(self, code); + } + F.onSchema(self, self.$total_route, function(err, body) { if (err) { self.$total_400(err); next = null; @@ -14139,8 +17141,7 @@ function extend_request(PROTO) { self.$total_schema = true; next(self, code); } - - }, route.schema[2], route.novalidate); + }); }; PROTO.$total_authorize = function(isLogged, user, roles) { @@ -14238,8 +17239,18 @@ function extend_request(PROTO) { this.$total_400('Invalid JSON data.'); return; } - } else + } else { + + for (var i = 0; i < this.buffer_data.length - 2; i++) { + if (this.buffer_data[i] === '%' && this.buffer_data[i + 1] === '0' && this.buffer_data[i + 2] === '0') { + this.buffer_data = null; + this.$total_400('Not allowed chars in the request body.'); + return; + } + } + F.$onParseQueryBody(this); + } route.schema && (this.$total_schema = true); this.buffer_data = null; @@ -14254,45 +17265,40 @@ function extend_request(PROTO) { if (!F._length_files) return res.continue(); - for (var i = 0; i < F._length_files; i++) { + for (var i = 0; i < F.routes.files.length; i++) { var file = F.routes.files[i]; - try { + // try { - if (file.extensions && !file.extensions[req.extension]) - continue; - - if (file.url) { - var skip = false; - var length = file.url.length; + if (file.extensions && !file.extensions[req.extension]) + continue; - if (!file.wildcard && !file.fixedfile && length !== req.path.length - 1) - continue; + if (file.url) { + var skip = false; + var length = file.url.length; - for (var j = 0; j < length; j++) { - if (file.url[j] === req.path[j]) - continue; - skip = true; - break; - } + if (!file.wildcard && !file.fixedfile && length !== req.path.length - 1) + continue; - if (skip) + for (var j = 0; j < length; j++) { + if (file.url[j] === req.path[j]) continue; + skip = true; + break; + } - } else if (file.onValidate && !file.onValidate.call(F, req, res, true)) + if (skip) continue; - if (file.middleware) - req.$total_endfilemiddleware(file); - else - file.execute.call(F, req, res, false); - return; + } else if (file.onValidate && !file.onValidate(req, res, true)) + continue; - } catch (err) { - F.error(err, file.controller, req.uri); - res.throw500(); - return; - } + if (file.middleware) + req.$total_endfilemiddleware(file); + else + file.execute(req, res, false); + + return; } res.continue(); @@ -14309,6 +17315,12 @@ function extend_request(PROTO) { this.$total_execute(400, true); }; + PROTO.$total_404 = function(problem) { + this.$total_route = F.lookup(this, '#404', EMPTYARRAY, 0); + this.$total_exception = problem; + this.$total_execute(404, true); + }; + PROTO.$total_500 = function(problem) { this.$total_route = F.lookup(this, '#500', EMPTYARRAY, 0); this.$total_exception = problem; @@ -14316,22 +17328,49 @@ function extend_request(PROTO) { }; PROTO.$total_prepare = function() { - var req = this; var length = req.flags.length; - if (F.onAuthorize) { + + if (F.onAuthorize.$newversion) { + req.flagslength = length; + F.onAuthorize(req, req.res, req.flags, req_authorizetotal); + return; + } + F.onAuthorize(req, req.res, req.flags, function(isAuthorized, user) { - var hasRoles = length !== req.flags.length; - if (hasRoles) + + if (!F.onAuthorize.isobsolete) { + F.onAuthorize.isobsolete = 1; + OBSOLETE('F.onAuthorize', 'You need to use a new authorization declaration: "AUTH(function($) {})"'); + } + + // @isAuthorized "null" for callbacks(err, user) + // @isAuthorized "true" + // @isAuthorized "object" is as user but "user" must be "undefined" + + var roles = length !== req.flags.length; + + if (roles) { req.$flags += req.flags.slice(length).join(''); - if (typeof(isAuthorized) !== 'boolean') { + req.$roles = true; + } + + if (isAuthorized instanceof Error || isAuthorized instanceof ErrorBuilder) { + // Error handling + isAuthorized = false; + } else if (isAuthorized == null && user) { + // A callback error handling + isAuthorized = true; + } else if (user == null && isAuthorized && isAuthorized !== true) { user = isAuthorized; - isAuthorized = !user; + isAuthorized = true; } + req.isAuthorized = isAuthorized; - req.$total_authorize(isAuthorized, user, hasRoles); + req.$total_authorize(isAuthorized, user, roles); }); + } else { if (!req.$total_route) req.$total_route = F.lookup(req, req.buffer_exceeded ? '#431' : req.uri.pathname, req.flags, 0); @@ -14339,16 +17378,48 @@ function extend_request(PROTO) { req.$total_route = F.lookup(req, '#404', EMPTYARRAY, 0); var code = req.buffer_exceeded ? 431 : 404; if (!req.$total_schema || !req.$total_route) - req.$total_execute(code); + req.$total_execute(code, code); else req.$total_validate(req.$total_route, subscribe_validate_callback, code); } }; + + PROTO.snapshot = function(callback) { + + var req = this; + var builder = []; + var keys = Object.keys(req.headers); + var max = 0; + + for (var i = 0; i < keys.length; i++) { + var length = keys[i].length; + if (length > max) + max = length; + } + + builder.push('url'.padRight(max + 1) + ': ' + req.method.toUpperCase() + ' ' + req.url); + + for (var i = 0; i < keys.length; i++) + builder.push(keys[i].padRight(max + 1) + ': ' + req.headers[keys[i]]); + + builder.push(''); + + var data = []; + req.on('data', chunk => data.push(chunk)); + req.on('end', function() { + builder.push(Buffer.concat(data).toString('utf8')); + callback(null, builder.join('\n')); + }); + }; } function total_endmiddleware(req) { + + if (req.total_middleware) + req.total_middleware = null; + try { - req.$total_filemiddleware.execute.call(F, req, req.res, false); + req.$total_filemiddleware.execute(req, req.res, false); } catch (err) { F.error(err, req.$total_filemiddleware.controller + ' :: ' + req.$total_filemiddleware.name, req.uri); req.res.throw500(); @@ -14374,8 +17445,8 @@ function extend_response(PROTO) { if (self.headersSent || self.success) return; - var cookieHeaderStart = name + '='; - var builder = [cookieHeaderStart + value]; + var cookiename = name + '='; + var builder = [cookiename + value]; var type = typeof(expires); if (expires && !U.isDate(expires) && type === 'object') { @@ -14390,7 +17461,7 @@ function extend_response(PROTO) { options = {}; options.path = options.path || '/'; - expires && builder.push('Expires=' + expires.toUTCString()); + expires && builder.push('Expires=' + expires.toUTCString()); options.domain && builder.push('Domain=' + options.domain); options.path && builder.push('Path=' + options.path); options.secure && builder.push('Secure'); @@ -14398,11 +17469,32 @@ function extend_response(PROTO) { if (options.httpOnly || options.httponly || options.HttpOnly) builder.push('HttpOnly'); + var same = options.security || options.samesite || options.sameSite; + if (same) { + switch (same) { + case 1: + same = 'lax'; + break; + case 2: + same = 'strict'; + break; + } + builder.push('SameSite=' + same); + } + var arr = self.getHeader('set-cookie') || []; // Cookie, already, can be in array, resulting in duplicate 'set-cookie' header - var idx = arr.findIndex(cookieStr => cookieStr.startsWith(cookieHeaderStart)); - idx !== -1 && arr.splice(idx, 1); + if (arr.length) { + var l = cookiename.length; + for (var i = 0; i < arr.length; i++) { + if (arr[i].substring(0, l) === cookiename) { + arr.splice(i, 1); + break; + } + } + } + arr.push(builder.join('; ')); self.setHeader('Set-Cookie', arr); return self; @@ -14412,7 +17504,7 @@ function extend_response(PROTO) { * Disable HTTP cache for current response * @return {Response} */ - PROTO.noCache = function() { + PROTO.noCache = PROTO.nocache = function() { var self = this; if (self.$nocache) @@ -14515,7 +17607,7 @@ function extend_response(PROTO) { if (!accept && isGZIP(req)) accept = 'gzip'; - var compress = F.config['allow-gzip'] && accept.indexOf('gzip') !== -1; + var compress = CONF.allow_gzip && accept.indexOf('gzip') !== -1; if (isHEAD) { compress && (headers['Content-Encoding'] = 'gzip'); res.writeHead(200, headers); @@ -14529,7 +17621,7 @@ function extend_response(PROTO) { return res; } - var buffer = U.createBuffer(body); + var buffer = Buffer.from(body); Zlib.gzip(buffer, function(err, data) { if (err) { @@ -14567,6 +17659,7 @@ function extend_response(PROTO) { res.options.compress = compress === undefined || compress === true; res.options.body = body; res.options.type = type; + res.options.compress = body.length > 4096; headers && (res.options.headers = headers); res.$text(); return res; @@ -14601,6 +17694,61 @@ function extend_response(PROTO) { return this.$file(); }; + /** + * Responds with a file from FileStorage + * @param {String} name A name of FileStorage + * @param {String/Number} id + * @param {String} download Optional, a download name. + * @param {Object} headers Optional, additional headers. + * @param {Function} done Optional, callback. + * @return {Framework} + */ + PROTO.filefs = function(name, id, download, headers, callback, checkmeta) { + var self = this; + var options = {}; + options.id = id; + options.download = download; + options.headers = headers; + options.done = callback; + FILESTORAGE(name).res(self, options, checkmeta, $file_notmodified); + return self; + }; + + PROTO.filenosql = function(name, id, download, headers, callback, checkmeta) { + var self = this; + var options = {}; + options.id = id; + options.download = download; + options.headers = headers; + options.done = callback; + NOSQL(name).binary.res(self, options, checkmeta, $file_notmodified); + return self; + }; + + PROTO.imagefs = function(name, id, make, headers, callback, checkmeta) { + var self = this; + var options = {}; + options.id = id; + options.image = true; + options.make = make; + options.headers = headers; + options.done = callback; + FILESTORAGE(name).res(self, options, checkmeta, $file_notmodified); + return self; + }; + + PROTO.imagenosql = function(name, id, make, headers, callback, checkmeta) { + var self = this; + var options = {}; + options.id = id; + options.image = true; + options.make = make; + options.headers = headers; + options.done = callback; + NOSQL(name).binary.res(self, options, checkmeta, $file_notmodified); + return self; + }; + /** * Responds with a stream * @param {String} contentType @@ -14623,6 +17771,19 @@ function extend_response(PROTO) { }; PROTO.binary = function(body, type, encoding, download, headers) { + + if (typeof(encoding) === 'object') { + var tmp = encoding; + encoding = download; + download = headers; + headers = tmp; + } + + if (typeof(download) === 'object') { + headers = download; + download = headers; + } + this.options.type = type; this.options.body = body; this.options.encoding = encoding; @@ -14634,6 +17795,8 @@ function extend_response(PROTO) { PROTO.proxy = function(url, headers, timeout, callback) { + OBSOLETE('res.proxy()', 'You need to use controller.proxy()'); + var res = this; if (res.success || res.headersSent) @@ -14652,7 +17815,7 @@ function extend_response(PROTO) { var options = { protocol: uri.protocol, auth: uri.auth, method: 'GET', hostname: uri.hostname, port: uri.port, path: uri.path, agent: false, headers: headers }; var connection = options.protocol === 'https:' ? require('https') : http; - var gzip = F.config['allow-gzip'] && (res.req.headers['accept-encoding'] || '').lastIndexOf('gzip') !== -1; + var gzip = CONF.allow_gzip && (res.req.headers['accept-encoding'] || '').lastIndexOf('gzip') !== -1; var client = connection.get(options, function(response) { @@ -14690,10 +17853,10 @@ function extend_response(PROTO) { }); client.on('close', function() { - if (res.success) - return; - F.stats.response.pipe++; - response_end(res); + if (!res.success) { + F.stats.response.pipe++; + response_end(res); + } }); }); @@ -14708,12 +17871,15 @@ function extend_response(PROTO) { * @param {Function} callback Optional. * @return {Framework} */ - PROTO.image = function(filename, make, headers, callback) { + PROTO.image = function(filename, make, headers, callback, persistent) { var res = this; res.options.make = make; + if (persistent === true || (persistent == null && CONF.allow_persistent_images === true)) + res.options.persistent = true; + headers && (res.options.headers = headers); callback && (res.options.callback = callback); @@ -14739,11 +17905,16 @@ function extend_response(PROTO) { PROTO.json = function(obj) { var res = this; F.stats.response.json++; + if (obj && obj.$$schema) + obj = obj.$clean(); res.options.body = JSON.stringify(obj); + res.options.compress = res.options.body.length > 4096; res.options.type = CT_JSON; return res.$text(); }; + const SECURITYTXT = { '/security.txt': 1, '/.well-known/security.txt': 1 }; + PROTO.continue = function(callback) { var res = this; @@ -14754,40 +17925,62 @@ function extend_response(PROTO) { if (res.success || res.headersSent) return res; - if (!F.config['static-accepts'][req.extension]) { - res.throw404(); + if (!CONF.static_accepts[req.extension]) { + if (!F.routes.filesfallback || !F.routes.filesfallback(req, res)) + res.throw404(); return res; } + if (SECURITYTXT[req.url] && CONF['security.txt']) { + res.send(200, CONF['security.txt'], 'text/plain'); + return; + } + req.$key = createTemporaryKey(req); if (F.temporary.notfound[req.$key]) { - res.throw404(); + if (!F.routes.filesfallback || !F.routes.filesfallback(req, res)) + res.throw404(); return res; } - var name = req.uri.pathname; - var index = name.lastIndexOf('/'); - var resizer = F.routes.resize[name.substring(0, index + 1)]; - var canResize = false; + var canresize = false; var filename = null; + var name = req.uri.pathname; - if (resizer) { - name = name.substring(index + 1); - canResize = resizer.extension['*'] || resizer.extension[req.extension]; - if (canResize) { - name = resizer.path + $decodeURIComponent(name); - filename = F.onMapping(name, name, false, false); + if (IMAGES[req.extension]) { + var index = name.lastIndexOf('/'); + var resizer = F.routes.resize[name.substring(0, index + 1)]; + if (resizer) { + name = name.substring(index + 1); + canresize = resizer.extension['*'] || resizer.extension[req.extension]; + if (canresize) { + name = resizer.path + $decodeURIComponent(name); + filename = F.onMapping(name, name, false, false); + } else + filename = F.onMapping(name, name, true, true); } else filename = F.onMapping(name, name, true, true); } else filename = F.onMapping(name, name, true, true); - if (!canResize) { + if (!filename) { + if (!F.routes.filesfallback || !F.routes.filesfallback(req, res)) + res.throw404(); + return; + } + + if (!canresize) { - if (F.components.has && F.components[req.extension] && req.uri.pathname === F.config['static-url-components'] + req.extension) { + if (F.components.has && F.components[req.extension] && req.uri.pathname === CONF.static_url_components + req.extension) { res.noCompress = true; - filename = F.path.temp('components' + (req.query.group ? '_g' + req.query.group : '') + '.' + req.extension); + res.options.components = true; + var g = req.query.group ? req.query.group.substring(0, req.query.group.length - 6) : ''; + filename = F.path.temp('components' + (g ? '_g' + g : '') + '.' + req.extension); + if (g) + req.$key = 'components_' + g + '.' + req.extension; + else + req.$key = 'components.' + req.extension; } res.options.filename = filename; @@ -14826,7 +18019,8 @@ function extend_response(PROTO) { var contentType = response.headers['content-type']; if (response.statusCode !== 200 || !contentType || !contentType.startsWith('image/')) { - res.throw404(); + if (!F.routes.filesfallback || !F.routes.filesfallback(req, res)) + res.throw404(); return; } @@ -14856,16 +18050,32 @@ function extend_response(PROTO) { var req = this.req; + // Localization + if (CONF.allow_localize && KEYSLOCALIZE[req.extension]) { + + // Is package? + if (options.filename && options.filename[0] === '@') + options.filename = F.path.package(options.filename.substring(1)); + + F.$filelocalize(req, res, false, options); + return; + } + !req.$key && (req.$key = createTemporaryKey(req)); - if (F.temporary.notfound[req.$key]) { - DEBUG && (F.temporary.notfound[req.$key] = undefined); - res.throw404(); - return res; + // "$keyskip" solves a problem with handling files in 404 state + if (!req.$keyskip) { + if (F.temporary.notfound[req.$key]) { + req.$keyskip = true; + DEBUG && (F.temporary.notfound[req.$key] = undefined); + if (!F.routes.filesfallback || !F.routes.filesfallback(req, res)) + res.throw404(); + return res; + } } // Is package? - if (options.filename[0] === '@') + if (options.filename && options.filename[0] === '@') options.filename = F.path.package(options.filename.substring(1)); var name = F.temporary.path[req.$key]; @@ -14889,7 +18099,7 @@ function extend_response(PROTO) { if (name === undefined) { if (F.temporary.processing[req.$key]) { - if (req.processing > F.config['default-request-timeout']) { + if (req.processing > CONF.default_request_timeout) { res.throw408(); } else { req.processing += 500; @@ -14910,9 +18120,9 @@ function extend_response(PROTO) { !accept && isGZIP(req) && (accept = 'gzip'); - var compress = F.config['allow-gzip'] && COMPRESSION[contentType] && accept.indexOf('gzip') !== -1 && name.length > 2; + var compress = CONF.allow_gzip && COMPRESSION[contentType] && accept.indexOf('gzip') !== -1 && name.length > 2; var range = req.headers.range; - var canCache = !res.$nocache && RELEASE && contentType !== 'text/cache-manifest'; + var canCache = !res.$nocache && RELEASE && contentType !== 'text/cache-manifest' && !RESPONSENOCACHE[req.extension]; if (canCache) { if (compress) @@ -14951,24 +18161,29 @@ function extend_response(PROTO) { if (res.getHeader('Last-Modified')) delete headers['Last-Modified']; - else + else if (!res.options.lastmodified) headers['Last-Modified'] = name[2]; - headers.Etag = ETAG + F.config['etag-version']; + headers.Etag = ETAG + CONF.etag_version; if (range) { $file_range(name[0], range, headers, res); return res; } - (DEBUG || res.$nocache) && F.isProcessed(req.$key) && (F.temporary.path[req.$key] = undefined); + // (DEBUG && !res.options.make) --> because of image convertor + if (!res.options.components && ((DEBUG && !res.options.make) || res.$nocache)) + F.isProcessed(req.$key) && (F.temporary.path[req.$key] = undefined); if (name[1] && !compress) headers[HEADER_LENGTH] = name[1]; + else if (compress && name[4]) + headers[HEADER_LENGTH] = name[4]; else if (headers[HEADER_LENGTH]) delete headers[HEADER_LENGTH]; F.stats.response.file++; + options.stream && DESTROY(options.stream); if (req.method === 'HEAD') { res.writeHead(res.options.code || 200, headers); @@ -15033,7 +18248,7 @@ function extend_response(PROTO) { var accept = req.headers['accept-encoding'] || ''; !accept && isGZIP(req) && (accept = 'gzip'); - var compress = F.config['allow-gzip'] && COMPRESSION[options.type] && accept.indexOf('gzip') !== -1; + var compress = CONF.allow_gzip && COMPRESSION[options.type] && accept.indexOf('gzip') !== -1; var headers = compress ? HEADERS.binary_compress : HEADERS.binary; headers['Vary'] = 'Accept-Encoding' + (req.$mobile ? ', User-Agent' : ''); @@ -15092,7 +18307,7 @@ function extend_response(PROTO) { var accept = req.headers['accept-encoding'] || ''; !accept && isGZIP(req) && (accept = 'gzip'); - var compress = (options.compress === undefined || options.compress) && F.config['allow-gzip'] && COMPRESSION[options.type] && accept.indexOf('gzip') !== -1; + var compress = (options.compress === undefined || options.compress) && CONF.allow_gzip && COMPRESSION[options.type] && accept.indexOf('gzip') !== -1; var headers; if (RELEASE) { @@ -15125,11 +18340,11 @@ function extend_response(PROTO) { headers = U.extend_headers(headers, options.headers); F.stats.response.stream++; - F.reqstats(false, req.isStaticFile); if (req.method === 'HEAD') { res.writeHead(options.code || 200, headers); res.end(); + options.stream && framework_internal.onFinished(res, () => framework_internal.destroyStream(options.stream)); response_end(res); return res; } @@ -15140,13 +18355,13 @@ function extend_response(PROTO) { options.stream.pipe(Zlib.createGzip(GZIPSTREAM)).pipe(res); framework_internal.onFinished(res, () => framework_internal.destroyStream(options.stream)); response_end(res); - return res; - } + } else { + res.writeHead(options.code || 200, headers); + framework_internal.onFinished(res, () => framework_internal.destroyStream(options.stream)); + options.stream.pipe(res); + response_end(res); + } - res.writeHead(options.code || 200, headers); - framework_internal.onFinished(res, () => framework_internal.destroyStream(options.stream)); - options.stream.pipe(res); - response_end(res); return res; }; @@ -15160,6 +18375,7 @@ function extend_response(PROTO) { // res.options.cache // res.options.headers // res.options.make = function(image, res) + // res.options.persistent var res = this; var options = res.options; @@ -15168,17 +18384,15 @@ function extend_response(PROTO) { return $image_nocache(res); var req = this.req; - !req.$key && (req.$key = createTemporaryKey(req)); + if (!req.$key) + req.$key = createTemporaryKey(req); - if (F.temporary.notfound[req.$key]) { - DEBUG && (F.temporary.notfound[req.$key] = undefined); - res.throw404(); - return res; - } + var key = req.$key; - var key = req.$key || createTemporaryKey(req); if (F.temporary.notfound[key]) { - res.throw404(); + DEBUG && (F.temporary.notfound[key] = undefined); + if (!F.routes.filesfallback || !F.routes.filesfallback(req, res)) + res.throw404(); return res; } @@ -15192,8 +18406,8 @@ function extend_response(PROTO) { return res; } - if (F.temporary.processing[req.$key]) { - if (req.processing > F.config['default-request-timeout']) { + if (F.temporary.processing[key]) { + if (req.processing > CONF.default_request_timeout) { res.throw408(); } else { req.processing += 500; @@ -15204,7 +18418,13 @@ function extend_response(PROTO) { var plus = F.id ? 'i-' + F.id + '_' : ''; - options.name = F.path.temp(plus + key); + options.name = F.path.temp((options.persistent ? 'timg_' : '') + plus + key); + + if (options.persistent) { + fsFileExists(options.name, $image_persistent, res); + return; + } + F.temporary.processing[key] = true; if (options.stream) @@ -15238,10 +18458,16 @@ function extend_response(PROTO) { if (res.headersSent) return res; + if (res.$evalroutecallback) { + res.headersSent = true; + res.$evalroutecallback(null, options.body, res.options.encoding || ENCODING); + return res; + } + var accept = req.headers['accept-encoding'] || ''; !accept && isGZIP(req) && (accept = 'gzip'); - var gzip = F.config['allow-gzip'] && (options.compress === undefined || options.compress) ? accept.indexOf('gzip') !== -1 : false; + var gzip = CONF.allow_gzip && (options.compress === undefined || options.compress) ? accept.indexOf('gzip') !== -1 : false; var headers; if (req.$mobile) @@ -15263,7 +18489,7 @@ function extend_response(PROTO) { } else { if (gzip) { res.writeHead(options.code || 200, headers); - Zlib.gzip(options.body instanceof Buffer ? options.body : U.createBuffer(options.body), (err, data) => res.end(data, res.options.encoding || ENCODING)); + Zlib.gzip(options.body instanceof Buffer ? options.body : Buffer.from(options.body), (err, data) => res.end(data, res.options.encoding || ENCODING)); } else { res.writeHead(options.code || 200, headers); res.end(options.body, res.options.encoding || ENCODING); @@ -15341,22 +18567,41 @@ function extend_response(PROTO) { return res; var req = res.req; + var key = 'error' + res.options.code; + res.options.problem && F.problem(res.options.problem, 'response' + res.options.code + '()', req.uri, req.ip); - res.writeHead(res.options.code || 501, res.options.headers || HEADERS.responseCode); - if (req.method === 'HEAD') + if (req.method === 'HEAD') { + res.writeHead(res.options.code || 501, res.options.headers || HEADERS.responseCode); res.end(); - else - res.end(res.options.body || U.httpStatus(res.options.code) + prepare_error(res.options && res.options.problem)); + F.stats.response[key]++; + response_end(res); + } else { + req.$total_route = F.lookup(req, '#' + res.options.code, EMPTYARRAY, 0); + req.$total_exception = res.options.problem; + req.$total_execute(res.options.code, true); + } - var key = 'error' + res.options.code; - F.$events[key] && F.emit(key, req, res, res.options.problem); - F.stats.response[key]++; - response_end(res); + F.$events[key] && EMIT(key, req, res, res.options.problem); return res; }; } +function $image_persistent(exists, size, isFile, stats, res) { + if (exists) { + delete F.temporary.processing[res.req.$key]; + F.temporary.path[res.req.$key] = [res.options.name, stats.size, stats.mtime.toUTCString()]; + res.options.filename = res.options.name; + res.$file(); + } else { + F.temporary.processing[res.req.$key] = true; + if (res.options.stream) + fsFileExists(res.options.name, $image_stream, res); + else + fsFileExists(res.options.filename, $image_filename, res); + } +} + function $continue_timeout(res) { res.continue(); } @@ -15372,7 +18617,7 @@ function $file_notmodified(res, name) { if (res.getHeader('Last-Modified')) delete headers['Last-Modified']; else - headers['Last-Modified'] = name[2]; + headers['Last-Modified'] = name instanceof Array ? name[2] : name; if (res.getHeader('Expires')) delete headers.Expires; @@ -15382,7 +18627,7 @@ function $file_notmodified(res, name) { if (res.getHeader('ETag')) delete headers.Etag; else - headers.Etag = ETAG + F.config['etag-version']; + headers.Etag = ETAG + CONF.etag_version; headers[HEADER_TYPE] = U.getContentType(req.extension); res.writeHead(304, headers); @@ -15392,12 +18637,14 @@ function $file_notmodified(res, name) { } function $file_nocompress(stream, next, res) { + stream.pipe(res); framework_internal.onFinished(res, function() { next(); framework_internal.destroyStream(stream); }); + response_end(res); } @@ -15461,7 +18708,7 @@ function $image_nocache(res) { // STREAM if (options.stream) { - var image = framework_image.load(options.stream, IMAGEMAGICK); + var image = framework_image.load(options.stream); options.make.call(image, image, res); options.type = U.getContentType(image.outputType); options.stream = image; @@ -15475,7 +18722,7 @@ function $image_nocache(res) { if (e) { F.path.verify('temp'); - var image = framework_image.load(options.filename, IMAGEMAGICK); + var image = framework_image.load(options.filename); options.make.call(image, image, res); F.stats.response.image++; options.type = U.getContentType(image.outputType); @@ -15483,7 +18730,8 @@ function $image_nocache(res) { res.$stream(); } else { options.headers = null; - res.throw404(); + if (!F.routes.filesfallback || !F.routes.filesfallback(res.req, res)) + res.throw404(); } }); } @@ -15501,7 +18749,13 @@ function $image_stream(exists, size, isFile, stats, res) { delete F.temporary.processing[req.$key]; F.temporary.path[req.$key] = [options.name, stats.size, stats.mtime.toUTCString()]; res.options.filename = options.name; - res.options.stream = null; + + if (options.stream) { + options.stream.once('error', NOOP); // sometimes is throwed: Bad description + DESTROY(options.stream); + options.stream = null; + } + res.$file(); DEBUG && (F.temporary.path[req.$key] = undefined); return; @@ -15509,7 +18763,7 @@ function $image_stream(exists, size, isFile, stats, res) { F.path.verify('temp'); - var image = framework_image.load(options.stream, IMAGEMAGICK); + var image = framework_image.load(options.stream); options.make.call(image, image, res); req.extension = U.getExtension(options.name); @@ -15523,6 +18777,13 @@ function $image_stream(exists, size, isFile, stats, res) { F.stats.response.image++; image.save(options.name, function(err) { + + if (options.stream) { + options.stream.once('error', NOOP); // sometimes is throwed: Bad description + DESTROY(options.stream); + options.stream = null; + } + delete F.temporary.processing[req.$key]; if (err) { F.temporary.notfound[req.$key] = true; @@ -15545,14 +18806,15 @@ function $image_filename(exists, size, isFile, stats, res) { if (!exists) { delete F.temporary.processing[req.$key]; F.temporary.notfound[req.$key] = true; - res.throw404(); + if (!F.routes.filesfallback || !F.routes.filesfallback(req, res)) + res.throw404(); DEBUG && (F.temporary.notfound[req.$key] = undefined); return; } F.path.verify('temp'); - var image = framework_image.load(options.filename, IMAGEMAGICK); + var image = framework_image.load(options.filename); options.make.call(image, image, res); req.extension = U.getExtension(options.name); @@ -15565,6 +18827,7 @@ function $image_filename(exists, size, isFile, stats, res) { } F.stats.response.image++; + image.save(options.name, function(err) { delete F.temporary.processing[req.$key]; @@ -15583,12 +18846,31 @@ function $image_filename(exists, size, isFile, stats, res) { } function response_end(res) { + F.reqstats(false, res.req.isStaticFile); res.success = true; - !res.req.isStaticFile && F.$events['request-end'] && F.emit('request-end', res.req, res); + + if (CONF.allow_reqlimit && F.temporary.ddos[res.req.ip]) + F.temporary.ddos[res.req.ip]--; + + if (!res.req.isStaticFile) { + F.$events['request-end'] && EMIT('request-end', res.req, res); + F.$events.request_end && EMIT('request_end', res.req, res); + } + res.req.clear(true); res.controller && res.req.$total_success(); - res.options.callback && res.options.callback(); + + if (res.options.callback) { + res.options.callback(); + res.options.callback = null; + } + + if (res.options.done) { + res.options.done(); + res.options.done = null; + } + // res.options = EMPTYOBJECT; res.controller = null; } @@ -15604,6 +18886,11 @@ function $decodeURIComponent(value) { } global.Controller = Controller; +global.WebSocketClient = WebSocketClient; + +process.on('unhandledRejection', function(e) { + F.error(e, '', null); +}); process.on('uncaughtException', function(e) { @@ -15612,16 +18899,17 @@ process.on('uncaughtException', function(e) { if (err.indexOf('listen EADDRINUSE') !== -1) { process.send && process.send('total:eaddrinuse'); console.log('\nThe IP address and the PORT is already in use.\nYou must change the PORT\'s number or IP address.\n'); - process.exit('SIGTERM'); + process.exit(1); return; - } else if (F.config['allow-filter-errors'] && REG_SKIPERROR.test(err)) + } else if (CONF.allow_filter_errors && REG_SKIPERROR.test(err)) return; F.error(e, '', null); }); function fsFileRead(filename, callback, a, b, c) { - U.queue('F.files', F.config['default-maximum-file-descriptors'], function(next) { + U.queue('F.files', CONF.default_maxopenfiles, function(next) { + F.stats.performance.open++; Fs.readFile(filename, function(err, result) { next(); callback(err, result, a, b, c); @@ -15630,7 +18918,8 @@ function fsFileRead(filename, callback, a, b, c) { } function fsFileExists(filename, callback, a, b, c) { - U.queue('F.files', F.config['default-maximum-file-descriptors'], function(next) { + U.queue('F.files', CONF.default_maxopenfiles, function(next) { + F.stats.performance.open++; Fs.lstat(filename, function(err, stats) { next(); callback(!err && stats.isFile(), stats ? stats.size : 0, stats ? stats.isFile() : false, stats, a, b, c); @@ -15638,7 +18927,7 @@ function fsFileExists(filename, callback, a, b, c) { }); } -function fsStreamRead(filename, options, callback, req, res) { +function fsStreamRead(filename, options, callback, res) { if (!callback) { callback = options; @@ -15659,10 +18948,11 @@ function fsStreamRead(filename, options, callback, req, res) { } else opt = HEADERS.fsStreamRead; - U.queue('F.files', F.config['default-maximum-file-descriptors'], function(next) { + U.queue('F.files', CONF.default_maxopenfiles, function(next) { + F.stats.performance.open++; var stream = Fs.createReadStream(filename, opt); stream.on('error', NOOP); - callback(stream, next, req, res); + callback(stream, next, res); }, filename); } @@ -15672,12 +18962,87 @@ function fsStreamRead(filename, options, callback, req, res) { * @return {String} */ function createTemporaryKey(req) { - return (req.uri ? req.uri.pathname : req).replace(REG_TEMPORARY, '-').substring(1); + return (req.uri ? req.uri.pathname : req).replace(REG_TEMPORARY, '_').substring(1); } -process.on('SIGTERM', () => F.stop()); -process.on('SIGINT', () => F.stop()); -process.on('exit', () => F.stop()); +F.createTemporaryKey = createTemporaryKey; + +function MiddlewareOptions() {} + +MiddlewareOptions.prototype = { + + get user() { + return this.req.user; + }, + + get session() { + return this.req.session; + }, + + get language() { + return this.req.$language; + }, + + get ip() { + return this.req.ip; + }, + + get headers() { + return this.req.headers; + }, + + get ua() { + return this.req ? this.req.ua : null; + }, + + get sessionid() { + return this.req.sessionid; + }, + + get id() { + return this.controller ? this.controller.id : null; + }, + + get params() { + return this.controller ? this.controller.params : null; + }, + + get files() { + return this.req.files; + }, + + get body() { + return this.req.body; + }, + + get query() { + return this.req.query; + } +}; + +const MiddlewareOptionsProto = MiddlewareOptions.prototype; + +MiddlewareOptionsProto.callback = function() { + this.next(); + return this; +}; + +MiddlewareOptionsProto.cancel = function() { + this.next(false); + return this; +}; + +function forcestop() { + F.stop(); +} + +process.on('SIGTERM', forcestop); +process.on('SIGINT', forcestop); +process.on('exit', forcestop); + +function process_ping() { + process.connected && process.send('total:ping'); +} process.on('message', function(msg, h) { if (msg === 'total:debug') { @@ -15687,39 +19052,131 @@ process.on('message', function(msg, h) { }, 10000, 500); } else if (msg === 'reconnect') F.reconnect(); + else if (msg === 'total:ping') + setImmediate(process_ping); + else if (msg === 'total:update') + EMIT('update'); else if (msg === 'reset') F.cache.clear(); else if (msg === 'stop' || msg === 'exit' || msg === 'kill') F.stop(); - else if (msg && msg.TYPE && msg.id !== F.id) { - msg.TYPE === 'cache-set' && F.cache.set(msg.key, msg.value, msg.expire, false); - msg.TYPE === 'cache-remove' && F.cache.remove(msg.key, false); - msg.TYPE === 'cache-remove-all' && F.cache.removeAll(msg.key, false); - msg.TYPE === 'cache-clear' && F.cache.clear(false); - msg.TYPE === 'nosql-lock' && F.databases[msg.name] && F.databases[msg.name].lock(); - msg.TYPE === 'nosql-unlock' && F.databases[msg.name] && F.databases[msg.name].unlock(); - msg.TYPE === 'nosql-meta' && F.databases[msg.name] && F.databases[msg.name].$meta(); - msg.TYPE === 'nosql-counter-lock' && F.databases[msg.name] && (F.databases[msg.name].counter.locked = true); - msg.TYPE === 'nosql-counter-unlock' && F.databases[msg.name] && (F.databases[msg.name].counter.locked = false); - msg.TYPE === 'req' && F.cluster.req(msg); - msg.TYPE === 'res' && msg.target === F.id && F.cluster.res(msg); - msg.TYPE === 'emit' && F.$events[msg.name] && F.emit(msg.name, msg.data); - } - F.$events.message && F.emit('message', msg, h); + else if (msg && msg.TYPE && msg.ID !== F.id) { + if (msg.TYPE === 'req') + F.cluster.req(msg); + else if (msg.TYPE === 'res') + msg.target === F.id && F.cluster.res(msg); + else if (msg.TYPE === 'emit') + F.$events[msg.name] && EMIT(msg.name, msg.a, msg.b, msg.c, msg.d, msg.e); + else if (msg.TYPE === 'nosql-meta') + NOSQL(msg.name).meta(msg.key, msg.value, true); + else if (msg.TYPE === 'table-meta') + TABLE(msg.name).meta(msg.key, msg.value, true); + else if (msg.TYPE === 'session') { + var session = SESSION(msg.NAME); + switch (msg.method) { + case 'remove': + session.$sync = false; + session.remove(msg.sessionid); + session.$sync = true; + break; + case 'remove2': + session.$sync = false; + session.remove2(msg.id); + session.$sync = true; + break; + case 'set2': + session.$sync = false; + session.set2(msg.id, msg.data, msg.expire, msg.note, msg.settings); + session.$sync = true; + break; + case 'set': + session.$sync = false; + session.set(msg.sessionid, msg.id, msg.data, msg.expire, msg.note, msg.settings); + session.$sync = true; + break; + case 'update2': + session.$sync = false; + session.update2(msg.id, msg.data, msg.expire, msg.note, msg.settings); + session.$sync = true; + break; + case 'update': + session.$sync = false; + session.update(msg.sessionid, msg.data, msg.expire, msg.note, msg.settings); + session.$sync = true; + break; + case 'clear': + session.$sync = false; + session.clear(msg.lastusage); + session.$sync = true; + break; + case 'clean': + session.$sync = false; + session.clean(); + session.$sync = true; + break; + } + } else if (msg.TYPE === 'cache') { + switch (msg.method) { + case 'set': + F.cache.$sync = false; + F.cache.set(msg.name, msg.value, msg.expire); + F.cache.$sync = true; + break; + case 'remove': + F.cache.$sync = false; + F.cache.remove(msg.name); + F.cache.$sync = true; + break; + case 'clear': + F.cache.$sync = false; + F.cache.clear(); + F.cache.$sync = true; + break; + case 'removeAll': + F.cache.$sync = false; + F.cache.removeAll(msg.search); + F.cache.$sync = true; + break; + } + } else if (msg.TYPE === 'filestorage') { + var fs = F.databases['storage_' + msg.NAME]; + if (fs) { + switch (msg.method) { + case 'add': + fs.meta.index = msg.index; + fs.meta.count = msg.count; + if (F.id === '0') + fs.$save(); + break; + case 'remove': + fs.meta.count = msg.count; + if (F.id === '0' && msg.id) { + fs.meta.free.push(msg.id); + fs.$save(); + } + break; + case 'refresh': + fs.$refresh(); + break; + } + } + } + } + + F.$events.message && EMIT('message', msg, h); }); function prepare_error(e) { if (!e) return ''; else if (e instanceof ErrorBuilder) - return ' :: ' + e.plain(); - else if (e.stack) - return RELEASE ? '' : e.stack; - return RELEASE ? '' : ' :: ' + e.toString(); + return e.plain(); + else if (DEBUG) + return e.stack ? e.stack : e.toString(); } function prepare_filename(name) { - return name[0] === '@' ? (F.isWindows ? U.combine(F.config['directory-temp'], name.substring(1)) : F.path.package(name.substring(1))) : U.combine('/', name); + return name[0] === '@' ? (F.isWindows ? U.combine(CONF.directory_temp, name.substring(1)) : F.path.package(name.substring(1))) : U.combine('/', name); } function prepare_staticurl(url, isDirectory) { @@ -15756,9 +19213,13 @@ function existsSync(filename, file) { } } +function getLoggerMiddleware(name) { + return 'MIDDLEWARE("' + name + '")'; +} + function async_middleware(index, req, res, middleware, callback, options, controller) { - if (res.success || res.headersSent) { + if (res.success || res.headersSent || res.finished) { req.$total_route && req.$total_success(); callback = null; return; @@ -15774,24 +19235,60 @@ function async_middleware(index, req, res, middleware, callback, options, contro return async_middleware(index, req, res, middleware, callback, options, controller); } - var output = item.call(framework, req, res, function(err) { - - if (err === false) { - req.$total_route && req.$total_success(); - callback = null; - return; + var output; + var $now; + + if (CONF.logger) + $now = Date.now(); + + if (item.$newversion) { + var opt = req.$total_middleware; + if (!index || !opt) { + opt = req.$total_middleware = new MiddlewareOptions(); + opt.req = req; + opt.res = res; + opt.middleware = middleware; + opt.options = options || EMPTYOBJECT; + opt.controller = controller; + opt.callback2 = callback; + opt.next = function(err) { + CONF.logger && F.ilogger(getLoggerMiddleware(name), req, $now); + var mid = req.$total_middleware; + if (err === false) { + req.$total_route && req.$total_success(); + req.$total_middleware = null; + callback = null; + } else if (err instanceof Error || err instanceof ErrorBuilder) { + res.throw500(err); + req.$total_middleware = null; + callback = null; + } else + async_middleware(mid.index, mid.req, mid.res, mid.middleware, mid.callback2, mid.options, mid.controller); + }; } - if (err instanceof Error || err instanceof ErrorBuilder) { - res.throw500(err); - callback = null; - return; - } + opt.index = index; + output = item(opt); - async_middleware(index, req, res, middleware, callback, options, controller); - }, options, controller); + } else { + output = item.call(framework, req, res, function(err) { + CONF.logger && F.ilogger(getLoggerMiddleware(name), req, $now); + if (err === false) { + req.$total_route && req.$total_success(); + callback = null; + } else if (err instanceof Error || err instanceof ErrorBuilder) { + res.throw500(err); + callback = null; + } else + async_middleware(index, req, res, middleware, callback, options, controller); + }, options, controller); + } - if (output !== false) + if (res.headersSent || res.finished) { + req.$total_route && req.$total_success(); + callback = null; + return; + } else if (output !== false) return; req.$total_route && req.$total_success(); @@ -15800,20 +19297,35 @@ function async_middleware(index, req, res, middleware, callback, options, contro global.setTimeout2 = function(name, fn, timeout, limit, param) { var key = ':' + name; + var internal = F.temporary.internal; + if (limit > 0) { - var key2 = key + ':limit'; - if (F.temporary.internal[key2] >= limit) + + var key2 = key + '_limit'; + var key3 = key + '_fn'; + + if (internal[key2] >= limit) { + internal[key] && clearTimeout(internal[key]); + internal[key] = internal[key2] = internal[key3] = undefined; + fn(); return; - F.temporary.internal[key2] = (F.temporary.internal[key2] || 0) + 1; - F.temporary.internal[key] && clearTimeout(F.temporary.internal[key]); - return F.temporary.internal[key] = setTimeout(function(param) { - F.temporary.internal[key2] = undefined; + } + + internal[key] && clearTimeout(internal[key]); + internal[key2] = (internal[key2] || 0) + 1; + + return internal[key] = setTimeout(function(param, key) { + F.temporary.internal[key] = F.temporary.internal[key + '_limit'] = F.temporary.internal[key + '_fn'] = undefined; fn && fn(param); - }, timeout, param); + }, timeout, param, key); } - F.temporary.internal[key] && clearTimeout(F.temporary.internal[key]); - return F.temporary.internal[key] = setTimeout(fn, timeout, param); + if (internal[key]) { + clearTimeout(internal[key]); + internal[key] = undefined; + } + + return internal[key] = setTimeout(fn, timeout, param); }; global.clearTimeout2 = function(name) { @@ -15830,19 +19342,74 @@ global.clearTimeout2 = function(name) { }; function parseComponent(body, filename) { - var response = {}; + var response = {}; response.css = ''; response.js = ''; response.install = ''; + response.files = {}; + response.parts = {}; var beg = 0; var end = 0; + var comname = U.getName(filename); + // Files while (true) { - beg = body.indexOf('', beg); if (end === -1) break; @@ -15885,32 +19452,358 @@ function parseComponent(body, filename) { return response; } +function getSchemaName(schema, params) { + if (!(schema instanceof Array)) + schema = schema.split('/'); + return schema[0] === 'default' ? (params ? params[schema[1]] : schema[1]) : (schema.length > 1 ? (schema[0] + '/' + schema[1]) : schema[0]); +} + // Default action for workflow routing function controller_json_workflow(id) { var self = this; - self.id = id; - self.$exec(self.route.workflow, null, self.callback()); + var w = self.route.workflow; + + self.id = self.route.paramidindex === -1 ? id : self.req.split[self.route.paramidindex]; + + CONF.logger && (self.req.$logger = []); + + if (w instanceof Object) { + + if (!w.type) { + + // IS IT AN OPERATION? + if (!self.route.schema.length) { + OPERATION(w.id, self.body, w.view ? self.callback(w.view) : self.callback(), self); + return; + } + + var schema = self.route.isDYNAMICSCHEMA ? framework_builders.findschema(self.req.$schemaname || (self.route.schema[0] + '/' + self.params[self.route.schema[1]])) : GETSCHEMA(self.route.schema[0], self.route.schema[1]); + if (!schema) { + var err = 'Schema "{0}" not found.'.format(getSchemaName(self.route.schema, self.route.isDYNAMICSCHEMA ? self.params : null)); + if (self.route.isDYNAMICSCHEMA) + self.throw404(err); + else + self.throw500(err); + return; + } + + if (schema.meta[w.id] !== undefined) { + w.type = '$' + w.id; + } else if (schema.meta['workflow#' + w.id] !== undefined) { + w.type = '$workflow'; + w.name = w.id; + } else if (schema.meta['transform#' + w.id] !== undefined) { + w.type = '$transform'; + w.name = w.id; + } else if (schema.meta['operation#' + w.id] !== undefined) { + w.type = '$operation'; + w.name = w.id; + } else if (schema.meta['hook#' + w.id] !== undefined) { + w.type = '$hook'; + w.name = w.id; + } + } + + if (w.name) + self[w.type](w.name, self.callback(w.view)); + else { + + if (w.type) + self[w.type](self.callback(w.view)); + else { + var err = 'Schema "{0}" does not contain "{1}" operation.'.format(schema.name, w.id); + if (self.route.isDYNAMICSCHEMA) + self.throw404(err); + else + self.throw500(err); + } + } + + if (self.route.isDYNAMICSCHEMA) + w.type = ''; + + } else + self.$exec(w, null, self.callback(w.view)); } -// Parses schema group and schema name from string e.g. "User" or "Company/User" -function parseSchema(name) { - var schema = F.temporary.internal['$$$' + name]; - if (schema) - return schema; +// Default action for workflow routing +function controller_json_workflow_multiple(id) { + + var self = this; + var w = self.route.workflow; + + self.id = self.route.paramidindex === -1 ? id : self.req.split[self.route.paramidindex]; + CONF.logger && (self.req.$logger = []); + + if (w instanceof Object) { + if (!w.type) { + + // IS IT AN OPERATION? + if (!self.route.schema.length) { + RUN(w.id, self.body, w.view ? self.callback(w.view) : self.callback(), null, self, w.index != null ? w.id[w.index] : null); + return; + } + + var schema = self.route.isDYNAMICSCHEMA ? framework_builders.findschema(self.route.schema[0] + '/' + self.params[self.route.schema[1]]) : GETSCHEMA(self.route.schema[0], self.route.schema[1]); + if (!schema) { + self.throw500('Schema "{0}" not found.'.format(getSchemaName(self.route.schema, self.isDYNAMICSCHEMA ? self.params : null))); + return; + } + + var op = []; + for (var i = 0; i < w.id.length; i++) { + var id = w.id[i]; + if (schema.meta[id] !== undefined) { + op.push({ name: '$' + id }); + } else if (schema.meta['workflow#' + id] !== undefined) { + op.push({ name: '$workflow', id: id }); + } else if (schema.meta['transform#' + id] !== undefined) { + op.push({ name: '$transform', id: id }); + } else if (schema.meta['operation#' + id] !== undefined) { + op.push({ name: '$operation', id: id }); + } else if (schema.meta['hook#' + id] !== undefined) { + op.push({ name: '$hook', id: id }); + } else { + // not found + self.throw500('Schema "{0}" does not contain "{1}" operation.'.format(schema.name, id)); + return; + } + } + w.async = op; + } + + var async = self.$async(self.callback(w.view), w.index); + for (var i = 0; i < w.async.length; i++) { + var a = w.async[i]; + if (a.id) + async[a.name](a.id); + else + async[a.name](); + } + } else + self.$exec(w, null, self.callback(w.view)); +} + +function ilogger(body) { + F.path.verify('logs'); + U.queue('F.ilogger', 5, (next) => Fs.appendFile(U.combine(CONF.directory_logs, 'logger.log'), body, next)); +} + +F.ilogger = function(name, req, ts) { - schema = name.split('/'); + if (req && req instanceof Controller) + req = req.req; - if (!schema[1]) { - schema[1] = schema[0]; - schema[0] = 'default'; + var isc = CONF.logger === 'console'; + var divider = ''; + + for (var i = 0; i < (isc ? 64 : 220); i++) + divider += '-'; + + var msg; + + if (req && !name && req.$logger && req.$logger.length) { + + msg = req.method + ' ' + req.url; + + req.$logger.unshift(msg); + req.$logger.push(divider); + + if (isc) + console.log(req.$logger.join('\n')); + else { + req.$logger.push(''); + ilogger(req.$logger.join('\n')); + } + + req.$logger = null; + return; } - F.temporary.internal['$$$' + name] = schema; - return schema; + if (!name) + return; + + var dt = new Date(); + + msg = dt.format('yyyy-MM-dd HH:mm:ss') + ' | ' + name.padRight(40, ' ') + ' | ' + (((dt.getTime() - ts) / 1000).format(3) + ' sec.').padRight(12) + ' | ' + (req ? (req.method + ' ' + req.url).max(70) : '').padRight(70); + + if (isc) { + if (req && req.$logger) + req.$logger.push(msg); + else + console.log(msg + '\n' + divider); + } else { + msg = msg + ' | ' + (req ? (req.ip || '') : '').padRight(20) + ' | ' + (req && req.headers ? (req.headers['user-agent'] || '') : ''); + if (req && req.$logger) + req.$logger.push(msg); + else + ilogger(msg + '\n' + divider + '\n'); + } +}; + +function evalroutehandleraction(controller) { + if (controller.route.isPARAM) + controller.route.execute.apply(controller, framework_internal.routeParam(controller.req.split, controller.route)); + else + controller.route.execute.call(controller); +} + +function evalroutehandler(controller) { + if (!controller.route.schema || !controller.route.schema[1] || controller.req.method === 'DELETE' || controller.req.method === 'GET') + return evalroutehandleraction(controller); + + F.onSchema(controller.req, controller.route, function(err, body) { + if (err) { + controller.$evalroutecallback(err, body); + } else { + controller.body = body; + evalroutehandleraction(controller); + } + }); +} + +global.ACTION = function(url, data, callback) { + + if (typeof(data) === 'function') { + callback = data; + data = null; + } + + var index = url.indexOf(' '); + var method = url.substring(0, index); + var params = ''; + var route; + + url = url.substring(index + 1); + index = url.indexOf('?'); + + if (index !== -1) { + params = url.substring(index + 1); + url = url.substring(0, index); + } + + url = url.trim(); + var routeurl = url; + + if (routeurl.endsWith('/')) + routeurl = routeurl.substring(0, routeurl.length - 1); + + var req = {}; + var res = {}; + + req.res = res; + req.$protocol = 'http'; + req.url = url; + req.ip = F.ip || '127.0.0.1'; + req.host = req.ip + ':' + (F.port || 8000); + req.headers = { 'user-agent': 'Total.js/v' + F.version_header }; + req.uri = framework_internal.parseURI(req); + req.path = framework_internal.routeSplit(req.uri.pathname); + req.body = data || {}; + req.query = params ? F.onParseQuery(params) : {}; + req.files = EMPTYARRAY; + req.method = method; + res.options = req.options = {}; + + var route = F.lookupaction(req, url); + if (!route) + return; + + if (route.isPARAM) + req.split = framework_internal.routeSplit(req.uri.pathname, true); + else + req.split = EMPTYARRAY; + + var controller = new Controller(route.controller, null, null, route.currentViewDirectory); + controller.route = route; + controller.req = req; + controller.res = res; + + res.$evalroutecallback = controller.$evalroutecallback = callback || NOOP; + setImmediate(evalroutehandler, controller); + return controller; +}; + +function runsnapshot() { + + var main = {}; + var stats = {}; + var lastwarning = 0; + + stats.id = F.id; + stats.version = {}; + stats.version.node = process.version; + stats.version.total = F.version_header; + stats.version.app = CONF.version; + stats.pid = process.pid; + stats.thread = global.THREAD; + stats.mode = DEBUG ? 'debug' : 'release'; + stats.overload = 0; + + main.pid = process.pid; + main.stats = [stats]; + + F.snapshotstats = function() { + + var memory = process.memoryUsage(); + stats.date = NOW; + stats.memory = (memory.heapUsed / 1024 / 1024).floor(2); + stats.rm = F.temporary.service.request || 0; // request min + stats.fm = F.temporary.service.file || 0; // files min + stats.wm = F.temporary.service.message || 0; // websocket messages min + stats.mm = F.temporary.service.mail || 0; // mail min + stats.om = F.temporary.service.open || 0; // mail min + stats.em = F.temporary.service.external || 0; // external requests min + stats.dbrm = F.temporary.service.dbrm || 0; // DB read min + stats.dbwm = F.temporary.service.dbwm || 0; // DB write min + stats.usage = F.temporary.service.usage.floor(2); // app usage in % + stats.requests = F.stats.request.request; + stats.pending = F.stats.request.pending; + stats.errors = F.stats.error; + stats.timeouts = F.stats.response.error408; + stats.uptime = F.cache.count; + stats.online = F.stats.performance.online; + + var err = F.errors[F.errors.length - 1]; + var timeout = F.timeouts[F.timeouts.length - 1]; + + stats.lasterror = err ? (err.date.toJSON() + ' ' + (err.error ? err.error : err)) : undefined; + stats.lasttimeout = timeout; + + if ((stats.usage > 80 || stats.memory > 600 || stats.pending > 1000) && lastwarning !== NOW.getHours()) { + lastwarning = NOW.getHours(); + stats.overload++; + } + + if (F.isCluster) { + if (process.connected) { + CLUSTER_SNAPSHOT.data = stats; + process.send(CLUSTER_SNAPSHOT); + } + } else + Fs.writeFile(process.mainModule.filename + '.json', JSON.stringify(main, null, ' '), NOOP); + }; +} + +var lastusagedate; + +function measure_usage_response() { + var diff = (Date.now() - lastusagedate) - 60; + if (diff > 50) + diff = 50; + var val = diff < 0 ? 0 : (diff / 50) * 100; + if (F.temporary.service.usage < val) + F.temporary.service.usage = val; + F.stats.performance.usage = val; +} + +function measure_usage() { + lastusagedate = Date.now(); + setTimeout(measure_usage_response, 50); } // Because of controller prototypes -// It's used in F.view() and F.viewCompile() +// It's used in VIEW() and VIEWCOMPILE() const EMPTYCONTROLLER = new Controller('', null, null, ''); EMPTYCONTROLLER.isConnected = false; EMPTYCONTROLLER.req = {}; @@ -15920,5 +19813,3 @@ EMPTYCONTROLLER.req.query = EMPTYOBJECT; EMPTYCONTROLLER.req.body = EMPTYOBJECT; EMPTYCONTROLLER.req.files = EMPTYARRAY; global.EMPTYCONTROLLER = EMPTYCONTROLLER; -global.LOGMAIL = F.logmail; -global.MAIL = F.mail; diff --git a/internal.js b/internal.js index da54f7e06..91ff43b8d 100755 --- a/internal.js +++ b/internal.js @@ -1,4 +1,4 @@ -// Copyright 2012-2018 (c) Peter Širka +// Copyright 2012-2020 (c) Peter Širka // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the @@ -21,7 +21,7 @@ /** * @module FrameworkInternal - * @version 2.9.2 + * @version 3.4.3 */ 'use strict'; @@ -48,21 +48,23 @@ const REG_5 = />\n\s{1,}]+/; const REG_7 = /\\/g; const REG_8 = /'/g; +const REG_9 = />\n\s+/g; +const REG_10 = /(\w|\W)\n\s+/i; -const REG_COMPONENTS = /@{(\s)?(component|components)(\s)?\(/i; -const REG_COMPONENTS_GROUP = /('|")[a-z0-9]+('|")/i; +const REG_COMPONENTS_GROUP = /('|")[a-z0-9_]+('|")/i; const HTTPVERBS = { 'get': true, 'post': true, 'options': true, 'put': true, 'delete': true, 'patch': true, 'upload': true, 'head': true, 'trace': true, 'propfind': true }; -const RENDERNOW = ['self.$import(', 'self.route', 'self.$js(', 'self.$css(', 'self.$favicon(', 'self.$script(', '$STRING(self.resource(', '$STRING(RESOURCE(', 'self.translate(', 'language', 'self.sitemap_url(', 'self.sitemap_name(', '$STRING(CONFIG(', '$STRING(config.', '$STRING(config[', '$STRING(config(']; +const RENDERNOW = ['self.$import(', 'self.route', 'self.$js(', 'self.$css(', 'self.$favicon(', 'self.$script(', '$STRING(self.resource(', '$STRING(RESOURCE(', 'self.translate(', 'language', 'self.sitemap_url(', 'self.sitemap_name(', '$STRING(CONFIG(', '$STRING(config.', '$STRING(config[', '$STRING(CONF.', '$STRING(CONF[', '$STRING(config(']; const REG_NOTRANSLATE = /@\{notranslate\}/gi; const REG_NOCOMPRESS = /@\{nocompress\s\w+}/gi; -const REG_TAGREMOVE = /[^>]\n\s{1,}$/; +const REG_TAGREMOVE = /[^>](\r)\n\s{1,}$/; const REG_HELPERS = /helpers\.[a-z0-9A-Z_$]+\(.*?\)+/g; const REG_SITEMAP = /\s+(sitemap_navigation\(|sitemap\()+/g; -const REG_CSS_1 = /\n|\s{2,}/g; +const REG_CSS_0 = /\s{2,}|\t/g; +const REG_CSS_1 = /\n/g; const REG_CSS_2 = /\s?\{\s{1,}/g; const REG_CSS_3 = /\s?\}\s{1,}/g; const REG_CSS_4 = /\s?:\s{1,}/g; @@ -71,25 +73,28 @@ const REG_CSS_6 = /,\s{1,}/g; const REG_CSS_7 = /\s\}/g; const REG_CSS_8 = /\s\{/g; const REG_CSS_9 = /;\}/g; -const REG_CSS_10 = /\$[a-z0-9-_]+:.*?;/gi; -const REG_CSS_11 = /\$[a-z0-9-_]+/gi; +const REG_CSS_10 = /\$[a-z0-9-_]+(\s)*:.*?;/gi; +const REG_CSS_11 = /\$.*?(\s|;|\}|!)/gi; +const REG_CSS_12 = /(margin|padding):.*?(;|})/g; +const REG_CSS_13 = /#(0{6}|1{6}|2{6}|3{6}|4{6}|5{6}|6{6}|7{6}|8{6}|9{6}|0{6}|A{6}|B{6}|C{6}|D{6}|E{6}|F{6})/gi; +const REG_VIEW_PART = /\/\*PART.*?\*\//g; const AUTOVENDOR = ['filter', 'appearance', 'column-count', 'column-gap', 'column-rule', 'display', 'transform', 'transform-style', 'transform-origin', 'transition', 'user-select', 'animation', 'perspective', 'animation-name', 'animation-duration', 'animation-timing-function', 'animation-delay', 'animation-iteration-count', 'animation-direction', 'animation-play-state', 'opacity', 'background', 'background-image', 'font-smoothing', 'text-size-adjust', 'backface-visibility', 'box-sizing', 'overflow-scrolling']; const WRITESTREAM = { flags: 'w' }; -const EMPTYBUFFER = framework_utils.createBufferSize(0); +const ALLOWEDMARKUP = { G: 1, M: 1, R: 1, repository: 1, model: 1, CONF: 1, config: 1, global: 1, resource: 1, RESOURCE: 1, CONFIG: 1, author: 1, root: 1, functions: 1, NOW: 1, F: 1 }; var INDEXFILE = 0; -var INDEXMIXED = 0; global.$STRING = function(value) { return value != null ? value.toString() : ''; }; global.$VIEWCACHE = []; +global.$VIEWASYNC = 0; exports.parseMULTIPART = function(req, contentType, route, tmpDirectory) { - var boundary = contentType.split(';')[1]; - if (!boundary) { + var beg = contentType.indexOf('boundary='); + if (beg === -1) { F.reqstats(false, false); F.stats.request.error400++; req.res.writeHead(400); @@ -97,15 +102,26 @@ exports.parseMULTIPART = function(req, contentType, route, tmpDirectory) { return; } + var end = contentType.length; + + for (var i = (beg + 10); i < end; i++) { + if (contentType[i] === ';' || contentType[i] === ' ') { + end = i; + break; + } + } + + var boundary = contentType.substring(beg + 9, end); + // For unexpected closing req.once('close', () => !req.$upload && req.clear()); var parser = new MultipartParser(); var size = 0; - var stream; var maximumSize = route.length; - var tmp; var close = 0; + var stream; + var tmp; var rm; var fn_close = function() { close--; @@ -117,13 +133,9 @@ exports.parseMULTIPART = function(req, contentType, route, tmpDirectory) { var path = framework_utils.combine(tmpDirectory, (F.id ? 'i-' + F.id + '_' : '') + 'uploadedfile-'); - // Why indexOf(.., 2)? Because performance - boundary = boundary.substring(boundary.indexOf('=', 2) + 1); - req.buffer_exceeded = false; req.buffer_has = true; req.buffer_parser = parser; - parser.initWithBoundary(boundary); parser.onPartBegin = function() { @@ -133,7 +145,7 @@ exports.parseMULTIPART = function(req, contentType, route, tmpDirectory) { // Temporary data tmp = new HttpFile(); - tmp.$data = framework_utils.createBufferSize(); + tmp.$data = Buffer.alloc(0); tmp.$step = 0; tmp.$is = false; tmp.length = 0; @@ -168,7 +180,6 @@ exports.parseMULTIPART = function(req, contentType, route, tmpDirectory) { } header = parse_multipart_header(header); - tmp.$step = 1; tmp.$is = header[1] !== null; tmp.name = header[0]; @@ -251,7 +262,8 @@ exports.parseMULTIPART = function(req, contentType, route, tmpDirectory) { } req.files.push(tmp); - F.$events['upload-begin'] && F.emit('upload-begin', req, tmp); + F.$events['upload-begin'] && EMIT('upload-begin', req, tmp); + F.$events.upload_begin && EMIT('upload_begin', req, tmp); close++; stream = Fs.createWriteStream(tmp.path, WRITESTREAM); stream.once('close', fn_close); @@ -270,11 +282,15 @@ exports.parseMULTIPART = function(req, contentType, route, tmpDirectory) { if (req.buffer_exceeded) return; + if (tmp == null) + return; + if (tmp.$is) { tmp.$data = undefined; tmp.$is = undefined; tmp.$step = undefined; F.$events['upload-end'] && F.emit('upload-end', req, tmp); + F.$events.upload_end && F.emit('upload_end', req, tmp); return; } @@ -315,128 +331,6 @@ function uploadparser_done() { this.buffer_parser.end(); } -exports.parseMULTIPART_MIXED = function(req, contentType, tmpDirectory, onFile) { - - var boundary = contentType.split(';')[1]; - if (!boundary) { - F.reqstats(false, false); - F.stats.request.error400++; - req.res.writeHead(400); - req.res.end(); - return; - } - - // For unexpected closing - req.once('close', () => !req.$upload && req.clear()); - - var parser = new MultipartParser(); - var close = 0; - var stream; - var tmp; - var counter = 0; - var path = framework_utils.combine(tmpDirectory, (F.id ? 'i-' + F.id + '_' : '') + 'uploadedmixed-'); - - boundary = boundary.substring(boundary.indexOf('=', 2) + 1); - req.buffer_exceeded = false; - req.buffer_has = true; - req.buffer_parser = parser; - - parser.initWithBoundary(boundary); - - parser.onPartBegin = function() { - // Temporary data - tmp = new HttpFile(); - tmp.$step = 0; - tmp.$is = false; - tmp.length = 0; - }; - - parser.onHeaderValue = function(buffer, start, end) { - - if (req.buffer_exceeded) - return; - - var header = buffer.slice(start, end).toString(ENCODING); - if (tmp.$step === 1) { - var index = header.indexOf(';'); - if (index === -1) - tmp.type = header.trim(); - else - tmp.type = header.substring(0, index).trim(); - tmp.$step = 2; - return; - } - - if (tmp.$step !== 0) - return; - - header = parse_multipart_header(header); - - tmp.$step = 1; - tmp.$is = header[1] !== null; - tmp.name = header[0]; - - if (tmp.$is) { - tmp.filename = header[1]; - tmp.path = path + (INDEXMIXED++) + '.bin'; - - stream = Fs.createWriteStream(tmp.path, WRITESTREAM); - stream.once('close', () => close--); - stream.once('error', () => close--); - close++; - } else - destroyStream(stream); - }; - - parser.onPartData = function(buffer, start, end) { - - if (req.buffer_exceeded) - return; - - var data = buffer.slice(start, end); - var length = data.length; - - if (!tmp.$is) - return; - - if (tmp.length) { - stream.write(data); - tmp.length += length; - return; - } - - stream.write(data); - tmp.length += length; - onFile(req, tmp, counter++); - }; - - parser.onPartEnd = function() { - - if (stream) { - stream.end(); - stream = null; - } - - if (req.buffer_exceeded || !tmp.$is) - return; - - tmp.$is = undefined; - tmp.$step = undefined; - }; - - parser.onEnd = function() { - if (close) { - setImmediate(parser.onEnd); - } else { - onFile(req, null); - F.responseContent(req, req.res, 200, EMPTYBUFFER, 'text/plain', false); - } - }; - - req.on('data', uploadparser); - req.on('end', uploadparser_done); -}; - function parse_multipart_header(header) { var arr = new Array(2); @@ -561,7 +455,7 @@ exports.routeCompare = function(url, route, isSystem, isWildcard) { var length = url.length; var lengthRoute = route.length; - if (lengthRoute !== length && !isWildcard) + if ((lengthRoute !== length && !isWildcard) || (isWildcard && length < lengthRoute)) return false; if (isWildcard && lengthRoute === 1 && route[0] === '/') @@ -663,6 +557,8 @@ exports.routeCompareFlags2 = function(req, route, membertype) { return 0; if ((route.isREFERER && req.flags.indexOf('referer') === -1) || (!route.isMULTIPLE && route.isJSON && req.flags.indexOf('json') === -1)) return 0; + if (route.isROLE && !req.$roles && membertype) + return -1; } var isRole = false; @@ -679,11 +575,6 @@ exports.routeCompareFlags2 = function(req, route, membertype) { continue; return 0; - case 'proxy': - if (!route.isPROXY) - return 0; - continue; - case 'debug': if (!route.isDEBUG && route.isRELEASE) return 0; @@ -776,7 +667,23 @@ function HttpFile() { this.rem = true; } -HttpFile.prototype.rename = HttpFile.prototype.move = function(filename, callback) { +HttpFile.prototype = { + get size() { + return this.length; + }, + get extension() { + if (!this.$extension) + this.$extension = framework_utils.getExtension(this.filename); + return this.$extension; + }, + set extension(val) { + this.$extension = val; + } +}; + +var HFP = HttpFile.prototype; + +HFP.rename = HFP.move = function(filename, callback) { var self = this; Fs.rename(self.path, filename, function(err) { @@ -790,7 +697,7 @@ HttpFile.prototype.rename = HttpFile.prototype.move = function(filename, callbac return self; }; -HttpFile.prototype.copy = function(filename, callback) { +HFP.copy = function(filename, callback) { var self = this; @@ -807,38 +714,39 @@ HttpFile.prototype.copy = function(filename, callback) { return self; }; -HttpFile.prototype.$$rename = HttpFile.prototype.$$move = function(filename) { +HFP.$$rename = HFP.$$move = function(filename) { var self = this; return function(callback) { return self.rename(filename, callback); }; }; -HttpFile.prototype.$$copy = function(filename) { +HFP.$$copy = function(filename) { var self = this; return function(callback) { return self.copy(filename, callback); }; }; -HttpFile.prototype.readSync = function() { +HFP.readSync = function() { return Fs.readFileSync(this.path); }; -HttpFile.prototype.read = function(callback) { +HFP.read = function(callback) { var self = this; + F.stats.performance.open++; Fs.readFile(self.path, callback); return self; }; -HttpFile.prototype.$$read = function() { +HFP.$$read = function() { var self = this; return function(callback) { self.read(callback); }; }; -HttpFile.prototype.md5 = function(callback) { +HFP.md5 = function(callback) { var self = this; var md5 = Crypto.createHash('md5'); var stream = Fs.createReadStream(self.path); @@ -862,39 +770,61 @@ HttpFile.prototype.md5 = function(callback) { return self; }; -HttpFile.prototype.$$md5 = function() { +HFP.$$md5 = function() { var self = this; return function(callback) { self.md5(callback); }; }; -HttpFile.prototype.stream = function(options) { +HFP.stream = function(options) { return Fs.createReadStream(this.path, options); }; -HttpFile.prototype.pipe = function(stream, options) { +HFP.pipe = function(stream, options) { return Fs.createReadStream(this.path, options).pipe(stream, options); }; -HttpFile.prototype.isImage = function() { +HFP.isImage = function() { return this.type.indexOf('image/') !== -1; }; -HttpFile.prototype.isVideo = function() { +HFP.isVideo = function() { return this.type.indexOf('video/') !== -1; }; -HttpFile.prototype.isAudio = function() { +HFP.isAudio = function() { return this.type.indexOf('audio/') !== -1; }; -HttpFile.prototype.image = function(im) { +HFP.image = function(im) { if (im === undefined) - im = F.config['default-image-converter'] === 'im'; + im = CONF.default_image_converter === 'im'; return framework_image.init(this.path, im, this.width, this.height); }; +HFP.fs = function(storagename, custom, callback, id) { + if (typeof(custom) === 'function') { + id = callback; + callback = custom; + custom = null; + } + var storage = FILESTORAGE(storagename); + var stream = Fs.createReadStream(this.path); + return id ? storage.update(id, this.filename, stream, custom, callback) : storage.insert(this.filename, stream, custom, callback); +}; + +HFP.nosql = function(name, custom, callback, id) { + if (typeof(custom) === 'function') { + id = callback; + callback = custom; + custom = null; + } + var storage = NOSQL(name).binary; + var stream = Fs.createReadStream(this.path); + return id ? storage.update(id, this.filename, stream, custom, callback) : storage.insert(this.filename, stream, custom, callback); +}; + // ********************************************************************************* // ================================================================================= // JS CSS + AUTO-VENDOR-PREFIXES @@ -906,13 +836,61 @@ function compile_autovendor(css) { var isAuto = css.substring(0, 100).indexOf(avp) !== -1; if (isAuto) css = autoprefixer(css.replace(avp, '')); - return css.replace(REG_CSS_1, '').replace(REG_CSS_2, '{').replace(REG_CSS_3, '}').replace(REG_CSS_4, ':').replace(REG_CSS_5, ';').replace(REG_CSS_6, function(search, index, text) { + return css.replace(REG_CSS_0, ' ').replace(REG_CSS_1, '').replace(REG_CSS_2, '{').replace(REG_CSS_3, '}').replace(REG_CSS_4, ':').replace(REG_CSS_5, ';').replace(REG_CSS_6, function(search, index, text) { for (var i = index; i > 0; i--) { if ((text[i] === '\'' || text[i] === '"') && (text[i - 1] === ':')) return search; } return ','; - }).replace(REG_CSS_7, '}').replace(REG_CSS_8, '{').replace(REG_CSS_9, '}').trim(); + }).replace(REG_CSS_7, '}').replace(REG_CSS_8, '{').replace(REG_CSS_9, '}').replace(REG_CSS_12, cssmarginpadding).replace(REG_CSS_13, csscolors).trim(); +} + +function csscolors(text) { + return text.substring(0, 4); +} + +function cssmarginpadding(text) { + + // margin + // padding + + var prop = ''; + var val; + var l = text.length - 1; + var last = text[l]; + + if (text[0] === 'm') { + prop = 'margin:'; + val = text.substring(7, l); + } else { + prop = 'padding:'; + val = text.substring(8, l); + } + + var a = val.split(' '); + + for (var i = 0; i < a.length; i++) { + if (a[i][0] === '0' && a[i].charCodeAt(1) > 58) + a[i] = '0'; + } + + // 0 0 0 0 --> 0 + if (a[0] === '0' && a[1] === '0' && a[2] === '0' && a[3] === '0') + return prop + '0' + last; + + // 20px 0 0 0 --> 20px 0 0 + if (a[0] !== '0' && a[1] === '0' && a[2] === '0' && a[3] === '0') + return prop + a[0] + ' 0 0' + last; + + // 20px 30px 20px 30px --> 20px 30px + if (a[1] && a[2] && a[3] && a[0] === a[2] && a[1] === a[3]) + return prop + a[0] + ' ' + a[1] + last; + + // 20px 30px 10px 30px --> 20px 30px 10px + if (a[2] && a[3] && a[1] === a[3] && a[0] !== a[2]) + return prop + a[0] + ' ' + a[1] + ' ' + a[2] + last; + + return text; } function autoprefixer(value) { @@ -946,7 +924,8 @@ function autoprefixer(value) { continue; // text-transform - var isPrefix = value.substring(index - 1, index) === '-'; + var before = value.substring(index - 1, index); + var isPrefix = before === '-'; if (isPrefix) continue; @@ -956,7 +935,7 @@ function autoprefixer(value) { if (end === -1 || css.substring(0, end + 1).replace(/\s/g, '') !== property + ':') continue; - builder.push({ name: property, property: css }); + builder.push({ name: property, property: before + css, css: css }); } } @@ -966,19 +945,24 @@ function autoprefixer(value) { for (var i = 0; i < length; i++) { var name = builder[i].name; - property = builder[i].property; + var replace = builder[i].property; + var before = replace[0]; + + property = builder[i].css.trim(); var plus = property; var delimiter = ';'; var updated = plus + delimiter; if (name === 'opacity') { - var opacity = +plus.replace('opacity', '').replace(':', '').replace(/\s/g, ''); - if (isNaN(opacity)) - continue; - updated += 'filter:alpha(opacity=' + Math.floor(opacity * 100) + ')'; - value = value.replacer(property, '@[[' + output.length + ']]'); - output.push(updated); + var opacity = plus.replace('opacity', '').replace(':', '').replace(/\s/g, ''); + index = opacity.indexOf('!'); + opacity = index === -1 ? (+opacity) : (+opacity.substring(0, index)); + if (!isNaN(opacity)) { + updated += 'filter:alpha(opacity=' + Math.floor(opacity * 100) + ')' + (index !== -1 ? ' !important' : ''); + value = value.replacer(replace, before + '@[[' + output.length + ']]'); + output.push(updated); + } continue; } @@ -986,7 +970,7 @@ function autoprefixer(value) { updated = plus + delimiter; updated += plus.replacer('font-smoothing', '-webkit-font-smoothing') + delimiter; updated += plus.replacer('font-smoothing', '-moz-osx-font-smoothing'); - value = value.replacer(property, '@[[' + output.length + ']]'); + value = value.replacer(replace, before + '@[[' + output.length + ']]'); output.push(updated); continue; } @@ -997,50 +981,49 @@ function autoprefixer(value) { updated += plus.replacer('repeating-linear-', '-moz-repeating-linear-') + delimiter; updated += plus.replacer('repeating-linear-', '-ms-repeating-linear-') + delimiter; updated += plus; - value = value.replacer(property, '@[[' + output.length + ']]'); + value = value.replacer(replace, before + '@[[' + output.length + ']]'); output.push(updated); } else if (property.indexOf('repeating-radial-gradient') !== -1) { updated = plus.replacer('repeating-radial-', '-webkit-repeating-radial-') + delimiter; updated += plus.replacer('repeating-radial-', '-moz-repeating-radial-') + delimiter; updated += plus.replacer('repeating-radial-', '-ms-repeating-radial-') + delimiter; updated += plus; - value = value.replacer(property, '@[[' + output.length + ']]'); + value = value.replacer(replace, before + '@[[' + output.length + ']]'); output.push(updated); } else if (property.indexOf('linear-gradient') !== -1) { updated = plus.replacer('linear-', '-webkit-linear-') + delimiter; updated += plus.replacer('linear-', '-moz-linear-') + delimiter; updated += plus.replacer('linear-', '-ms-linear-') + delimiter; updated += plus; - value = value.replacer(property, '@[[' + output.length + ']]'); + value = value.replacer(replace, before + '@[[' + output.length + ']]'); output.push(updated); } else if (property.indexOf('radial-gradient') !== -1) { updated = plus.replacer('radial-', '-webkit-radial-') + delimiter; updated += plus.replacer('radial-', '-moz-radial-') + delimiter; updated += plus.replacer('radial-', '-ms-radial-') + delimiter; updated += plus; - value = value.replacer(property, '@[[' + output.length + ']]'); + value = value.replacer(replace, before + '@[[' + output.length + ']]'); output.push(updated); } - continue; } if (name === 'text-overflow') { updated = plus + delimiter; updated += plus.replacer('text-overflow', '-ms-text-overflow'); - value = value.replacer(property, '@[[' + output.length + ']]'); + value = value.replacer(replace, before + '@[[' + output.length + ']]'); output.push(updated); continue; } if (name === 'display') { - if (property.indexOf('box') === -1) - continue; - updated = plus + delimiter; - updated += plus.replacer('box', '-webkit-box') + delimiter; - updated += plus.replacer('box', '-moz-box'); - value = value.replacer(property, '@[[' + output.length + ']]'); - output.push(updated); + if (property.indexOf('box') !== -1) { + updated = plus + delimiter; + updated += plus.replacer('box', '-webkit-box') + delimiter; + updated += plus.replacer('box', '-moz-box'); + value = value.replacer(replace, before + '@[[' + output.length + ']]'); + output.push(updated); + } continue; } @@ -1050,7 +1033,7 @@ function autoprefixer(value) { if (name.indexOf('animation') === -1) updated += delimiter + '-ms-' + plus; - value = value.replacer(property, '@[[' + output.length + ']]'); + value = value.replacer(replace, before + '@[[' + output.length + ']]'); output.push(updated); } @@ -1142,11 +1125,12 @@ function minify_javascript(data) { var alpha = /[0-9a-z$]/i; var white = /\W/; var skip = { '$': true, '_': true }; + var newlines = { '\n': 1, '\r': 1 }; var regexp = false; - var scope; - var prev; - var next; - var last; + var scope, prev, next, last; + var vtmp = false; + var regvar = /^(\s)*var /; + var vindex = 0; while (true) { @@ -1177,7 +1161,7 @@ function minify_javascript(data) { if (c === '/' && next === '/') { isCI = true; continue; - } else if (isCI && (c === '\n' || c === '\r')) { + } else if (isCI && newlines[c]) { isCI = false; alpha.test(last) && output.push(' '); last = ''; @@ -1188,7 +1172,7 @@ function minify_javascript(data) { continue; } - if (c === '\t' || c === '\n' || c === '\r') { + if (c === '\t' || newlines[c]) { if (!last || !alpha.test(last)) continue; output.push(' '); @@ -1197,8 +1181,11 @@ function minify_javascript(data) { } if (!regexp && (c === ' ' && (white.test(prev) || white.test(next)))) { - if (!skip[prev] && !skip[next]) - continue; + // if (!skip[prev] && !skip[next]) + if (!skip[prev]) { + if (!skip[next] || !alpha.test(prev)) + continue; + } } if (regexp) { @@ -1229,6 +1216,43 @@ function minify_javascript(data) { scope = c; } + // var + if (!scope && c === 'v' && next === 'a') { + var v = c + data[index] + data[index + 1] + data[index + 2]; + if (v === 'var ') { + if (vtmp && output[output.length - 1] === ';') { + output.pop(); + output.push(','); + } else + output.push('var '); + index += 3; + vtmp = true; + continue; + } + } else { + if (vtmp) { + vindex = index + 1; + while (true) { + if (!data[vindex] || !white.test(data[vindex])) + break; + vindex++; + } + if (c === '(' || c === ')' || (c === ';' && !regvar.test(data.substring(vindex, vindex + 20)))) + vtmp = false; + } + } + + if ((c === '+' || c === '-') && next === ' ') { + if (data[index + 1] === c) { + index += 2; + output.push(c); + output.push(' '); + output.push(c); + last = c; + continue; + } + } + if ((c === '}' && last === ';') || ((c === '}' || c === ']') && output[output.length - 1] === ' ' && alpha.test(output[output.length - 2]))) output.pop(); @@ -1239,7 +1263,11 @@ function minify_javascript(data) { return output.join('').trim(); } -exports.compile_css = function(value, filename) { +exports.compile_css = function(value, filename, nomarkup) { + + // Internal markup + if (!nomarkup) + value = markup(value, filename); if (global.F) { value = modificators(value, filename, 'style'); @@ -1251,10 +1279,7 @@ exports.compile_css = function(value, filename) { var isVariable = false; - value = nested(value, '', function() { - isVariable = true; - }); - + value = nested(value, '', () => isVariable = true); value = compile_autovendor(value); if (isVariable) @@ -1262,12 +1287,16 @@ exports.compile_css = function(value, filename) { return value; } catch (ex) { - F.error(new Error('CSS compiler exception: ' + ex.message)); + F.error(new Error('CSS compiler error: ' + ex.message)); return ''; } }; -exports.compile_javascript = function(source, filename) { +exports.compile_javascript = function(source, filename, nomarkup) { + + // Internal markup + if (!nomarkup) + source = markup(source, filename); if (global.F) { source = modificators(source, filename, 'script'); @@ -1278,8 +1307,8 @@ exports.compile_javascript = function(source, filename) { return minify_javascript(source); }; -exports.compile_html = function(source, filename) { - return compressCSS(compressJS(compressHTML(source, true), 0, filename), 0, filename); +exports.compile_html = function(source, filename, nomarkup) { + return compressCSS(compressJS(compressHTML(source, true), 0, filename, nomarkup), 0, filename, nomarkup); }; // ********************************************************************************* @@ -1359,6 +1388,7 @@ function MultipartParser() { } exports.MultipartParser = MultipartParser; +const MultipartParserProto = MultipartParser.prototype; MultipartParser.stateToString = function(stateNumber) { for (var state in S) { @@ -1367,19 +1397,19 @@ MultipartParser.stateToString = function(stateNumber) { } }; -MultipartParser.prototype.initWithBoundary = function(str) { +MultipartParserProto.initWithBoundary = function(str) { var self = this; - self.boundary = framework_utils.createBufferSize(str.length + 4); + self.boundary = Buffer.alloc(str.length + 4); self.boundary.write('\r\n--', 0, 'ascii'); self.boundary.write(str, 4, 'ascii'); - self.lookbehind = framework_utils.createBufferSize(self.boundary.length + 8); + self.lookbehind = Buffer.alloc(self.boundary.length + 8); self.state = S.START; self.boundaryChars = {}; for (var i = 0; i < self.boundary.length; i++) self.boundaryChars[self.boundary[i]] = true; }; -MultipartParser.prototype.write = function(buffer) { +MultipartParserProto.write = function(buffer) { var self = this, i = 0, len = buffer.length, @@ -1424,11 +1454,14 @@ MultipartParser.prototype.write = function(buffer) { for (i = 0; i < len; i++) { c = buffer[i]; switch (state) { + case S.PARSER_UNINITIALIZED: return i; + case S.START: index = 0; state = S.START_BOUNDARY; + case S.START_BOUNDARY: if (index == boundary.length - 2) { if (c === HYPHEN) @@ -1456,10 +1489,12 @@ MultipartParser.prototype.write = function(buffer) { if (c === boundary[index + 2]) index++; break; + case S.HEADER_FIELD_START: state = S.HEADER_FIELD; mark('headerField'); index = 0; + case S.HEADER_FIELD: if (c === CR) { clear('headerField'); @@ -1483,12 +1518,15 @@ MultipartParser.prototype.write = function(buffer) { cl = lower(c); if (cl < A || cl > Z) return i; + break; + case S.HEADER_VALUE_START: if (c === SPACE) break; mark('headerValue'); state = S.HEADER_VALUE; + case S.HEADER_VALUE: if (c === CR) { dataCallback('headerValue', true); @@ -1496,23 +1534,26 @@ MultipartParser.prototype.write = function(buffer) { state = S.HEADER_VALUE_ALMOST_DONE; } break; + case S.HEADER_VALUE_ALMOST_DONE: if (c !== LF) return i; state = S.HEADER_FIELD_START; break; + case S.HEADERS_ALMOST_DONE: if (c !== LF) return i; callback('headersEnd'); state = S.PART_DATA_START; break; + case S.PART_DATA_START: state = S.PART_DATA; mark('partData'); + case S.PART_DATA: prevIndex = index; - if (!index) { // boyer-moore derrived algorithm to safely skip non-boundary data i += boundaryEnd; @@ -1577,8 +1618,10 @@ MultipartParser.prototype.write = function(buffer) { i--; } break; + case S.END: break; + default: return i; } @@ -1595,7 +1638,7 @@ MultipartParser.prototype.write = function(buffer) { return len; }; -MultipartParser.prototype.end = function() { +MultipartParserProto.end = function() { if ((this.state === S.HEADER_FIELD_START && this.index === 0) || (this.state === S.PART_DATA && this.index == this.boundary.length)) { this.onPartEnd && this.onPartEnd(); this.onEnd && this.onEnd(); @@ -1606,7 +1649,7 @@ MultipartParser.prototype.end = function() { } }; -MultipartParser.prototype.explain = function() { +MultipartParserProto.explain = function() { return 'state = ' + MultipartParser.stateToString(this.state); }; @@ -1676,10 +1719,11 @@ function localize(language, command) { return output; } +var VIEW_IF = { 'if ': 1, 'if(': 1 }; + function view_parse(content, minify, filename, controller) { - if (minify) - content = removeComments(content); + content = removeComments(content).ROOT(); var nocompressHTML = false; var nocompressJS = false; @@ -1715,10 +1759,10 @@ function view_parse(content, minify, filename, controller) { }).trim(); if (!nocompressJS) - content = compressJS(content, 0, filename); + content = compressJS(content, 0, filename, true); if (!nocompressCSS) - content = compressCSS(content, 0, filename); + content = compressCSS(content, 0, filename, true); content = F.$versionprepare(content); @@ -1732,36 +1776,7 @@ function view_parse(content, minify, filename, controller) { var isFirst = false; var txtindex = -1; var index = 0; - - if ((controller.$hasComponents || REG_COMPONENTS.test(content)) && REG_HEAD.test(content)) { - - index = content.indexOf('@{import('); - - var add = true; - while (index !== -1) { - var str = content.substring(index, content.indexOf(')', index)); - if (str.indexOf('components') !== -1) { - add = false; - break; - } else - index = content.indexOf('@{import(', index + str.length); - } - - if (add && controller.$hasComponents) { - if (controller.$hasComponents instanceof Array) { - content = content.replace(REG_HEAD, function(text) { - var str = ''; - for (var i = 0; i < controller.$hasComponents.length; i++) { - var group = F.components.groups[controller.$hasComponents[i]]; - if (group) - str += group.links; - } - return str + text; - }); - } else - content = content.replace(REG_HEAD, text => F.components.links + text); - } - } + var isCookie = false; function escaper(value) { @@ -1806,10 +1821,14 @@ function view_parse(content, minify, filename, controller) { var isCOMPILATION = false; var builderTMP = ''; var sectionName = ''; + var components = {}; var text; while (command) { + if (!isCookie && command.command.indexOf('cookie') !== -1) + isCookie = true; + if (old) { text = content.substring(old.end + 1, command.beg); if (text) { @@ -1846,14 +1865,14 @@ function view_parse(content, minify, filename, controller) { if (cmd[0] === '\'' || cmd[0] === '"') { if (cmd[1] === '%') { - var t = F.config[cmd.substring(2, cmd.length - 1)]; + var t = CONF[cmd.substring(2, cmd.length - 1)]; if (t != null) - builder += '+' + DELIMITER + t + DELIMITER; + builder += '+' + DELIMITER + (t.toString()).replace(/'/g, "\\'") + DELIMITER; } else builder += '+' + DELIMITER + (new Function('self', 'return self.$import(' + cmd[0] + '!' + cmd.substring(1) + ')'))(controller) + DELIMITER; } else if (cmd7 === 'compile' && cmd.lastIndexOf(')') === -1) { - builderTMP = builder + '+(F.onCompileView.call(self,\'' + (cmd8[7] === ' ' ? cmd.substring(8) : '') + '\','; + builderTMP = builder + '+(F.onCompileView.call(self,\'' + (cmd8[7] === ' ' ? cmd.substring(8).trim() : '') + '\','; builder = ''; sectionName = cmd.substring(8); isCOMPILATION = true; @@ -1866,7 +1885,6 @@ function view_parse(content, minify, filename, controller) { sectionName = cmd.substring(8); isSECTION = true; isFN = true; - } else if (cmd7 === 'helper ') { builderTMP = builder; @@ -1913,12 +1931,12 @@ function view_parse(content, minify, filename, controller) { } else { counter--; - builder += '}return $output;})()'; + builder += '}return $output})()'; newCommand = ''; } - } else if (cmd.substring(0, 3) === 'if ') { - builder += ';if (' + view_prepare_keywords(cmd).substring(3) + '){$output+=$EMPTY'; + } else if (VIEW_IF[cmd.substring(0, 3)]) { + builder += ';if (' + (cmd.substring(2, 3) === '(' ? '(' : '') + view_prepare_keywords(cmd).substring(3) + '){$output+=$EMPTY'; } else if (cmd7 === 'else if') { builder += '} else if (' + view_prepare_keywords(cmd).substring(7) + ') {$output+=$EMPTY'; } else if (cmd === 'else') { @@ -1927,7 +1945,7 @@ function view_parse(content, minify, filename, controller) { builder += '}$output+=$EMPTY'; } else { - tmp = view_prepare(command.command, newCommand, functionsName, controller); + tmp = view_prepare(command.command, newCommand, functionsName, controller, components); var can = false; // Inline rendering is supported only in release mode @@ -1937,20 +1955,21 @@ function view_parse(content, minify, filename, controller) { if (!a) { var isMeta = tmp.indexOf('\'meta\'') !== -1; var isHead = tmp.indexOf('\'head\'') !== -1; - var isComponent = tmp.indexOf('\'components\'') !== -1; - tmp = tmp.replace(/'(meta|head|components)',/g, '').replace(/(,,|,\)|\s{1,})/g, ''); - if (isMeta || isHead || isComponent) { + tmp = tmp.replace(/(\s)?'(meta|head)'(\s|,)?/g, '').replace(/(,,|,\)|\s{2,})/g, ''); + if (isMeta || isHead) { var tmpimp = ''; if (isMeta) tmpimp += (isMeta ? '\'meta\'' : ''); if (isHead) tmpimp += (tmpimp ? ',' : '') + (isHead ? '\'head\'' : ''); - if (isComponent) - tmpimp += (tmpimp ? ',' : '') + (isComponent ? '\'components\'' : ''); - builder += '+self.$import(' + tmpimp + ')'; + if (tmpimp) + builder += '+self.$import(' + tmpimp + ')'; } } - can = true; + + if (tmp !== 'self.$import()') + can = true; + break; } } @@ -1958,7 +1977,11 @@ function view_parse(content, minify, filename, controller) { if (can && !counter) { try { - var r = (new Function('self', 'config', 'return ' + tmp))(controller, F.config).replace(REG_7, '\\\\').replace(REG_8, '\\\''); + + if (tmp.lastIndexOf(')') === -1) + tmp += ')'; + + var r = (new Function('self', 'config', 'return ' + tmp))(controller, CONF).replace(REG_7, '\\\\').replace(REG_8, '\\\''); if (r) { txtindex = $VIEWCACHE.indexOf(r); if (txtindex === -1) { @@ -1969,7 +1992,7 @@ function view_parse(content, minify, filename, controller) { } } catch (e) { - console.log('VIEW EXCEPTION --->', filename, e, tmp); + console.log('A view compilation error --->', filename, e, tmp); F.errors.push({ error: e.stack, name: filename, url: null, date: new Date() }); if (view_parse_plus(builder)) @@ -1979,7 +2002,10 @@ function view_parse(content, minify, filename, controller) { } else if (tmp) { if (view_parse_plus(builder)) builder += '+'; - builder += wrapTryCatch(tmp, command.command, command.line); + if (tmp.substring(1, 4) !== '@{-' && tmp.substring(0, 11) !== 'self.$view') + builder += wrapTryCatch(tmp, command.command, command.line); + else + builder += tmp; } } @@ -1996,9 +2022,32 @@ function view_parse(content, minify, filename, controller) { if (RELEASE) builder = builder.replace(/(\+\$EMPTY\+)/g, '+').replace(/(\$output=\$EMPTY\+)/g, '$output=').replace(/(\$output\+=\$EMPTY\+)/g, '$output+=').replace(/(\}\$output\+=\$EMPTY)/g, '}').replace(/(\{\$output\+=\$EMPTY;)/g, '{').replace(/(\+\$EMPTY\+)/g, '+').replace(/(>'\+'<)/g, '><').replace(/'\+'/g, ''); - var fn = '(function(self,repository,model,session,query,body,url,global,helpers,user,config,functions,index,output,cookie,files,mobile,settings){var get=query;var post=body;var G=F.global;var R=this.repository;var M=model;var theme=this.themeName;var language=this.language;var sitemap=this.repository.$sitemap;var cookie=function(name){return self.req.cookie(name)};' + (functions.length ? functions.join('') + ';' : '') + 'var controller=self;' + builder + ';return $output;})'; + var keys = Object.keys(components); + + builder = builder.replace(REG_VIEW_PART, function(text) { + var data = []; + var comkeys = Object.keys(F.components.instances); + var key = text.substring(6, text.length - 2); + for (var i = 0; i < comkeys.length; i++) { + var com = F.components.instances[comkeys[i]]; + if (com.parts && com.group && components[com.group] && com.parts[key]) + data.push(com.parts[key]); + } + + if (!data.length) + return '$EMPTY'; + + data = data.join(''); + var index = $VIEWCACHE.indexOf(data); + if (index === -1) + index = $VIEWCACHE.push(data) - 1; + return '$VIEWCACHE[' + index + ']'; + }); + + var fn = ('(function(self,repository,model,session,query,body,url,global,helpers,user,config,functions,index,output,files,mobile,settings){var G=F.global;var R=this.repository;var M=model;var theme=this.themeName;var language=this.language;var sitemap=this.repository.$sitemap;' + (isCookie ? 'var cookie=function(name){return self.req.cookie(name)};' : '') + (functions.length ? functions.join('') + ';' : '') + 'var controller=self;' + builder + ';return $output;})'); try { fn = eval(fn); + fn.components = keys; } catch (e) { throw new Error(filename + ': ' + e.message.toString()); } @@ -2018,7 +2067,7 @@ function view_parse_plus(builder) { return c !== '!' && c !== '?' && c !== '+' && c !== '.' && c !== ':'; } -function view_prepare(command, dynamicCommand, functions, controller) { +function view_prepare(command, dynamicCommand, functions, controller, components) { var a = command.indexOf('.'); var b = command.indexOf('('); @@ -2054,6 +2103,10 @@ function view_prepare(command, dynamicCommand, functions, controller) { case 'end': return ''; + case 'part': + tmp = command.indexOf('('); + return '/*PART{0}*/'.format(command.substring(tmp + 2, command.length - 2)); + case 'section': tmp = command.indexOf('('); return tmp === -1 ? '' : '(repository[\'$section_' + command.substring(tmp + 1, command.length - 1).replace(/'|"/g, '') + '\'] || \'\')'; @@ -2074,18 +2127,23 @@ function view_prepare(command, dynamicCommand, functions, controller) { case '!isomorphic': return '$STRING(' + command + ')'; + case 'root': + var r = CONF.default_root; + return '\'' + (r ? r.substring(0, r.length - 1) : r) + '\''; + case 'M': case 'R': case 'G': case 'model': case 'repository': - case 'get': - case 'post': case 'query': case 'global': + case 'MAIN': case 'session': case 'user': case 'config': + case 'CONF': + case 'REPO': case 'controller': return view_is_assign(command) ? ('self.$set(' + command + ')') : ('$STRING(' + command + ').encode()'); @@ -2105,6 +2163,7 @@ function view_prepare(command, dynamicCommand, functions, controller) { case 'isomorphic': case 'settings': case 'CONFIG': + case 'FUNC': case 'function': case 'MODEL': case 'SCHEMA': @@ -2125,6 +2184,7 @@ function view_prepare(command, dynamicCommand, functions, controller) { case '!session': case '!user': case '!config': + case '!CONF': case '!functions': case '!model': case '!CONFIG': @@ -2175,10 +2235,21 @@ function view_prepare(command, dynamicCommand, functions, controller) { case 'sitemap_url': case 'sitemap_name': case 'sitemap_navigation': + case 'sitemap_url2': + case 'sitemap_name2': return 'self.' + command; + case 'breadcrumb_url': + case 'breadcrumb_name': + case 'breadcrumb_url2': + case 'breadcrumb_name2': + case 'breadcrumb_navigation': + return 'self.sitemap_' + command.substring(10); case 'sitemap': + case 'breadcrumb': case 'place': + if (name === 'breadcrumb') + name = 'sitemap'; return command.indexOf('(') === -1 ? '(repository[\'$' + command + '\'] || \'\')' : 'self.$' + command; case 'meta': @@ -2194,15 +2265,10 @@ function view_prepare(command, dynamicCommand, functions, controller) { case 'components': - if (!controller.$hasComponents) - controller.$hasComponents = []; - - if (controller.$hasComponents instanceof Array) { - var group = command.match(REG_COMPONENTS_GROUP); - if (group && group.length) { - group = group[0].toString().replace(/'|"'/g, ''); - controller.$hasComponents.indexOf(group) === -1 && controller.$hasComponents.push(group); - } + var group = command.match(REG_COMPONENTS_GROUP); + if (group && group.length) { + group = group[0].toString().replace(/'|"'/g, ''); + components[group] = 1; } return 'self.$' + command + (command.indexOf('(') === -1 ? '()' : ''); @@ -2211,22 +2277,58 @@ function view_prepare(command, dynamicCommand, functions, controller) { return '(' + command + ')'; case 'component': - controller.$hasComponents = true; + + tmp = command.indexOf('\''); + + var is = false; + if (tmp !== -1) { + name = command.substring(tmp + 1, command.indexOf('\'', tmp + 1)); + tmp = F.components.instances[name]; + if (tmp && tmp.render) + is = true; + } else { + tmp = command.indexOf('"'); + name = command.substring(tmp + 1, command.indexOf('"', tmp + 1)); + tmp = F.components.instances[name]; + if (tmp && tmp.render) + is = true; + } + + if (tmp) + components[tmp.group] = 1; + + if (is) { + + var settings = command.substring(11 + name.length + 2, command.length - 1).trim(); + if (settings === ')') + settings = ''; + + $VIEWASYNC++; + return '\'@{-{0}-}\'+(function(index){!controller.$viewasync&&(controller.$viewasync=[]);controller.$viewasync.push({replace:\'@{-{0}-}\',name:\'{1}\',settings:{2}});return $EMPTY})({0})'.format($VIEWASYNC, name, settings || 'null'); + } + return 'self.' + command; case 'routeJS': - case 'routeCSS': case 'routeScript': + case 'routeCSS': case 'routeStyle': case 'routeImage': case 'routeFont': case 'routeDownload': - case 'routeVideo': case 'routeStatic': - return 'self.' + command; + case 'routeVideo': + case 'public_js': + case 'public_css': + case 'public_image': + case 'public_font': + case 'public_download': + case 'public_video': + case 'public': case 'translate': return 'self.' + command; case 'json': + case 'json2': case 'sitemap_change': case 'sitemap_replace': case 'sitemap_add': @@ -2237,14 +2339,13 @@ function view_prepare(command, dynamicCommand, functions, controller) { case 'template': case 'templateToggle': case 'viewCompile': + case 'view_compile': case 'viewToggle': case 'download': case 'selected': case 'disabled': case 'checked': - case 'etag': case 'header': - case 'modified': case 'options': case 'readonly': case 'canonical': @@ -2264,7 +2365,7 @@ function view_prepare(command, dynamicCommand, functions, controller) { case 'hidden': case 'textarea': case 'password': - return 'self.$' + exports.appendModel(command); + return 'self.$' + appendModel(command); default: return F.helpers[name] ? ('helpers.' + view_insert_call(command)) : ('$STRING(' + (functions.indexOf(name) === -1 ? command[0] === '!' ? command.substring(1) + ')' : command + ').encode()' : command + ')')); @@ -2344,13 +2445,11 @@ function view_is_assign(value) { if (!skip) return true; } - } - return false; } -function view_find_command(content, index) { +function view_find_command(content, index, entire) { index = content.indexOf('@{', index); if (index === -1) @@ -2380,12 +2479,12 @@ function view_find_command(content, index) { if (command[0] === '{') return view_find_command(content, index + 1); - return { - beg: index, - end: i, - line: view_line_counter(content.substr(0, index)), - command: command - }; + var obj = { beg: index, end: i, line: view_line_counter(content.substr(0, index)), command: command }; + + if (entire) + obj.phrase = content.substring(index, i + 1); + + return obj; } return null; @@ -2458,12 +2557,25 @@ function removeComments(html) { function compressView(html, minify) { var cache = []; + var beg = 0; + var end; while (true) { - var beg = html.indexOf('@{'); + beg = html.indexOf('@{compile ', beg - 1); if (beg === -1) break; - var end = html.indexOf('}', beg + 2); + end = html.indexOf('@{end}', beg + 6); + if (end === -1) + break; + cache.push(html.substring(beg, end + 6)); + html = html.substring(0, beg) + '#@' + (cache.length - 1) + '#' + html.substring(end + 6); + } + + while (true) { + beg = html.indexOf('@{', beg); + if (beg === -1) + break; + end = html.indexOf('}', beg + 2); if (end === -1) break; cache.push(html.substring(beg, end + 1)); @@ -2484,9 +2596,9 @@ function compressView(html, minify) { * @param {Number} index Last index. * @return {String} */ -function compressJS(html, index, filename) { +function compressJS(html, index, filename, nomarkup) { - if (!F.config['allow-compile-script']) + if (!CONF.allow_compile_script) return html; var strFrom = ' \ No newline at end of file + diff --git a/test/config-debug b/test/config-debug index ef433010f..ae9af02e2 100755 --- a/test/config-debug +++ b/test/config-debug @@ -1,3 +1,8 @@ etag-version : 1 secret : total.js test -array (Array) : [1, 2, 3, 4] \ No newline at end of file +array (Array) : [1, 2, 3, 4] + +testbase : base64 MTIzNDU2 +testhex : hex 313233343536 + +testenv (Env) : APP_NAME \ No newline at end of file diff --git a/test/config-release b/test/config-release index ef433010f..ae9af02e2 100755 --- a/test/config-release +++ b/test/config-release @@ -1,3 +1,8 @@ etag-version : 1 secret : total.js test -array (Array) : [1, 2, 3, 4] \ No newline at end of file +array (Array) : [1, 2, 3, 4] + +testbase : base64 MTIzNDU2 +testhex : hex 313233343536 + +testenv (Env) : APP_NAME \ No newline at end of file diff --git a/test/controllers/default.js b/test/controllers/default.js index fbab051ba..c10694b94 100755 --- a/test/controllers/default.js +++ b/test/controllers/default.js @@ -2,7 +2,7 @@ var assert = require('assert'); exports.install = function() { - F.localize('/templates/'); + //F.localize('/templates/'); F.route(function DEFER(url, req, flags) { return url === '/custom/route/'; @@ -26,6 +26,18 @@ exports.install = function() { this.plain('ROBOT'); }, ['robot']); + GROUP(['get'], '/prefix1/', function() { + ROUTE('/test/', function() { + this.plain('PREFIX1TEST'); + }); + }); + + GROUP('/prefix2/', ['get'], function() { + ROUTE('/test/', function() { + this.plain('PREFIX2TEST'); + }); + }); + F.route('#route'); F.route('/view-in-modules/', '.' + F.path.modules('someview')); F.route('/options/', plain_options, ['options']); @@ -35,7 +47,6 @@ exports.install = function() { F.route('/sync/', synchronize); F.route('/schema-filter/', ['post', '*filter#update']); F.route('/package/', '@testpackage/test'); - F.route('/precompile/', view_precomile); F.route('/homepage/', view_homepage); F.route('/usage/', view_usage); F.route('/sse/', viewSSE_html); @@ -75,7 +86,7 @@ exports.install = function() { F.route('/post/json/', plain_post_json, ['json']); F.route('/post/xml/', plain_post_xml, ['xml']); F.route('/multiple/', plain_multiple, ['post', 'get', 'put', 'delete']); - F.route('/post/schema/', plain_post_schema_parse, ['post', '*test/User']); + F.route('POST /post/schema/', plain_post_schema_parse, ['*test/User']); F.route('/rest/', plain_rest, ['post']); F.route('/rest/', plain_rest, ['put']); F.route('/rest/', plain_rest, ['get', 'head']); @@ -89,6 +100,12 @@ exports.install = function() { F.route('/live/', viewLive); F.route('/live/incoming/', viewLiveIncoming, ['mixed']); + ROUTE('GET /api/static/orders/ *Orders --> @query'); + ROUTE('GET /api/static/users/ *Users --> @query'); + ROUTE('POST /api/static/orders/ *Orders --> @save'); + ROUTE('GET /api/dynamic/{schema}/ *{schema} --> @query'); + ROUTE('POST /api/dynamic/{schema}/ *{schema} --> @save'); + F.redirect('http://www.google.sk', 'http://www.petersirka.sk'); F.route('#408', function DEFER() { @@ -97,11 +114,11 @@ exports.install = function() { self.plain('408'); }); - assert.ok(F.encrypt('123456', 'key', false) === 'MjM9QR8HExlaHQJQBxcGAEoaFQoGGgAW', 'F.encrypt(string)'); - assert.ok(F.decrypt('MjM9QR8HExlaHQJQBxcGAEoaFQoGGgAW', 'key', false) === '123456', 'F.decrypt(string)'); + assert.ok(F.encrypt('123456', 'key', false) === '5787-32333d411f0713195a1d0250071706004a1a150a061a0016', 'F.encrypt(string)'); + assert.ok(F.decrypt('5787-32333d411f0713195a1d0250071706004a1a150a061a0016', 'key', false) === '123456', 'F.decrypt(string)'); - assert.ok(F.encrypt({ name: 'Peter' }, 'key', false) === 'MzM9QVUXTkwCThBbF3RXQRlYBkUFVRdOTAJOEFsXdFdBGQ', 'F.encrypt(object)'); - assert.ok(F.decrypt('MzM9QVUXTkwCThBbF3RXQRlYBkUFVRdOTAJOEFsXdFdBGQ', 'key').name === 'Peter', 'F.decrypt(object)') + assert.ok(F.encrypt({ name: 'Peter' }, 'key', false) === '6931-33333d4155174e4c024e105b17745741195806450555174e4c024e105b1774574119', 'F.encrypt(object)'); + assert.ok(F.decrypt('6931-33333d4155174e4c024e105b17745741195806450555174e4c024e105b1774574119', 'key').name === 'Peter', 'F.decrypt(object)'); assert.ok(SOURCE('main').hello() === 'world', 'source'); assert.ok(INCLUDE('main').hello() === 'world', 'source'); @@ -138,7 +155,7 @@ function plain_options() { function *synchronize() { var self = this; - var content = (yield sync(require('fs').readFile)(self.path.public('file.txt'))).toString('utf8'); + var content = (yield sync(require('fs').readFile)(PATH.public('file.txt'))).toString('utf8'); self.plain(content); } @@ -146,12 +163,6 @@ function plain_rest() { this.plain(this.req.method); } -function view_precomile() { - var self = this; - self.layout('precompile._layout'); - self.view('precompile.homepage'); -} - function plain_multiple() { var self = this; self.plain('POST-GET-PUT-DELETE'); @@ -171,7 +182,7 @@ function plain_post_parse() { var self = this; self.layout(''); var output = self.view('params', null, true); - assert.ok(output === '--body=total.js--query=query--post=total.js--get=query--', 'Problem with getting values from request body and URL.'); + assert.ok(output === '--body=total.js--query=query--', 'Problem with getting values from request body and URL.'); self.body.type = 'parse'; self.json(self.body); } @@ -408,7 +419,7 @@ function viewTest() { } function viewDynamic() { - this.viewCompile('@{model.name}', { name: 'Peter' }); + this.viewcompile('@{model.name}', { name: 'Peter' }); } function viewTranslate() { @@ -421,9 +432,9 @@ function viewIndex() { var self = this; var name = 'controller: '; - assert.ok(self.path.public('file.txt').endsWith('/public/file.txt'), name + 'path.public'); - assert.ok(self.path.logs('file.txt').endsWith('/file.txt'), name + 'path.logs'); - assert.ok(self.path.temp('file.txt').endsWith('/file.txt'), name + 'path.temp'); + assert.ok(PATH.public('file.txt').endsWith('/public/file.txt'), name + 'path.public'); + assert.ok(PATH.logs('file.txt').endsWith('/file.txt'), name + 'path.logs'); + assert.ok(PATH.temp('file.txt').endsWith('/file.txt'), name + 'path.temp'); self.meta('A', 'B'); assert.ok(self.repository['$title'] === 'A' && self.repository['$description'] === 'B', name + 'meta() - write'); @@ -440,33 +451,25 @@ function viewIndex() { assert.ok(framework.model('other/products').ok === 2, 'framework: model() - 2'); assert.ok(self.secured === false, 'controller.secured'); - assert.ok(self.config.isDefinition === true, 'definitions()'); + assert.ok(CONF.isDefinition === true, 'definitions()'); assert.ok(!self.xhr, name + 'xhr'); - assert.ok(self.flags.indexOf('get') !== -1, name + 'flags') + assert.ok(self.flags.indexOf('get') !== -1, name + 'flags'); assert.ok(self.resource('name') === 'default' && self.resource('default', 'name') === 'default', name + 'resource(default)'); assert.ok(self.resource('test', 'name') === 'test', name + 'resource(test.resource)'); self.log('test'); - - assert.ok(self.hash('sha1', '123456', false) === '7c4a8d09ca3762af61e59520943dc26494f8941b', 'controller.hash()'); - - self.setModified('123456'); - var date = new Date(); date.setFullYear(1984); - self.setModified(date); - self.setExpires(date); - - assert.ok(self.routeScript('p.js') === '/js/p.js', name + 'routeScript()'); - assert.ok(self.routeStyle('p.css') === '/css/p.css', name + 'routeStyle()'); - assert.ok(self.routeImage('p.jpg') === '/img/p.jpg', name + 'routeImage()'); - assert.ok(self.routeVideo('p.avi') === '/video/p.avi', name + 'routeVideo()'); - assert.ok(self.routeFont('p.woff') === '/fonts/p.woff', name + 'routeFont()'); - assert.ok(self.routeDownload('p.pdf') === '/download/p.pdf', name + 'routeDownload()'); - assert.ok(self.routeStatic('/p.zip') === '/p.zip', name + 'routeStatic()'); + assert.ok(self.public_js('p.js') === '/js/p.js', name + 'public_js()'); + assert.ok(self.public_css('p.css') === '/css/p.css', name + 'public_css()'); + assert.ok(self.public_image('p.jpg') === '/img/p.jpg', name + 'public_image()'); + assert.ok(self.public_video('p.avi') === '/video/p.avi', name + 'public_video()'); + assert.ok(self.public_font('p.woff') === '/fonts/p.woff', name + 'public_font()'); + assert.ok(self.public_download('p.pdf') === '/download/p.pdf', name + 'public_download()'); + assert.ok(self.public('/p.zip') === '/p.zip', name + 'public()'); self.layout(''); assert.ok(self.view('test', null, true) === 'Total.js', name + 'view'); @@ -523,7 +526,6 @@ function viewViews() { //console.log('\n\n\n'); //self.framework.stop(); //return; - assert.ok(output.contains('#COMPONENTVIEWPETER#'), name + 'components rendering'); assert.ok(output.contains('#
@{{ vue_command }}
#'), name + 'VUE command'); assert.ok(output.contains('#mobilefalse#'), name + 'mobile'); @@ -532,14 +534,14 @@ function viewViews() { assert.ok(output.contains('HELPER:1-10'), name + 'inline helper + foreach 1'); assert.ok(output.contains('HELPER:2-21'), name + 'inline helper + foreach 2'); assert.ok(output.contains('
SECTION
'), name + 'section'); - assert.ok(output.contains('COMPILE_TANGULARCOMPILED'), name + 'onCompileView with name'); + assert.ok(output.contains('COMPILE_TANGULAR\nCOMPILED'), name + 'onCompileView with name'); assert.ok(output.contains('COMPILE_WITHOUTCOMPILED'), name + 'onCompileView without name'); assert.ok(output.contains('
4
4
FOREACH
'), name + 'foreach'); assert.ok(output.contains('
3
3
C:10
C:11
C:12
'), name + 'foreach - nested'); assert.ok(output.contains('5'), name + 'Inline assign value'); - assert.ok(output.contains('var d="$\'"'), name + 'JS script special chars 1'); - assert.ok(output.contains("var e='$\\'';"), name + "JS script special chars 2"); - assert.ok(output.contains(''), name + ' minify html'); + assert.ok(output.contains(',d="$\'"'), name + 'JS script special chars 1'); + assert.ok(output.contains(",e='$\\'',"), name + "JS script special chars 2"); + assert.ok(output.contains('') || output.contains(''), name + ' minify html'); assert.ok(output.contains('#tag-encode<b>A</b>#'), name + 'encode value'); assert.ok(output.contains('#tag-rawA#'), name + 'raw value'); assert.ok(output.contains('#helper-fn-A#'), name + 'helper function'); @@ -552,7 +554,6 @@ function viewViews() { assert.ok(output.contains('#options-empty#'), name + 'options() - without property name and value'); assert.ok(output.contains('#options#'), name + 'options() - with property name and value'); assert.ok(output.contains('#view#bmodel##'), name + 'view() with model'); - assert.ok(output.contains('#view-toggle#'), name + 'viewToggle()'); assert.ok(output.contains('#titleTITLE#'), name + 'title'); assert.ok(output.contains('#routejs-/js/p.js#'), name + 'route to static'); assert.ok(output.contains('#
content#'), name + 'download'); @@ -595,7 +596,7 @@ function viewViews() { assert.ok(output.contains('#ACAXXX#'), name + 'if'); assert.ok(output.contains(''), name + 'radio'); assert.ok(output.contains('
NESTED
'), name + 'if - nested'); - assert.ok(output.contains('---
Hello World!
Price: 12
---'), name + '- "/" view path problem'); + assert.ok(output.contains('---
Hello World!
---'), name + '- "/" view path problem'); F.script('next(value.toLowerCase())', 'PETER', function(err, val) { assert.ok(val ==='peter', 'SCRIPT: lowercase'); diff --git a/test/controllers/share.js b/test/controllers/share.js index 9591e7451..071aa367b 100755 --- a/test/controllers/share.js +++ b/test/controllers/share.js @@ -1,5 +1,3 @@ -exports.dependencies = ['test']; - exports.install = function() { framework.route('/share/', view_share); framework.route('/router/', view_router); diff --git a/test/definitions/initialize.js b/test/definitions/initialize.js index 65c2b35a1..3bacbf4af 100644 --- a/test/definitions/initialize.js +++ b/test/definitions/initialize.js @@ -3,76 +3,65 @@ var assert = require('assert'); F.register(F.path.root('default.resource')); framework.onMeta = function(a,b) { - return a + b; + return a + b; }; framework.onSettings = function(a,b) { - return a + b; + return a + b; }; framework.onPictureDimension = function(dimension) { - switch(dimension) { - case 'small': - return { width: 128, height: 96 }; - case 'middle': - return { width: 320, height: 240 }; - } + switch(dimension) { + case 'small': + return { width: 128, height: 96 }; + case 'middle': + return { width: 320, height: 240 }; + } - return null; + return null; }; framework.on('load', function() { - var self = this; - - self.log = function(value) { - assert.ok(value === 'test', 'framework: log()'); - return self; - }; - - self.helpers.property = 'OK'; - self.helpers.fn = function(a) { - return a; - }; - - self.global.header = 0; - self.global.middleware = 0; - self.global.timeout = 0; - self.global.file = 0; - self.global.all = 0; - -/* - REMOVED - self.middleware(function(next) { - self.global.header++; - next(); - }); -*/ - self.middleware('each', function(req, res, next) { - self.global.all++; - next(); - }); - - self.middleware('middleware', function(req, res, next) { - self.global.middleware++; - next(); - }); - - self.middleware('file', function(req, res, next) { - self.global.file++; - assert.ok(req.isStaticFile === true, 'file middleware problem'); - next(); - }); - - self.use('each'); + var self = this; + + self.log = function(value) { + assert.ok(value === 'test', 'framework: log()'); + return self; + }; + + self.helpers.property = 'OK'; + self.helpers.fn = function(a) { + return a; + }; + + self.global.header = 0; + self.global.middleware = 0; + self.global.timeout = 0; + self.global.file = 0; + self.global.all = 0; + + self.middleware('each', function($) { + self.global.all++; + $.next(); + }); + + self.middleware('middleware', function($) { + self.global.middleware++; + $.next(); + }); + + self.middleware('file', function($) { + self.global.file++; + assert.ok($.req.isStaticFile === true, 'file middleware problem'); + $.next(); + }); + + self.use('each'); }); framework.onPictureUrl = function(dimension, id, width, height, alt) { - return dimension + '-' + id + '.jpg'; -}; - -framework.onValidate = function(name, value) { - return name + value; + return dimension + '-' + id + '.jpg'; }; // Is read from http://www.totaljs.com/framework/include.js diff --git a/test/dependencies b/test/dependencies deleted file mode 100644 index 6e662936e..000000000 --- a/test/dependencies +++ /dev/null @@ -1 +0,0 @@ -module (1 day) : https://www.totaljs.com/framework/include.js --> {"test":true} \ No newline at end of file diff --git a/test/isomorphic/test.js b/test/isomorphic/test.js deleted file mode 100644 index d6edb1d74..000000000 --- a/test/isomorphic/test.js +++ /dev/null @@ -1,6 +0,0 @@ -exports.id = 'test'; -exports.url = 'isomorphic.js'; - -exports.price = function(count) { - return count * 1.20; -}; \ No newline at end of file diff --git a/test/modules/inline-view.js b/test/modules/inline-view.js index 0df8982b6..39b6fac8a 100644 --- a/test/modules/inline-view.js +++ b/test/modules/inline-view.js @@ -1,12 +1,11 @@ var assert = require('assert'); -exports.dependencies = ['test']; exports.installed = false; exports.install = function() { - exports.installed = true; - framework.route('/inline-view-route/'); - setTimeout(function() { - assert.ok(framework.view('view') === '
Total.js
', 'framework.view()'); - }, 100); + exports.installed = true; + ROUTE('/inline-view-route/'); + setTimeout(function() { + assert.ok(VIEW('view') === '
Total.js
', 'VIEW()'); + }, 100); }; \ No newline at end of file diff --git a/test/modules/test.js b/test/modules/test.js index 88825c6f6..0699116ed 100755 --- a/test/modules/test.js +++ b/test/modules/test.js @@ -5,9 +5,9 @@ exports.install = function() { app = framework; assert.ok(typeof(framework.modules) === 'object', 'module install'); - setTimeout(function() { - assert.ok(MODULE('inline-view').installed, 'module install dependencies'); - }, 3000); + setTimeout(function() { + assert.ok(MODULE('inline-view').installed, 'module install dependencies'); + }, 3000); }; exports.message = function() { diff --git a/test/schemas/user.js b/test/schemas/user.js new file mode 100644 index 000000000..15a631945 --- /dev/null +++ b/test/schemas/user.js @@ -0,0 +1,20 @@ +F.global.schemas = 1; + +NEWSCHEMA('Orders', function(schema) { + + schema.define('name', String, true); + + schema.setQuery(function($) { + $.success('orders'); + }); + + schema.setSave(function($) { + $.success('orders'); + }); +}); + +NEWSCHEMA('Users', function(schema) { + schema.setQuery(function($) { + $.success('users'); + }); +}); \ No newline at end of file diff --git a/test/sql.js b/test/sql.js new file mode 100644 index 000000000..05fd36741 --- /dev/null +++ b/test/sql.js @@ -0,0 +1,192 @@ +require('../index'); + +function SQL(query) { + + var self = this; + self.options = {}; + self.builder = new framework_nosql.DatabaseBuilder(null); + self.query = query; + + var type = self.parseType(); + switch (type) { + case 'select': + self.parseLimit(); + self.parseOrder(); + self.parseWhere(); + self.parseJoins(); + self.parseTable(); + self.parseNames(); + break; + case 'update': + self.parseWhere(); + self.parseTable(); + self.parseUpdate(); + break; + case 'insert': + self.parseWhere(); + self.parseInsert(); + break; + case 'delete': + self.parseWhere(); + self.parseTable(); + break; + } + + console.log(self.options); +} + +var SQLP = SQL.prototype; + +SQLP.parseLimit = function() { + var self = this; + var tmp = self.query.match(/take \d+ skip \d+$/i); + !tmp && (tmp = self.query.match(/skip \d+ take \d+$/i)); + !tmp && (tmp = self.query.match(/take \d+$/i)); + !tmp && (tmp = self.query.match(/skip \d+$/i)); + if (!tmp) + return self; + self.query = self.query.replace(tmp, '').trim(); + var arr = tmp[0].toString().toLowerCase().split(' '); + if (arr[0] === 'take') + self.options.take = +arr[1]; + else if (arr[2] === 'take') + self.options.take = +arr[3]; + if (arr[0] === 'skip') + self.options.skip = +arr[1]; + else if (arr[2] === 'skip') + self.options.skip = +arr[3]; + return self; +}; + +SQLP.parseOrder = function() { + var self = this; + var tmp = self.query.match(/order by .*?$/i); + + if (!tmp) + return self; + + self.query = self.query.replace(tmp, '').trim(); + var arr = tmp[0].toString().substring(9).split(','); + + self.options.sort = []; + + for (var i = 0; i < arr.length; i++) { + tmp = arr[i].trim().split(' '); + self.options.sort.push({ name: tmp[0], desc: (tmp[1] || '').toLowerCase() === 'desc' }); + } + + return self; +}; + +SQLP.parseWhere = function() { + var self = this; + var tmp = self.query.match(/where .*?$/i); + if (!tmp) + return self; + self.query = self.query.replace(tmp, ''); + + tmp = tmp[0].toString().substring(6).replace(/\sAND\s/gi, ' && ').replace(/\sOR\s/gi, ' || ').replace(/[a-z0-9]=/gi, function(text) { + return text + '='; + }); + + self.options.where = tmp; + return self; +}; + +SQLP.parseJoins = function() { + var self = this; + var tmp = self.query.match(/left join.*?$/i); + if (!tmp) { + tmp = self.query.match(/join.*?$/i); + if (!tmp) + return self; + } + self.query = self.query.replace(tmp, ''); + tmp = tmp[0].toString().trim(); + // console.log(tmp); + return self; +}; + +SQLP.parseTable = function() { + var self = this; + var tmp = self.query.match(/from\s.*?$/i); + if (!tmp) + return self; + self.query = self.query.replace(tmp, ''); + tmp = tmp[0].toString().substring(5).trim(); + + var arr = tmp.split(' '); + // console.log(arr); + + return self; +}; + +SQLP.parseNames = function() { + var self = this; + var tmp = self.query.match(/select\s.*?$/i); + if (!tmp) + return self; + + self.query = self.query.replace(tmp, ''); + tmp = tmp[0].toString().substring(6).trim().split(','); + + self.options.fields = []; + + for (var i = 0; i < tmp.length; i++) { + var field = tmp[i].trim(); + var alias = field.match(/as\s.*?$/); + var name = ''; + var type = 0; + + if (alias) { + field = field.replace(alias, ''); + alias = alias.toString().substring(3); + } + + var index = field.indexOf('('); + if (index !== -1) { + switch (field.substring(0, index).toLowerCase()) { + case 'count': + type = 1; + break; + case 'min': + type = 2; + break; + case 'max': + type = 3; + break; + case 'avg': + type = 4; + break; + case 'sum': + type = 5; + break; + case 'distinct': + type = 6; + break; + } + name = field.substring(index + 1, field.lastIndexOf(')')); + } else + name = field; + self.options.fields.push({ alias: alias || name, name: name, type: type }); + } + + return self; +}; + +SQLP.parseUpdate = function() { + var self = this; + return self; +}; + +SQLP.parseInsert = function() { + var self = this; + return self; +}; + +SQLP.parseType = function() { + var self = this; + return self.query.substring(0, self.query.indexOf(' ')).toLowerCase(); +}; + +var sql = new SQL('SELECT COUNT(*) as count, id FROM table a JOIN users ON id=id WHERE a.name="Peter" AND a.age=30 ORDER BY a.name, a.age ASC TAKE 20 SKIP 10'); \ No newline at end of file diff --git a/test/test-builders.js b/test/test-builders.js index c3d6353e8..e140df2ae 100755 --- a/test/test-builders.js +++ b/test/test-builders.js @@ -169,29 +169,29 @@ function test_Schema() { }); }); - GETSCHEMA('2').addTransform('xml', function(err, model, helper, next) { - next('OK'); + GETSCHEMA('2').addTransform('xml', function($) { + $.next('OK'); }).addWorkflow('send', function($) { countW++; $.callback('workflow'); - }).addOperation('test', function(err, model, helper, next) { - assert.ok(!model, 'schema - operation 1'); - assert.ok(helper === true, 'schema - operation 2'); - next(false); - }).setGet(function(error, model, helper, next) { - assert.ok(error.hasError() === false, 'schema - setGet'); - model.age = 99; - next(); - }).setSave(function(error, model, helper, next) { + }).addOperation('test', function($) { + assert.ok(!$.model, 'schema - operation 1'); + assert.ok($.options === true, 'schema - operation 2'); + $.next(false); + }).setGet(function($) { + assert.ok($.error.hasError() === false, 'schema - setGet'); + $.model.age = 99; + $.next(); + }).setSave(function($) { countS++; - assert.ok(error.hasError() === false, 'schema - setSave'); - next(true); - }).setRemove(function(error, helper, next) { - assert.ok(error.hasError() === false, 'schema - setRemove'); - next(true); - }).setQuery(function(error, helper, next) { - assert.ok(error.hasError() === false, 'schema - setQuery'); - next([]); + assert.ok($.error.hasError() === false, 'schema - setSave'); + $.next(true); + }).setRemove(function($) { + assert.ok($.error.hasError() === false, 'schema - setRemove'); + $.next(true); + }).setQuery(function($) { + assert.ok($.error.hasError() === false, 'schema - setQuery'); + $.next([]); }); //console.log(builders.defaults('1', { name: 'Peter', age: 30, join: { name: 20 }})); @@ -227,19 +227,17 @@ function test_Schema() { assert.ok(!result, 'schema - operation - result'); }); - GETSCHEMA('default', '2').addOperation('test2', function(error, model, helper, next) { - assert.ok(model === 1 || model == null, 'schema - operation problem with model'); - assert.ok(helper === 2 || helper == null, 'schema - operation problem with helper'); - next(3); + GETSCHEMA('default', '2').addOperation('test2', function($) { + assert.ok($.model === 1 || $.model == null, 'schema - operation problem with model'); + assert.ok($.options != null, 'schema - operation problem with options'); + $.next(3); }).operation('test2', 1, 2, function(err, value) { assert.ok(value === 3, 'schema - operation advanced 1'); }).operation('test2', 2, function(err, value) { assert.ok(value === 3, 'schema - operation advanced 2'); }).operation('test2', null, function(err, value) { assert.ok(value === 3, 'schema - operation advanced 3'); - }).constant('test', true); - - assert.ok(GETSCHEMA('2').constant('test') === true, 'schema - constant'); + }); NEWSCHEMA('validator').make(function(schema) { schema.define('name', String, true); @@ -299,7 +297,7 @@ function test_Schema() { }); x.setValidate(function(name, value, path, model) { - if (!path.startsWith('x.')) + if (!path.startsWith('x.') && path.indexOf('ref.') === -1) assert.ok((name === 'age' && value > 22) || (name === 'note' && value.length > 3), 'SchemaBuilderEntity.validation() 2'); }); @@ -366,6 +364,7 @@ function test_Schema() { var NewTypes = NEWSCHEMA('NewTypes').make(function(schema) { schema.define('capitalize', 'Capitalize'); schema.define('capitalize10', 'Capitalize(10)'); + schema.define('capitalize2', 'Capitalize2'); schema.define('lower', 'Lower'); schema.define('lower10', 'Lower(10)'); schema.define('upper', 'Upper'); @@ -374,6 +373,7 @@ function test_Schema() { schema.define('phone', 'Phone'); schema.define('url', 'Url'); schema.define('uid', 'UID'); + schema.define('base64', 'Base64'); var obj = {}; schema.fields.forEach(n => obj[n] = 'total fraMEWOrk'); @@ -381,10 +381,12 @@ function test_Schema() { obj.phone = '+421 903 163 302'; obj.url = 'https://www.totaljs.com'; obj.uid = UID(); + obj.base64 = 'WA=='; var res = schema.make(obj); assert.ok(res.capitalize === 'Total FraMEWOrk', 'SchemaBuilder: Capitalize'); assert.ok(res.capitalize10 === 'Total FraM', 'SchemaBuilder: Capitalize(10)'); + assert.ok(res.capitalize2 === 'Total fraMEWOrk', 'SchemaBuilder: Capitalize2'); assert.ok(res.lower === 'total framework', 'SchemaBuilder: Lower'); assert.ok(res.lower10 === 'total fram', 'SchemaBuilder: Lower(10)'); assert.ok(res.upper === 'TOTAL FRAMEWORK', 'SchemaBuilder: Upper'); @@ -393,38 +395,41 @@ function test_Schema() { assert.ok(res.phone === '+421903163302', 'SchemaBuilder: Phone'); assert.ok(res.url === 'https://www.totaljs.com', 'SchemaBuilder: URL'); assert.ok(res.uid ? true : false, 'SchemaBuilder: UID'); + assert.ok(res.base64 ? true : false, 'SchemaBuilder: Base64'); obj.phone = '+4210000'; obj.uid = U.GUID(10); obj.url = 'totaljs.com'; - obj.zip = 'A349393'; + obj.zip = '349393'; + obj.base64 = 'adlajkd'; + res = schema.make(obj); - assert.ok(res.zip ? false : true, 'SchemaBuilder: Zip must be empty'); assert.ok(res.phone ? false : true, 'SchemaBuilder: Phone must be empty'); assert.ok(res.url ? false : true, 'SchemaBuilder: URL must be empty'); assert.ok(res.uid ? false : true, 'SchemaBuilder: UID must be empty'); + assert.ok(res.base64 ? false : true, 'SchemaBuilder: Base64 must be empty'); }); NEWSCHEMA('Hooks').make(function(schema) { - schema.addHook('1', function(error, model, options, callback) { - model.counter = 1; - callback(); + schema.addHook('1', function($) { + $.model.counter = 1; + $.callback(); }); - schema.addHook('1', function(error, model, options, callback) { - model.counter++; - callback(); + schema.addHook('1', function($) { + $.model.counter++; + $.callback(); }); - schema.addHook('1', function(error, model, options, callback) { - model.counter++; - callback(); + schema.addHook('1', function($) { + $.model.counter++; + $.callback(); }); - schema.addHook('1', function(error, model, options, callback) { - model.counter++; - callback(); + schema.addHook('1', function($) { + $.model.counter++; + $.callback(); }); schema.hook('1', null, null, function(err, response) { @@ -432,21 +437,28 @@ function test_Schema() { }); }); - NEWSCHEMA('EnumKeyValue').make(function(schema) { + NEWSCHEMA('Special').make(function(schema) { + schema.define('enum_int', [1, 2, 0.3, 4], true); schema.define('enum_string', ['Peter', 'Širka'], true); schema.define('keyvalue', { 'peter': 1, 'lucia': 2 }, true); + schema.define('number', 'Number2'); schema.make({ enum_int: '0.3', 'keyvalue': 'lucia', enum_string: 'Širka' }, function(err, response) { - assert.ok(response.enum_int === 0.3, 'Schema enums (int)'); - assert.ok(response.enum_string === 'Širka', 'Schema enums (int)'); + assert.ok(response.number === null, 'Special schema nullable (number2)'); + assert.ok(response.enum_int === 0.3, 'Special schema (int)'); + assert.ok(response.enum_string === 'Širka', 'Special schema (int)'); assert.ok(response.keyvalue === 2, 'Schema keyvalue'); }); - schema.make({ enum_int: '5', 'keyvalue': 'luciaa', enum_string: 'Širkaa' }, function(err) { - assert.ok(err.items[0].path === 'EnumKeyValue.enum_int', 'Schema enums (int) 2'); - assert.ok(err.items[1].path === 'EnumKeyValue.enum_string', 'Schema enums (string) 2'); - assert.ok(err.items[2].path === 'EnumKeyValue.keyvalue', 'Schema keyvalue 2'); + schema.make({ enum_int: '0.3', 'keyvalue': 'lucia', enum_string: 'Širka', number: '10' }, function(err, response) { + assert.ok(response.number === 10, 'Special schema with number (number2)'); + }); + + schema.make({ enum_int: '5', 'keyvalue': 'luciaa', enum_string: 'Širkaa', number: '10' }, function(err) { + assert.ok(err.items[0].path === 'enum_int', 'Special schema (int) 2'); + assert.ok(err.items[1].path === 'enum_string', 'Special schema (string) 2'); + assert.ok(err.items[2].path === 'keyvalue', 'Schema keyvalue 2'); }); }); @@ -512,42 +524,42 @@ function test_ErrorBuilder() { schema.define('name', String); - schema.addWorkflow('1', function(error, model, options, callback) { + schema.addWorkflow('1', function($) { arr.push('workflow1'); - model.$next('workflow', '2'); - callback(); + $.model.$next('workflow', '2'); + $.callback(); }); - schema.addWorkflow('2', function(error, model, options, callback) { + schema.addWorkflow('2', function($) { arr.push('workflow2'); - callback(); + $.callback(); }); - schema.addWorkflow('3', function(error, model, options, callback) { + schema.addWorkflow('3', function($) { arr.push('workflow3'); - callback(); + $.callback(); }); - schema.addTransform('1', function(error, model, options, callback) { + schema.addTransform('1', function($) { arr.push('transform1'); - model.$next('transform', '2'); - model.$push('transform', '4'); - callback(); + $.model.$next('transform', '2'); + $.model.$push('transform', '4'); + $.callback(); }); - schema.addTransform('2', function(error, model, options, callback) { + schema.addTransform('2', function($) { arr.push('transform2'); - callback(); + $.callback(); }); - schema.addTransform('3', function(error, model, options, callback) { + schema.addTransform('3', function($) { arr.push('transform3'); - callback(); + $.callback(); }); - schema.addTransform('4', function(error, model, options, callback) { + schema.addTransform('4', function($) { arr.push('transform4'); - callback(); + $.callback(); }); var model = schema.create(); @@ -567,13 +579,13 @@ function test_ErrorBuilder() { NEWSCHEMA('Repository').make(function(schema) { - schema.addWorkflow('1', function(error, model, options, callback) { - model.$repository('valid', true); - callback(); + schema.addWorkflow('1', function($) { + $.model.$repository('valid', true); + $.callback(); }); - schema.addWorkflow('2', function(error, model, options, callback) { - callback(); + schema.addWorkflow('2', function($) { + $.callback(); }); var model = schema.create(); @@ -586,17 +598,17 @@ function test_ErrorBuilder() { NEWSCHEMA('Output').make(function(schema) { - schema.addWorkflow('1', function(error, model, options, callback) { - callback(1); + schema.addWorkflow('1', function($) { + $.callback(1); }); - schema.addWorkflow('2', function(error, model, options, callback) { - model.$output(); - callback(2); + schema.addWorkflow('2', function($) { + $.model.$output(); + $.callback(2); }); - schema.addWorkflow('3', function(error, model, options, callback) { - callback(3); + schema.addWorkflow('3', function($) { + $.callback(3); }); var model = schema.create(); @@ -608,15 +620,20 @@ function test_ErrorBuilder() { } +function test_Convertors() { + var a = CONVERT({ page: 5, age: 3, money: '-100', tags: 'Total.js' }, 'page:Number,age:Number, money:Number, tags:[String], empty: Boolean'); + assert.ok(a.page === 5 && a.age === 3 && a.money === -100 && a.tags[0] === 'Total.js' && a.empty === false, 'Problem in convertor'); +} + function test_Operations() { - NEWOPERATION('testA', function(error, value, callback) { - callback(SUCCESS(true, value)); + NEWOPERATION('testA', function($) { + $.callback(SUCCESS(true, $.value)); }); - NEWOPERATION('testB', function(error, value, callback) { - error.push('bug'); - callback(); + NEWOPERATION('testB', function($) { + $.error.push('bug'); + $.callback(); }); OPERATION('testA', 123456, function(err, response) { @@ -628,6 +645,25 @@ function test_Operations() { assert.ok(err.hasError('bug'), 'OPERATIONS: ErrorHandling 1'); assert.ok(response === undefined, 'OPERATIONS: ErrorHandling 2'); }); + + NEWOPERATION('testC', function($) { + assert.ok($.controller === EMPTYCONTROLLER, 'OPERATIONS: Controller 1'); + $.callback(true); + }); + + NEWOPERATION('testD', function($) { + assert.ok($.options.ok === 100, 'OPERATIONS: Custom options + controller'); + assert.ok($.controller === EMPTYCONTROLLER, 'OPERATIONS: Controller 2'); + $.callback(false); + }); + + OPERATION('testC', 1, function(err, response) { + assert.ok(response === true, 'OPERATIONS: controller 1 response'); + }, EMPTYCONTROLLER); + + OPERATION('testD', 2, function(err, response) { + assert.ok(response === false, 'OPERATIONS: controller 2 response'); + }, { ok: 100 }, EMPTYCONTROLLER); } test_PageBuilder(); @@ -635,6 +671,7 @@ test_UrlBuilder(); test_Schema(); test_ErrorBuilder(); test_Operations(); +test_Convertors(); console.log('================================================'); console.log('success - OK'); diff --git a/test/test-css.js b/test/test-css.js index c6e16ff95..89e7e88ae 100755 --- a/test/test-css.js +++ b/test/test-css.js @@ -11,21 +11,19 @@ buffer.push('@keyframes test{border-radius:5px}'); buffer.push('div{background:linear-gradient(90deg, #000000, #FFFFFF);}'); var css = buffer.join('\n'); -assert.ok(internal.compile_css(css) === 'b{border-radius:1px}a{border-radius:1px 2px 3px 4px}a{text-overflow:ellipsis}span{opacity:0;filter:alpha(opacity=0)}@keyframes test{border-radius:5px}@-webkit-keyframes test{border-radius:5px}@-moz-keyframes test{border-radius:5px}@-o-keyframes test{border-radius:5px}div{background:-webkit-linear-gradient(90deg,#000000,#FFFFFF);background:-moz-linear-gradient(90deg,#000000,#FFFFFF);background:-ms-linear-gradient(90deg,#000000,#FFFFFF);background:linear-gradient(90deg,#000000,#FFFFFF)}', 'automated CSS vendor prefixes'); +assert.ok(internal.compile_css(css) === 'b{border-radius:1px}a{border-radius:1px 2px 3px 4px}a{text-overflow:ellipsis}span{opacity:0;filter:alpha(opacity=0)}@keyframes test{border-radius:5px}@-webkit-keyframes test{border-radius:5px}@-moz-keyframes test{border-radius:5px}@-o-keyframes test{border-radius:5px}div{background:-webkit-linear-gradient(90deg,#000,#FFF);background:-moz-linear-gradient(90deg,#000,#FFF);background:-ms-linear-gradient(90deg,#000,#FFF);background:linear-gradient(90deg,#000,#FFF)}', 'automated CSS vendor prefixes'); -// console.log(internal.compile_css('/*auto*/\ndiv{background:repeating-linear-gradient(90deg, #000000, #FFFFFF);}')); - -css = '.input{ }, .input:disabled, .input:hover { background-color: red; } .required{content:"This, field is required"}'; +css = '.input{ }, .input:disabled, .input:hover { background-color: red; } .required{content:"This, field is required"}'; assert.ok(internal.compile_css(css) === '.input{},.input:disabled,.input:hover{background-color:red}.required{content:"This, field is required"}', 'Problem with content.'); buffer = []; buffer.push('$color: red; $font: "Times New Roman";'); buffer.push('$radius: 4px;'); -buffer.push('body { background-color: $color; font-family: $font }'); +buffer.push('body { background-color: $color; font-family: $font !important; }'); buffer.push('div { border-radius: $radius; }'); css = buffer.join('\n'); -assert.ok(internal.compile_css(css) === 'body{background-color:red;font-family:"Times New Roman"}div{border-radius:4px}', 'CSS variables'); +assert.ok(internal.compile_css(css) === 'body{background-color:red;font-family:"Times New Roman"!important}div{border-radius:4px}', 'CSS variables'); buffer = []; buffer.push('@import url(\'font.css\');'); diff --git a/test/test-framework-debug.js b/test/test-framework-debug.js index 8e2d4ad99..e8f80a714 100755 --- a/test/test-framework-debug.js +++ b/test/test-framework-debug.js @@ -5,13 +5,6 @@ var url = 'http://127.0.0.1:8001/'; var errorStatus = 0; var max = 100; -//F.snapshot('/templates/localization.html', '/users/petersirka/desktop/localization.html'); - -// INSTALL('module', 'https://www.totaljs.com/framework/include.js', { test: true }); - -//framework.map('/minify/', '@testpackage', ['.html', 'js']); -//framework.map('/minify/', 'models'); -//framework.map('/minify/', F.path.models()); framework.onCompileView = function(name, html) { return html + 'COMPILED'; }; @@ -29,13 +22,19 @@ framework.on('ready', function() { }); t.on('exit', () => assert.ok(a === true, 'F.load() in worker')); assert.ok(F.config.array.length === 4, 'Problem with config sub types.'); + assert.ok(CONF.testhex === 123456, 'config: hex encode'); + assert.ok(CONF.testbase === 123456, 'config: base encode'); + assert.ok(CONF.testenv === 'custom environment app', 'config: read env'); + assert.ok(CONF.JEBO === 'Z LESA', 'threads: config'); + assert.ok(process.env.APP_ENV === 'staging', '.env: not parsed'); + assert.ok(process.env.DB_HOST2 === 'totallus', '.env-mode: not parsed'); }); -framework.onAuthorize = function(req, res, flags, cb) { - req.user = { alias: 'Peter Širka' }; - req.session = { ready: true }; - cb(req.url !== '/unauthorize/'); -}; +AUTH(function($) { + $.req.user = { alias: 'Peter Širka' }; + $.req.session = { ready: true }; + $.next($.req.url !== '/unauthorize/'); +}); framework.onError = function(error, name, uri) { @@ -65,11 +64,9 @@ function end() { } function test_controller_functions(next) { - utils.request(url, ['get'], function(error, data, code, headers) { + utils.request(url, ['get', 'keepalive'], function(error, data, code, headers) { error && assert.ok(false, 'test_controller_functions: ' + error.toString()); assert.ok(code === 404, 'controller: statusCode ' + code); - assert.ok(headers['etag'] === '1234561', 'controller: setModified(etag)'); - assert.ok(headers['last-modified'].toString().indexOf('1984') !== -1, 'controller: setModified(date)'); next(); }); } @@ -79,7 +76,6 @@ function test_view_functions(next) { if (error) assert.ok(false, 'test_view_functions: ' + error.toString()); - assert.ok(data === '{"r":true}', 'json'); next(); }); @@ -100,6 +96,7 @@ function test_routing(next) { var async = new utils.Async(); + /* async.await('cors 1', function(complete) { utils.request(url + '/cors/origin-all/', ['options'], function(error, data, code, headers) { if (error) @@ -124,7 +121,7 @@ function test_routing(next) { throw error; assert.ok(code === 200, 'CORS, problem with origin (valid origin)'); complete(); - }, null, { 'origin': 'http://www.petersirka.eu' }); + }, null, { 'origin': 'https://www.totajs.com' }); }); async.await('cors asterix / wildcard', function(complete) { @@ -133,7 +130,7 @@ function test_routing(next) { throw error; assert.ok(code === 200, 'CORS, problem with origin (wildcard routing)'); complete(); - }, null, { 'origin': 'http://www.petersirka.eu' }); + }, null, { 'origin': 'https://www.totajs.com' }); }); async.await('cors headers', function(complete) { @@ -143,13 +140,13 @@ function test_routing(next) { // "access-control-allow-origin" doesn't support * (wildcard) when "access-control-allow-credentials" is set to true // node.js doesn't support duplicates headers - assert.ok(headers['access-control-allow-origin'] === 'http://www.petersirka.eu', 'CORS, headers problem 1'); + assert.ok(headers['access-control-allow-origin'] === 'null', 'CORS, headers problem 1'); assert.ok(headers['access-control-allow-credentials'] === 'true', 'CORS, headers problem 2'); assert.ok(headers['access-control-allow-methods'] === 'POST, PUT, DELETE, OPTIONS', 'CORS, headers problem 3'); assert.ok(headers['access-control-allow-headers'] === 'x-ping', 'CORS, headers problem 4'); complete(); - }, null, { 'origin': 'http://www.petersirka.eu' }); - }); + }, null, { 'origin': 'https://www.totajs.com' }); + });*/ async.await('options', function(complete) { utils.request(url + 'options/', ['options'], function(error, data, code, headers) { @@ -173,7 +170,7 @@ function test_routing(next) { utils.request(url + 'html-nocompress/', ['get'], function(error, data, code, headers) { if (error) throw error; - assert(data.indexOf('
\nA\n
') !== -1, 'HTML nocompress'); + assert(data.indexOf('
\nA\n
') !== -1 || data.indexOf('
\r\nA\r\n
') !== -1, 'HTML nocompress'); complete(); }); }); @@ -397,20 +394,29 @@ function test_routing(next) { }); }); - async.await('package/', function(complete) { - utils.request(url + 'package/', ['get'], function(error, data, code, headers) { + async.await('prefix -- 1', function(complete) { + utils.request(url + 'prefix1/test/', ['get'], function(error, data) { if (error) throw error; - assert.ok(data === '
PACKAGELAYOUT
PACKAGEVIEW
', 'package view problem'); + assert.ok(data === 'PREFIX1TEST', 'Group + Prefix 1'); complete(); }); }); - async.await('precompile', function(complete) { - utils.request(url + 'precompile/', ['get'], function(error, data, code, headers) { + async.await('prefix -- 2', function(complete) { + utils.request(url + 'prefix2/test/', ['get'], function(error, data) { + if (error) + throw error; + assert.ok(data === 'PREFIX2TEST', 'Group + Prefix 2'); + complete(); + }); + }); + + async.await('package/', function(complete) { + utils.request(url + 'package/', ['get'], function(error, data, code, headers) { if (error) throw error; - assert.ok(data.indexOf('precompile') === -1, 'framework.precompile() problem'); + assert.ok(data === '
PACKAGELAYOUT
PACKAGEVIEW
', 'package view problem'); complete(); }); }); @@ -479,7 +485,7 @@ function test_routing(next) { utils.request(url + 'schema-filter/', ['post'], 'EMPTY', function(error, data, code, headers) { if (error) throw error; - assert(data === '[{"name":"age","error":"The field \\"age\\" is invalid.","path":"filter.age","prefix":"age"}]', 'schema filter'); + assert(data === '[{"name":"age","error":"The field \\"age\\" is invalid.","path":"age","prefix":"age"}]', 'schema filter'); complete(); }); }); @@ -497,7 +503,7 @@ function test_routing(next) { utils.request(url + 'post/schema/', ['post'], 'age=Peter123456789012345678901234567890#', function(error, data, code, headers) { if (error) throw error; - assert(data === '[{"name":"name","error":"default","path":"User.name","prefix":"name"}]', 'post-schema 2'); + assert(data === '[{"name":"name","error":"default","path":"name","prefix":"name"}]', 'post-schema 2'); complete(); }); }); @@ -628,6 +634,24 @@ function test_routing(next) { }); }); + async.await('component-filename-1', function(complete) { + utils.request(url + '~contactform/a.txt', [], function(error, data, code, headers) { + if (error) + throw error; + assert(code === 200 && data === 'Total.js v3', 'problem with component files 1'); + complete(); + }); + }); + + async.await('component-filename-2', function(complete) { + utils.request(url + '~contactform/b.txt', [], function(error, data, code, headers) { + if (error) + throw error; + assert(code === 200 && data === 'Peter Sirka', 'problem with component files 2'); + complete(); + }); + }); + async.await('static-file-notfound-because-directory1', function(complete) { utils.request(url + 'directory.txt', [], function(error, data, code, headers) { if (error) @@ -736,7 +760,7 @@ function test_routing(next) { utils.request(url + 'merge-blocks-a.js', [], function(error, data, code, headers) { if (error) throw error; - assert(data.indexOf('var common=true;var a=true;') !== -1, 'merge blocks - A'); + assert(data.indexOf('var common=true,a=true;') !== -1, 'merge blocks - A'); complete(); }); }); @@ -745,7 +769,7 @@ function test_routing(next) { utils.request(url + 'merge-blocks-b.js', [], function(error, data, code, headers) { if (error) throw error; - assert(data.indexOf('var common=true;var b=true;') !== -1, 'merge blocks - B'); + assert(data.indexOf('var common=true,b=true;') !== -1, 'merge blocks - B'); complete(); }); }); @@ -754,7 +778,7 @@ function test_routing(next) { utils.request(url + 'blocks-a.js', [], function(error, data, code, headers) { if (error) throw error; - assert(data.indexOf('var common=true;var a=true;') !== -1, 'mapping blocks - A'); + assert(data.indexOf('var common=true,a=true;') !== -1, 'mapping blocks - A'); complete(); }); }); @@ -763,7 +787,7 @@ function test_routing(next) { utils.request(url + 'blocks-b.js', [], function(error, data, code, headers) { if (error) throw error; - assert(data.indexOf('var common=true;var b=true;') !== -1, 'mapping blocks - B'); + assert(data.indexOf('var common=true,b=true;') !== -1, 'mapping blocks - B'); complete(); }); }); @@ -772,7 +796,7 @@ function test_routing(next) { utils.request(url + 'blocks-c.js', [], function(error, data, code, headers) { if (error) throw error; - assert(data.indexOf('var common=true;var a=true;var b=true;') !== -1, 'mapping blocks - C'); + assert(data.indexOf('var common=true,a=true,b=true;') !== -1, 'mapping blocks - C'); complete(); }); }); @@ -787,7 +811,7 @@ function test_routing(next) { }); async.await('theme-green', function(complete) { - utils.request(url + '/green/js/default.js', [], function(error, data, code, headers) { + utils.request(url + 'green/js/default.js', [], function(error, data, code, headers) { if (error) throw error; assert(data === 'var a=1+1;', 'Themes: problem with static files.'); @@ -796,7 +820,7 @@ function test_routing(next) { }); async.await('theme-green-merge', function(complete) { - utils.request(url + '/merge-theme.js', [], function(error, data, code, headers) { + utils.request(url + 'merge-theme.js', [], function(error, data, code, headers) { if (error) throw error; assert(data.indexOf('var a=1+1;') !== -1 && data.indexOf('var b=2+2;'), 'Themes: problem with merging static files'); @@ -805,7 +829,7 @@ function test_routing(next) { }); async.await('theme-green-map', function(complete) { - utils.request(url + '/map-theme.js', [], function(error, data, code, headers) { + utils.request(url + 'map-theme.js', [], function(error, data, code, headers) { if (error) throw error; assert(data === 'var a=1+1;', 'Themes: problem with mapping static files.'); @@ -813,11 +837,65 @@ function test_routing(next) { }); }); - async.await('theme-green', function(complete) { - utils.request(url + 'theme-green/', ['get'], function(error, data, code, headers) { + async.await('dynamic-schema-1', function(complete) { + utils.request(url + 'api/dynamic/orders/', [], function(error, data, code) { + if (error) + throw error; + assert(code === 200 && data === '{"success":true,"value":"orders"}', 'Dynamic schemas 1'); + complete(); + }); + }); + + async.await('dynamic-schema-2', function(complete) { + utils.request(url + 'api/dynamic/users/', [], function(error, data, code) { + if (error) + throw error; + assert(code === 200 && data === '{"success":true,"value":"users"}', 'Dynamic schemas 2'); + complete(); + }); + }); + + async.await('dynamic-schema-3', function(complete) { + utils.request(url + 'api/dynamic/products/', [], function(error, data, code) { + if (error) + throw error; + assert(code === 404, 'Dynamic schemas 3'); + complete(); + }); + }); + + async.await('dynamic-schema-4', function(complete) { + utils.request(url + 'api/dynamic/orders/', ['post', 'json'], { name: 'Total.js' }, function(error, data, code, headers) { + if (error) + throw error; + assert(code === 200 && data === '{"success":true,"value":"orders"}', 'Dynamic schemas 4'); + complete(); + }); + }); + + async.await('static-schema-1', function(complete) { + utils.request(url + 'api/static/orders/', [], function(error, data, code) { if (error) throw error; - console.log('--->', data); + assert(code === 200 && data === '{"success":true,"value":"orders"}', 'static schemas 1'); + complete(); + }); + }); + + async.await('static-schema-2', function(complete) { + utils.request(url + 'api/static/users/', [], function(error, data, code) { + if (error) + throw error; + assert(code === 200 && data === '{"success":true,"value":"users"}', 'static schemas 2'); + complete(); + }); + }); + + async.await('static-schema-3', function(complete) { + utils.request(url + 'api/static/orders/', ['post', 'json'], { name: 'Total.js' }, function(error, data, code, headers) { + if (error) + throw error; + assert(code === 200 && data === '{"success":true,"value":"orders"}', 'Dynamic schemas 3'); complete(); }); }); @@ -842,7 +920,7 @@ function run() { UNINSTALL('source', { uninstall: true }); UNINSTALL('view', 'precompile._layout'); - framework.uninstall('precompile', 'precompile.homepage'); + //framework.uninstall('precompile', 'precompile.homepage'); framework.clear(); setTimeout(function() { @@ -887,6 +965,7 @@ framework.on('load', function() { assert.ok(RESOURCE('default', 'name-root').length > 0, 'custom resource mapping 1'); assert.ok(RESOURCE('default', 'name-theme').length > 0, 'custom resource mapping 2'); assert.ok(F.global.newslettercomponent, 'components: inline '); -var result1 = ''; +var result1 = ''; assert.ok(javascript.compile_javascript(buffer.join('\n')) === result1, 'javascript'); assert.ok(Buffer.from(javascript.compile_javascript(fs.readFileSync('javascript.js').toString('utf8'))).toString('base64') === 'cmV0dXJuJ1xcJysyO3ZhciBhdHRyaWJ1dGVzPSJcXFsiK2ErIiooIitiKyIpKD86IitjKyIqKFsqXiR8IX5dPz0pIitkKyIqKD86JygoPzpcXFxcLnxbXlxcXFwnXSkqKSd8XCIoKD86XFxcXC58W15cXFxcXCJdKSopXCJ8KCIrZSsiKSl8KSIrZisiKlxcXSI7dmFyIGE9MjAwOw==', 'Problem 1'); diff --git a/test/test-tmp.js b/test/test-tmp.js index cf3890e06..717d744ae 100644 --- a/test/test-tmp.js +++ b/test/test-tmp.js @@ -1,6 +1,29 @@ require('../index'); -F.backup(F.path.root('semtu.package'), ['config-debug', 'my-config.txt', '/workers/'], function(err, filename) { - console.log(filename); - F.restore(filename, F.path.root('tmp')); +NEWSCHEMA('Address', function(schema) { + schema.define('countryid', 'Upper(3)', true); + + schema.verify('countryid', function($) { + console.log('1--->', $.value); + $.invalid('error-country'); + //$.next(); + }); + +}); + +NEWSCHEMA('Users', function(schema) { + + schema.define('address', 'Address', true); + schema.define('userid', 'String(20)', true); + + schema.verify('userid', function($) { + console.log('2--->', $.value); + $.next(); + }); + + schema.make({ address: { countryid: 'kokotaris' }, userid: '123456' }, function(err, response) { + console.log(''); + console.log(''); + console.log(err, response); + }); }); \ No newline at end of file diff --git a/test/test-utils.js b/test/test-utils.js index 9fe9598b5..26e811538 100755 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -4,13 +4,15 @@ global.builders = require('../builders'); var assert = require('assert'); var utils = require('../utils'); +process.env.TZ = 'utc'; + // test: date prototype function prototypeDate() { var dt = new Date(1404723152167); - assert.ok(dt.toString() === 'Mon Jul 07 2014 10:52:32 GMT+0200 (CEST)', 'date problem'); - assert.ok(dt.format() === '2014-07-07T10:52:32.167Z', 'date format(0) problem'); - assert.ok(dt.add('minute', 5).toString() === 'Mon Jul 07 2014 10:57:32 GMT+0200 (CEST)', 'date add'); + assert.ok(dt.toUTCString() === 'Mon, 07 Jul 2014 08:52:32 GMT', 'date problem'); + assert.ok(dt.format() === '2014-07-07T08:52:32.167Z', 'date format(0) problem'); + assert.ok(dt.add('minute', 5).toUTCString() === 'Mon, 07 Jul 2014 08:57:32 GMT', 'date add'); assert.ok(dt.format('MMM') === 'Jul', 'month name 1'); assert.ok(dt.format('MMMM') === 'July', 'month name 2'); assert.ok(dt.format('MMM', 'sk') === 'Júl', 'localized month name 1'); @@ -23,10 +25,13 @@ function prototypeDate() { assert.ok('1 minute 5 seconds'.parseDateExpiration().format('mm:ss') === dt.format('mm:ss'), 'date expiration'); dt = '2010-01-01 12:05:10'.parseDate(); - assert.ok('Fri Jan 01 2010 12:05:10 GMT+0100 (CET)' === dt.toString(), 'date parsing 1'); + /* + Because of our time offset :-( + assert.ok('Fri, 01 Jan 2010 12:05:10 GMT' === dt.toUTCString(), 'date parsing 1'); dt = '2010-01-02'.parseDate(); - assert.ok('Sat Jan 02 2010 00:00:00 GMT+0100 (CET)' === dt.toString(), 'date parsing 2'); + assert.ok('Sat, 02 Jan 2010 00:00:00 GMT' === dt.toUTCString(), 'date parsing 2'); + */ dt = '2100-01-01'.parseDate(); assert.ok(dt.compare(new Date()) === 1, 'date compare (earlier)'); @@ -40,7 +45,6 @@ function prototypeDate() { // test: number prototype function prototypeNumber() { - var format = ''; assert.ok((10000).format(2) === '10 000.00', 'format number with decimal parameter'); assert.ok((10000).format(3) === '10 000.000', 'format/decimal: A'); assert.ok((10000).format(3, ',', '.') === '10,000.000', 'format/decimal: B'); @@ -115,9 +119,9 @@ function prototypeString() { assert.ok(' {} '.isJSON() === true, 'string.isJSON({})'); assert.ok('"'.isJSON() === false, 'string.isJSON(")'); assert.ok('""'.isJSON() === true, 'string.isJSON("")'); - assert.ok('12'.isJSON() === false, 'string.isJSON(12)'); + assert.ok('12'.isJSON() === true, 'string.isJSON(12)'); assert.ok('[}'.isJSON() === false, 'string.isJSON([})'); - assert.ok('["'.isJSON() === false, 'string.isJSON([")'); + assert.ok('['.isJSON() === false, 'string.isJSON([)'); assert.ok(str.isJSON() === false, 'string.isJSON()'); assert.ok(JSON.parse(JSON.stringify(new Date())).isJSONDate(), 'string.isJSONDate()'); @@ -141,9 +145,6 @@ function prototypeString() { str = 'https://mail.google.com'; assert.ok(str.isURL() === true, 'string.isURL(): ' + str); - str = 'http://w'; - assert.ok(str.isURL() === false, 'string.isURL(): ' + str); - str = 'petersirka@gmail.com'; assert.ok(str.isEmail() === true, 'string.isEmail(): ' + str); @@ -262,6 +263,8 @@ function prototypeString() { assert.ok('á'.localeCompare2('a') === 1, 'localeCompare2 - 1'); assert.ok('á'.localeCompare2('b') === -1, 'localeCompare2 - 2'); assert.ok('č'.localeCompare2('b') === 1, 'localeCompare2 - 3'); + + assert.ok('Hello {{ what }}!'.arg({ what: 'world' }) === 'Hello world!', 'String.arg()'); } function prototypeArray() { @@ -331,20 +334,13 @@ function prototypeArray() { assert.ok(item.join(',') === '1,2,3', 'arrray.limit(0-3)'); else if (beg === 3 && end === 6) assert.ok(item.join(',') === '4,5,6', 'arrray.limit(3-6)'); - next(); + next(); }); - var arr1 = [{ id: 1, name: 'Peter', age: 25 }, { id: 2, name: 'Lucia', age: 19 }, { id: 3, name: 'Jozef', age: 33 }, { id: 10, name: 'New', age: 39 }]; - var arr2 = [{ id: 2, age: 5, name: 'Lucka' }, { id: 3, name: 'Peter', age: 50 }, { id: 1, name: 'Peter', age: 25 }, { id: 5, name: 'New', age: 33 }]; - - arr1.compare('id', arr2, function(a, b, ai, bi) { - - if (!b) - assert.ok(a.age === 39, 'array.compare(0)'); - - if (!a) - assert.ok(b.age === 33, 'array.compare(1)'); - }); + //var a = [{ id: '1' }, { id: '3' }]; + //var b = [{ id: '5' }, { id: '3' }]; + //var r = U.diff('id', a, b); + //assert.ok(r.add.length === 1 || r.upd.length === 2 || r.rem.length === 1, 'U.diff(a, b)'); arr = [1, 2, 3, 1, 3, 2, 4]; @@ -371,39 +367,6 @@ function prototypeArray() { }); } -function t_callback1(a, cb) { - cb(null, a); -} - -function t_callback2(a, b, cb) { - cb(null, a + b); -} - -function t_callback3(a, b, cb) { - cb(new Error('TEST'), a + b); -} -/* -function harmony() { - - async(function *() { - var a = yield sync(t_callback1)(1); - assert.ok(a === 1, 'harmony t_callback1'); - - var b = yield sync(t_callback2)(1, 1); - assert.ok(b === 2, 'harmony t_callback2'); - - return a + b; - })(function(err, value) { - assert.ok(value === 3, 'harmony callback'); - }); - - async(function *() { - var err = yield sync(t_callback3)(1, 1); - })(function(err, value) { - assert.ok(err.message === 'TEST', 'harmony t_callback3'); - }); -}*/ - function other() { var obj = {}; @@ -424,6 +387,10 @@ function other() { utils.copy({ name: 'A', age: -1 }, obj); assert.ok(obj.name === 'A' && obj.age === -1, 'utils.copy(rewrite=true)'); + assert.ok(U.get(obj, 'arr').join(',') === '1,2,3,4', 'utils.get()'); + U.set(obj, 'address.city', 'Banská Bystrica'); + assert.ok(obj.address.city === 'Banská Bystrica', 'utils.set()'); + var a = utils.reduce(obj, ['name']); var b = utils.reduce(obj, ['name'], true); @@ -572,10 +539,6 @@ function other() { assert.ok(err !== null, 'utils.request (error)'); }); - var resource = function(name) { - return 'resource-' + name; - }; - assert.ok(utils.getName('/aaa/bbb/ccc/dddd') === 'dddd', 'problem with getName (1)'); assert.ok(utils.getName('\\aaa\\bbb\\ccc\\dddd') === 'dddd', 'problem with getName (2)'); assert.ok(utils.getName('/aaa/bbb/ccc/dddd/') === 'dddd', 'problem with getName (3)'); @@ -589,7 +552,7 @@ function other() { assert(err === null, 'utils.wait()'); }); - utils.wait(noop, function(err) { + utils.wait(NOOP, function(err) { assert(err !== null, 'utils.wait() - timeout'); }, 1000); @@ -649,13 +612,67 @@ function other() { var a = { buf: Buffer.from('123456') }; assert.ok(U.clone(a).buf !== a, 'Cloning buffers'); + var input = '12345čťžýáýáííéídfsfgd'; + var a = U.btoa(input); + var b = U.atob(a); + + assert.ok(b === input, 'U.atob() / U.btoa()'); + assert.ok(U.decryptUID(U.encryptUID(100)) === 100, 'U.encryptUID() + U.decryptUID()'); } +function Utils_Ls2_StringFilter() { + var result; + var async = new U.Async(); + + async.await('U.ls2', function(next) { + U.ls2( + './app', + function(files, folders) { + result = {files: files, folders: folders}; + next(); + }, + 'app' + ); + }); + + async.run(function() { + assert.ok(result.files.length === 1, 'problem with number of files from U.ls2 string filter'); + assert.ok(result.files[0].filename.indexOf('virtual.txt') !== -1, 'problem with files[0].filename from U.ls2 string filter'); + assert.ok(result.files[0].stats, 'problem with files[0].stats from U.ls2'); + assert.ok(result.folders.length === 0, 'problem with folders from U.ls2'); + }); +} + +function Utils_Ls_RegExpFilter() { + var result; + var async = new U.Async(); + + async.await('U.ls', function(next) { + U.ls( + './app', + function(files, folders) { + result = {files: files, folders: folders}; + next(); + }, + /QQQ/ + ); + }); + + async.run(function() { + assert.ok(result.files.length === 0, 'problem with files from U.ls regExp filter'); + assert.ok(result.folders.length === 0, 'problem with folders from U.ls regExp filter'); + }); +} + + prototypeDate(); prototypeNumber(); prototypeString(); prototypeArray(); other(); +Utils_Ls_RegExpFilter(); +Utils_Ls2_StringFilter(); + //harmony(); console.log('================================================'); diff --git a/test/threads/users/config b/test/threads/users/config new file mode 100644 index 000000000..afc340a48 --- /dev/null +++ b/test/threads/users/config @@ -0,0 +1 @@ +JEBO : Z LESA \ No newline at end of file diff --git a/test/threads/users/something.js b/test/threads/users/something.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/views/a.html b/test/views/a.html index 984340e26..ab2b5c586 100755 --- a/test/views/a.html +++ b/test/views/a.html @@ -30,16 +30,15 @@ #resource@{resource('name')}# #options-empty@{options(repository.optionsEmpty, 'B')}# #options@{options(R.options, 'C', 'k', 'v')}# -#view-toggle@{viewToggle(false, 'b')}# #view@{view('current/b', 'model')}# -#routejs-@{routeScript('p.js')}# +#routejs-@{public_js('p.js')}# #title@{title}# #mobile@{mobile}# #@{head}# #@{download('test.pdf', 'content', 'test')}# a @{place('footer', 'PLACE')} -#dynamic@{viewCompile('OK')}# +#dynamic@{view_compile('OK')}# - @{js('default.js', '#test')} + @{js('default.js')} @{favicon('favicon.ico')} diff --git a/test/views/params.html b/test/views/params.html index d9fe03ba6..52d79dbdc 100644 --- a/test/views/params.html +++ b/test/views/params.html @@ -1 +1 @@ ---body=@{body.name}--query=@{query.value}--post=@{post.name}--get=@{get.value}-- \ No newline at end of file +--body=@{body.name}--query=@{query.value}-- \ No newline at end of file diff --git a/test/workers/test.js b/test/workers/test.js index e9873dcea..2bbf3b6f3 100644 --- a/test/workers/test.js +++ b/test/workers/test.js @@ -1,6 +1,6 @@ require('../../index'); -F.load(true, ['definitions'], '/Volumes/Development/github/framework/test/'); +F.load(true, ['definitions']); F.on('ready', function() { F.send('assert'); diff --git a/test/workflows b/test/workflows deleted file mode 100644 index 9e97848fe..000000000 --- a/test/workflows +++ /dev/null @@ -1,8 +0,0 @@ -// User -user-query : query (response) -user-read : read (response) -user-delete : delete (response) -user-save : workflow:'check' { hidden: true } --> save (response) -user-create : workflow:'check' --> save (response) --> workflow:'confirmation' - -save (User) : save (response) \ No newline at end of file diff --git a/tools/merge.sh b/tools/merge.sh deleted file mode 100644 index c03a40689..000000000 --- a/tools/merge.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) -cd "$DIR" -cd .. -cd merged -echo "MERGING" -node merge.js -echo "UGLIFY" -node merge.js --minify -echo "DONE" \ No newline at end of file diff --git a/utils.js b/utils.js index fd52eff1a..c384469be 100755 --- a/utils.js +++ b/utils.js @@ -1,4 +1,4 @@ -// Copyright 2012-2018 (c) Peter Širka +// Copyright 2012-2020 (c) Peter Širka // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the @@ -21,7 +21,7 @@ /** * @module FrameworkUtils - * @version 2.9.2 + * @version 3.4.4 */ 'use strict'; @@ -35,6 +35,11 @@ const Path = require('path'); const Fs = require('fs'); const Events = require('events'); const Crypto = require('crypto'); +const Zlib = require('zlib'); +const Tls = require('tls'); +const KeepAlive = new Http.Agent({ keepAlive: true, timeout: 60000 }); + +const COMPRESS = { gzip: 1, deflate: 1 }; const CONCAT = [null, null]; const COMPARER = global.Intl ? global.Intl.Collator().compare : function(a, b) { return a.removeDiacritics().localeCompare(b.removeDiacritics()); @@ -43,26 +48,32 @@ const COMPARER = global.Intl ? global.Intl.Collator().compare : function(a, b) { if (!global.framework_utils) global.framework_utils = exports; +const Internal = require('./internal'); var regexpSTATIC = /\.\w{2,8}($|\?)+/; const regexpTRIM = /^[\s]+|[\s]+$/g; const regexpDATE = /(\d{1,2}\.\d{1,2}\.\d{4})|(\d{4}-\d{1,2}-\d{1,2})|(\d{1,2}:\d{1,2}(:\d{1,2})?)/g; -const regexpDATEFORMAT = /yyyy|yy|M+|d+|HH|H|hh|h|mm|m|ss|s|a|ww|w/g; +const regexpDATEFORMAT = /YYYY|yyyy|YY|yy|MMMM|MMM|MM|M|dddd|DDDD|DDD|ddd|DD|dd|D|d|HH|H|hh|h|mm|m|ss|s|a|ww|w/g; const regexpSTRINGFORMAT = /\{\d+\}/g; const regexpPATH = /\\/g; const regexpTags = /<\/?[^>]+(>|$)/g; const regexpDiacritics = /[^\u0000-\u007e]/g; +const regexpUA = /[a-z]+/gi; const regexpXML = /\w+=".*?"/g; const regexpDECODE = /&#?[a-z0-9]+;/g; const regexpPARAM = /\{{2}[^}\n]*\}{2}/g; -const regexpARG = /\{[^}\n]*\}/g; +const regexpARG = /\{{1,2}[a-z0-9_.-\s]+\}{1,2}/gi; const regexpINTEGER = /(^-|\s-)?[0-9]+/g; const regexpFLOAT = /(^-|\s-)?[0-9.,]+/g; const regexpALPHA = /^[A-Za-z0-9]+$/; const regexpSEARCH = /[^a-zA-Zá-žÁ-Ž\d\s:]/g; -const regexpDECRYPT = /-|_/g; -const regexpENCRYPT = /\/|\+/g; -const regexpUNICODE = /\\u([\d\w]{4})/gi; const regexpTERMINAL = /[\w\S]+/g; +const regexpCONFIGURE = /\[\w+\]/g; +const regexpY = /y/g; +const regexpN = /\n/g; +const regexpCHARS = /\W|_/g; +const regexpCHINA = /[\u3400-\u9FBF]/; +const regexpLINES = /\n|\r|\r\n/; +const regexpBASE64 = /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/; const SOUNDEX = { a: '', e: '', i: '', o: '', u: '', b: 1, f: 1, p: 1, v: 1, c: 2, g: 2, j: 2, k: 2, q: 2, s: 2, x: 2, z: 2, d: 3, t: 3, l: 4, m: 5, n: 5, r: 6 }; const ENCODING = 'utf8'; const NEWLINE = '\r\n'; @@ -71,113 +82,174 @@ const DIACRITICSMAP = {}; const STREAM_READONLY = { flags: 'r' }; const STREAM_END = { end: false }; const ALPHA_INDEX = { '<': '<', '>': '>', '"': '"', '&apos': '\'', '&': '&', '<': '<', '>': '>', '"': '"', ''': '\'', '&': '&' }; -const EMPTYARRAY = []; -const EMPTYOBJECT = []; const NODEVERSION = parseFloat(process.version.toString().replace('v', '').replace(/\./g, '')); const STREAMPIPE = { end: false }; +const CT = 'Content-Type'; +const CRC32TABLE = '00000000,77073096,EE0E612C,990951BA,076DC419,706AF48F,E963A535,9E6495A3,0EDB8832,79DCB8A4,E0D5E91E,97D2D988,09B64C2B,7EB17CBD,E7B82D07,90BF1D91,1DB71064,6AB020F2,F3B97148,84BE41DE,1ADAD47D,6DDDE4EB,F4D4B551,83D385C7,136C9856,646BA8C0,FD62F97A,8A65C9EC,14015C4F,63066CD9,FA0F3D63,8D080DF5,3B6E20C8,4C69105E,D56041E4,A2677172,3C03E4D1,4B04D447,D20D85FD,A50AB56B,35B5A8FA,42B2986C,DBBBC9D6,ACBCF940,32D86CE3,45DF5C75,DCD60DCF,ABD13D59,26D930AC,51DE003A,C8D75180,BFD06116,21B4F4B5,56B3C423,CFBA9599,B8BDA50F,2802B89E,5F058808,C60CD9B2,B10BE924,2F6F7C87,58684C11,C1611DAB,B6662D3D,76DC4190,01DB7106,98D220BC,EFD5102A,71B18589,06B6B51F,9FBFE4A5,E8B8D433,7807C9A2,0F00F934,9609A88E,E10E9818,7F6A0DBB,086D3D2D,91646C97,E6635C01,6B6B51F4,1C6C6162,856530D8,F262004E,6C0695ED,1B01A57B,8208F4C1,F50FC457,65B0D9C6,12B7E950,8BBEB8EA,FCB9887C,62DD1DDF,15DA2D49,8CD37CF3,FBD44C65,4DB26158,3AB551CE,A3BC0074,D4BB30E2,4ADFA541,3DD895D7,A4D1C46D,D3D6F4FB,4369E96A,346ED9FC,AD678846,DA60B8D0,44042D73,33031DE5,AA0A4C5F,DD0D7CC9,5005713C,270241AA,BE0B1010,C90C2086,5768B525,206F85B3,B966D409,CE61E49F,5EDEF90E,29D9C998,B0D09822,C7D7A8B4,59B33D17,2EB40D81,B7BD5C3B,C0BA6CAD,EDB88320,9ABFB3B6,03B6E20C,74B1D29A,EAD54739,9DD277AF,04DB2615,73DC1683,E3630B12,94643B84,0D6D6A3E,7A6A5AA8,E40ECF0B,9309FF9D,0A00AE27,7D079EB1,F00F9344,8708A3D2,1E01F268,6906C2FE,F762575D,806567CB,196C3671,6E6B06E7,FED41B76,89D32BE0,10DA7A5A,67DD4ACC,F9B9DF6F,8EBEEFF9,17B7BE43,60B08ED5,D6D6A3E8,A1D1937E,38D8C2C4,4FDFF252,D1BB67F1,A6BC5767,3FB506DD,48B2364B,D80D2BDA,AF0A1B4C,36034AF6,41047A60,DF60EFC3,A867DF55,316E8EEF,4669BE79,CB61B38C,BC66831A,256FD2A0,5268E236,CC0C7795,BB0B4703,220216B9,5505262F,C5BA3BBE,B2BD0B28,2BB45A92,5CB36A04,C2D7FFA7,B5D0CF31,2CD99E8B,5BDEAE1D,9B64C2B0,EC63F226,756AA39C,026D930A,9C0906A9,EB0E363F,72076785,05005713,95BF4A82,E2B87A14,7BB12BAE,0CB61B38,92D28E9B,E5D5BE0D,7CDCEFB7,0BDBDF21,86D3D2D4,F1D4E242,68DDB3F8,1FDA836E,81BE16CD,F6B9265B,6FB077E1,18B74777,88085AE6,FF0F6A70,66063BCA,11010B5C,8F659EFF,F862AE69,616BFFD3,166CCF45,A00AE278,D70DD2EE,4E048354,3903B3C2,A7672661,D06016F7,4969474D,3E6E77DB,AED16A4A,D9D65ADC,40DF0B66,37D83BF0,A9BCAE53,DEBB9EC5,47B2CF7F,30B5FFE9,BDBDF21C,CABAC28A,53B39330,24B4A3A6,BAD03605,CDD70693,54DE5729,23D967BF,B3667A2E,C4614AB8,5D681B02,2A6F2B94,B40BBE37,C30C8EA1,5A05DF1B,2D02EF8D'.split(',').map(s => parseInt(s, 16)); +const REGISARR = /\[\d+\]|\[\]$/; +const REGREPLACEARR = /\[\]/g; +const PROXYBLACKLIST = { 'localhost': 1, '127.0.0.1': 1, '0.0.0.0': 1 }; +const PROXYOPTIONS = { headers: {}, method: 'CONNECT', agent: false }; +const PROXYTLS = { headers: {}}; +const PROXYOPTIONSHTTP = {}; +const REG_ROOT = /@\{#\}(\/)?/g; +const REG_NOREMAP = /@\{noremap\}(\n)?/g; +const REG_REMAP = /href=".*?"|src=".*?"/gi; +const REG_AJAX = /('|")+(!)?(GET|POST|PUT|DELETE|PATCH)\s(\(.*?\)\s)?\//g; +const REG_URLEXT = /(https|http|wss|ws|file):\/\/|\/\/[a-z0-9]|[a-z]:/i; +const REG_TEXTAPPLICATION = /text|application/i; +const REG_TIME = /am|pm/i; +const REG_XMLKEY = /\[|\]|:|\.|_/g; exports.MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; exports.DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; -Object.freeze(EMPTYARRAY); -Object.freeze(EMPTYOBJECT); - var DIACRITICS=[{b:' ',c:'\u00a0'},{b:'0',c:'\u07c0'},{b:'A',c:'\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'},{b:'AA',c:'\ua732'},{b:'AE',c:'\u00c6\u01fc\u01e2'},{b:'AO',c:'\ua734'},{b:'AU',c:'\ua736'},{b:'AV',c:'\ua738\ua73a'},{b:'AY',c:'\ua73c'},{b:'B',c:'\u24b7\uff22\u1e02\u1e04\u1e06\u0243\u0181'},{b:'C',c:'\u24b8\uff23\ua73e\u1e08\u0106C\u0108\u010a\u010c\u00c7\u0187\u023b'},{b:'D',c:'\u24b9\uff24\u1e0a\u010e\u1e0c\u1e10\u1e12\u1e0e\u0110\u018a\u0189\u1d05\ua779'},{b:'Dh',c:'\u00d0'},{b:'DZ',c:'\u01f1\u01c4'},{b:'Dz',c:'\u01f2\u01c5'},{b:'E',c:'\u025b\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\u1d07'},{b:'F',c:'\ua77c\u24bb\uff26\u1e1e\u0191\ua77b'}, {b:'G',c:'\u24bc\uff27\u01f4\u011c\u1e20\u011e\u0120\u01e6\u0122\u01e4\u0193\ua7a0\ua77d\ua77e\u0262'},{b:'H',c:'\u24bd\uff28\u0124\u1e22\u1e26\u021e\u1e24\u1e28\u1e2a\u0126\u2c67\u2c75\ua78d'},{b:'I',c:'\u24be\uff29\u00cc\u00cd\u00ce\u0128\u012a\u012c\u0130\u00cf\u1e2e\u1ec8\u01cf\u0208\u020a\u1eca\u012e\u1e2c\u0197'},{b:'J',c:'\u24bf\uff2a\u0134\u0248\u0237'},{b:'K',c:'\u24c0\uff2b\u1e30\u01e8\u1e32\u0136\u1e34\u0198\u2c69\ua740\ua742\ua744\ua7a2'},{b:'L',c:'\u24c1\uff2c\u013f\u0139\u013d\u1e36\u1e38\u013b\u1e3c\u1e3a\u0141\u023d\u2c62\u2c60\ua748\ua746\ua780'}, {b:'LJ',c:'\u01c7'},{b:'Lj',c:'\u01c8'},{b:'M',c:'\u24c2\uff2d\u1e3e\u1e40\u1e42\u2c6e\u019c\u03fb'},{b:'N',c:'\ua7a4\u0220\u24c3\uff2e\u01f8\u0143\u00d1\u1e44\u0147\u1e46\u0145\u1e4a\u1e48\u019d\ua790\u1d0e'},{b:'NJ',c:'\u01ca'},{b:'Nj',c:'\u01cb'},{b:'O',c:'\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'}, {b:'OE',c:'\u0152'},{b:'OI',c:'\u01a2'},{b:'OO',c:'\ua74e'},{b:'OU',c:'\u0222'},{b:'P',c:'\u24c5\uff30\u1e54\u1e56\u01a4\u2c63\ua750\ua752\ua754'},{b:'Q',c:'\u24c6\uff31\ua756\ua758\u024a'},{b:'R',c:'\u24c7\uff32\u0154\u1e58\u0158\u0210\u0212\u1e5a\u1e5c\u0156\u1e5e\u024c\u2c64\ua75a\ua7a6\ua782'},{b:'S',c:'\u24c8\uff33\u1e9e\u015a\u1e64\u015c\u1e60\u0160\u1e66\u1e62\u1e68\u0218\u015e\u2c7e\ua7a8\ua784'},{b:'T',c:'\u24c9\uff34\u1e6a\u0164\u1e6c\u021a\u0162\u1e70\u1e6e\u0166\u01ac\u01ae\u023e\ua786'}, {b:'Th',c:'\u00de'},{b:'TZ',c:'\ua728'},{b:'U',c:'\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'},{b:'V',c:'\u24cb\uff36\u1e7c\u1e7e\u01b2\ua75e\u0245'},{b:'VY',c:'\ua760'},{b:'W',c:'\u24cc\uff37\u1e80\u1e82\u0174\u1e86\u1e84\u1e88\u2c72'},{b:'X',c:'\u24cd\uff38\u1e8a\u1e8c'},{b:'Y',c:'\u24ce\uff39\u1ef2\u00dd\u0176\u1ef8\u0232\u1e8e\u0178\u1ef6\u1ef4\u01b3\u024e\u1efe'}, {b:'Z',c:'\u24cf\uff3a\u0179\u1e90\u017b\u017d\u1e92\u1e94\u01b5\u0224\u2c7f\u2c6b\ua762'},{b:'a',c:'\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\u0251'},{b:'aa',c:'\ua733'},{b:'ae',c:'\u00e6\u01fd\u01e3'},{b:'ao',c:'\ua735'},{b:'au',c:'\ua737'},{b:'av',c:'\ua739\ua73b'},{b:'ay',c:'\ua73d'}, {b:'b',c:'\u24d1\uff42\u1e03\u1e05\u1e07\u0180\u0183\u0253\u0182'},{b:'c',c:'\uff43\u24d2\u0107\u0109\u010b\u010d\u00e7\u1e09\u0188\u023c\ua73f\u2184'},{b:'d',c:'\u24d3\uff44\u1e0b\u010f\u1e0d\u1e11\u1e13\u1e0f\u0111\u018c\u0256\u0257\u018b\u13e7\u0501\ua7aa'},{b:'dh',c:'\u00f0'},{b:'dz',c:'\u01f3\u01c6'},{b:'e',c:'\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\u01dd'}, {b:'f',c:'\u24d5\uff46\u1e1f\u0192'},{b:'ff',c:'\ufb00'},{b:'fi',c:'\ufb01'},{b:'fl',c:'\ufb02'},{b:'ffi',c:'\ufb03'},{b:'ffl',c:'\ufb04'},{b:'g',c:'\u24d6\uff47\u01f5\u011d\u1e21\u011f\u0121\u01e7\u0123\u01e5\u0260\ua7a1\ua77f\u1d79'},{b:'h',c:'\u24d7\uff48\u0125\u1e23\u1e27\u021f\u1e25\u1e29\u1e2b\u1e96\u0127\u2c68\u2c76\u0265'},{b:'hv',c:'\u0195'},{b:'i',c:'\u24d8\uff49\u00ec\u00ed\u00ee\u0129\u012b\u012d\u00ef\u1e2f\u1ec9\u01d0\u0209\u020b\u1ecb\u012f\u1e2d\u0268\u0131'}, {b:'j',c:'\u24d9\uff4a\u0135\u01f0\u0249'},{b:'k',c:'\u24da\uff4b\u1e31\u01e9\u1e33\u0137\u1e35\u0199\u2c6a\ua741\ua743\ua745\ua7a3'},{b:'l',c:'\u24db\uff4c\u0140\u013a\u013e\u1e37\u1e39\u013c\u1e3d\u1e3b\u017f\u0142\u019a\u026b\u2c61\ua749\ua781\ua747\u026d'},{b:'lj',c:'\u01c9'},{b:'m',c:'\u24dc\uff4d\u1e3f\u1e41\u1e43\u0271\u026f'},{b:'n',c:'\u24dd\uff4e\u01f9\u0144\u00f1\u1e45\u0148\u1e47\u0146\u1e4b\u1e49\u019e\u0272\u0149\ua791\ua7a5\u043b\u0509'},{b:'nj', c:'\u01cc'},{b:'o',c:'\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\ua74b\ua74d\u0275\u0254\u1d11'},{b:'oe',c:'\u0153'},{b:'oi',c:'\u01a3'},{b:'oo',c:'\ua74f'},{b:'ou',c:'\u0223'},{b:'p',c:'\u24df\uff50\u1e55\u1e57\u01a5\u1d7d\ua751\ua753\ua755\u03c1'},{b:'q',c:'\u24e0\uff51\u024b\ua757\ua759'}, {b:'r',c:'\u24e1\uff52\u0155\u1e59\u0159\u0211\u0213\u1e5b\u1e5d\u0157\u1e5f\u024d\u027d\ua75b\ua7a7\ua783'},{b:'s',c:'\u24e2\uff53\u015b\u1e65\u015d\u1e61\u0161\u1e67\u1e63\u1e69\u0219\u015f\u023f\ua7a9\ua785\u1e9b\u0282'},{b:'ss',c:'\u00df'},{b:'t',c:'\u24e3\uff54\u1e6b\u1e97\u0165\u1e6d\u021b\u0163\u1e71\u1e6f\u0167\u01ad\u0288\u2c66\ua787'},{b:'th',c:'\u00fe'},{b:'tz',c:'\ua729'},{b:'u',c:'\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'}, {b:'v',c:'\u24e5\uff56\u1e7d\u1e7f\u028b\ua75f\u028c'},{b:'vy',c:'\ua761'},{b:'w',c:'\u24e6\uff57\u1e81\u1e83\u0175\u1e87\u1e85\u1e98\u1e89\u2c73'},{b:'x',c:'\u24e7\uff58\u1e8b\u1e8d'},{b:'y',c:'\u24e8\uff59\u1ef3\u00fd\u0177\u1ef9\u0233\u1e8f\u00ff\u1ef7\u1e99\u1ef5\u01b4\u024f\u1eff'},{b:'z',c:'\u24e9\uff5a\u017a\u1e91\u017c\u017e\u1e93\u1e95\u01b6\u0225\u0240\u2c6c\ua763'}]; for (var i=0; i = max_count) + break; + + continue; + } if (word.length < min_length) continue; @@ -371,9 +470,6 @@ exports.keywords = function(content, forSearch, alternative, max_count, max_leng if (counter >= max_count) break; - if (forSearch) - word = word.replace(/\W|_/g, ''); - // Gets 80% length of word if (alternative) { if (isSoundex) @@ -407,6 +503,27 @@ exports.keywords = function(content, forSearch, alternative, max_count, max_leng return keys; }; +function keywordscleaner(c) { + return c.charCodeAt(0) < 200 ? '' : c; +} + +function parseProxy(p) { + var key = 'proxy_' + p; + if (F.temporary.other[key]) + return F.temporary.other[key]; + + if (p.indexOf('://') === -1) + p = 'http://' + p; + + var obj = Url.parse(p); + + if (obj.auth) + obj._auth = 'Basic ' + Buffer.from(obj.auth).toString('base64'); + + obj.port = +obj.port; + return F.temporary.other[key] = obj; +} + /** * Create a request to a specific URL * @param {String} url URL address. @@ -419,7 +536,10 @@ exports.keywords = function(content, forSearch, alternative, max_count, max_leng * @param {Number} timeout Request timeout. * return {Boolean} */ -exports.request = function(url, flags, data, callback, cookies, headers, encoding, timeout, files) { + +const NOBODY = { GET: 1, OPTIONS: 1, HEAD: 1 }; + +global.REQUEST = exports.request = function(url, flags, data, callback, cookies, headers, encoding, timeout, files, param) { // No data (data is optional argument) if (typeof(data) === 'function') { @@ -434,14 +554,20 @@ exports.request = function(url, flags, data, callback, cookies, headers, encodin if (callback === NOOP) callback = null; - var options = { length: 0, timeout: timeout || 10000, evt: new EventEmitter2(), encoding: typeof(encoding) !== 'string' ? ENCODING : encoding, callback: callback, post: false, redirect: 0 }; + if (global.F) + global.F.stats.performance.external++; + + var options = { length: 0, timeout: timeout || CONF.default_restbuilder_timeout, evt: new EventEmitter2(), encoding: typeof(encoding) !== 'string' ? ENCODING : encoding, callback: callback, post: false, redirect: 0 }; var method; var type = 0; var isCookies = false; + var def; + var proxy; - if (headers) + if (headers) { headers = exports.extend({}, headers); - else + def = headers[CT]; + } else headers = {}; if (flags instanceof Array) { @@ -458,7 +584,15 @@ exports.request = function(url, flags, data, callback, cookies, headers, encodin continue; } + if (flags[i][0] === 'p' && flags[i][4] === 'y') { + proxy = parseProxy(flags[i].substring(6)); + continue; + } + switch (flags[i].toLowerCase()) { + case 'insecure': + options.insecure = true; + break; case 'utf8': case 'ascii': case 'base64': @@ -470,22 +604,27 @@ exports.request = function(url, flags, data, callback, cookies, headers, encodin headers['X-Requested-With'] = 'XMLHttpRequest'; break; case 'plain': - headers['Content-Type'] = 'text/plain'; + if (!def) + headers[CT] = 'text/plain'; break; case 'html': - headers['Content-Type'] = 'text/html'; + if (!def) + headers[CT] = 'text/html'; break; case 'raw': type = 3; - headers['Content-Type'] = 'application/octet-stream'; + if (!def) + headers[CT] = 'application/octet-stream'; break; case 'json': - headers['Content-Type'] = 'application/json'; + if (!def) + headers[CT] = 'application/json'; !method && (method = 'POST'); type = 1; break; case 'xml': - headers['Content-Type'] = 'text/xml'; + if (!def) + headers[CT] = 'text/xml'; !method && (method = 'POST'); type = 2; break; @@ -493,7 +632,7 @@ exports.request = function(url, flags, data, callback, cookies, headers, encodin case 'get': case 'options': case 'head': - method = flags[i].toUpperCase(); + method = flags[i].charCodeAt(0) > 96 ? flags[i].toUpperCase() : flags[i]; break; case 'noredirect': @@ -505,7 +644,7 @@ exports.request = function(url, flags, data, callback, cookies, headers, encodin options.upload = true; options.files = files || EMPTYARRAY; options.boundary = '----totaljs' + Math.random().toString(16).substring(2); - headers['Content-Type'] = 'multipart/form-data; boundary=' + options.boundary; + headers[CT] = 'multipart/form-data; boundary=' + options.boundary; break; case 'post': @@ -513,39 +652,60 @@ exports.request = function(url, flags, data, callback, cookies, headers, encodin case 'delete': case 'patch': method = flags[i].toUpperCase(); - !headers['Content-Type'] && (headers['Content-Type'] = 'application/x-www-form-urlencoded'); + !def && !headers[CT] && (headers[CT] = 'application/x-www-form-urlencoded'); break; case 'dnscache': options.resolve = true; break; + case 'keepalive': + options.keepalive = true; + break; + case 'cookies': isCookies = true; break; + default: + + // Fallback for methods (e.g. CalDAV) + if (!method) + method = flags[i].charCodeAt(0) > 96 ? flags[i].toUpperCase() : flags[i]; + + break; } } } if (method) - options.post = method === 'POST' || method === 'PUT' || method === 'DELETE' || method === 'PATCH'; + options.post = !NOBODY[method]; else method = 'GET'; if (type < 3) { + if (typeof(data) !== 'string') data = type === 1 ? JSON.stringify(data) : Qs.stringify(data); else if (data[0] === '?') data = data.substring(1); if (!options.post) { - data.length && url.indexOf('?') === -1 && (url += '?' + data); + if (data.length) { + if (url.indexOf('?') === -1) + url += '?' + data; + else + url += '&' + data; + } data = ''; } + + // "null" or "empty string" is valid JSON value too + if (type === 1 && (data === EMPTYOBJECT || data === undefined) && options.post) + data = BUFEMPTYJSON; } if (data && type !== 4) { - options.data = data instanceof Buffer ? data : exports.createBuffer(data, ENCODING); + options.data = data instanceof Buffer ? data : Buffer.from(data, ENCODING); headers['Content-Length'] = options.data.length; } else options.data = data; @@ -561,74 +721,238 @@ exports.request = function(url, flags, data, callback, cookies, headers, encodin } var uri = Url.parse(url); + + if (!uri.hostname || !uri.host) { + callback && callback(new Error('URL doesn\'t contain a hostname'), '', 0); + return; + } + uri.method = method; - uri.agent = false; uri.headers = headers; + options.uri = uri; - if (options.resolve) { - exports.resolve(url, function(err, u) { - !err && (uri.host = u.host); - request_call(uri, options); - }); - } else + if (options.insecure) { + uri.rejectUnauthorized = false; + uri.requestCert = true; + } + + if (options.resolve && (uri.hostname === 'localhost' || uri.hostname.charCodeAt(0) < 64)) + options.resolve = null; + + if (CONF.default_proxy && !proxy && !PROXYBLACKLIST[uri.hostname]) + proxy = parseProxy(CONF.default_proxy); + + if (proxy && (uri.hostname === 'localhost' || uri.hostname === '127.0.0.1')) + proxy = null; + + options.proxy = proxy; + options.param = param; + + if (proxy && uri.protocol === 'https:') { + proxy.tls = true; + uri.agent = new ProxyAgent(options); + uri.agent.request = Http.request; + uri.agent.createSocket = createSecureSocket; + uri.agent.defaultPort = 443; + } + + if (options.keepalive && !options.proxy && uri.protocol !== 'https:') + uri.agent = KeepAlive; + + if (proxy) + request_call(uri, options); + else if (options.resolve) + exports.resolve(url, request_resolve, options); + else request_call(uri, options); return options.evt; }; +function request_resolve(err, uri, options) { + if (!err) + options.uri.host = uri.host; + request_call(options.uri, options); +} + +function ProxyAgent(options) { + var self = this; + self.options = options; + self.maxSockets = Http.Agent.defaultMaxSockets; + self.requests = []; +} + +const PAP = ProxyAgent.prototype; + +PAP.createConnection = function(pending) { + var self = this; + self.createSocket(pending, function(socket) { + pending.request.onSocket(socket); + }); +}; + +PAP.createSocket = function(options, callback) { + + var self = this; + var proxy = self.options.proxy; + var uri = self.options.uri; + + PROXYOPTIONS.host = proxy.hostname; + PROXYOPTIONS.port = proxy.port; + PROXYOPTIONS.path = PROXYOPTIONS.headers.host = uri.hostname + ':' + (uri.port || '443'); + + if (proxy._auth) + PROXYOPTIONS.headers['Proxy-Authorization'] = proxy._auth; + + var req = self.request(PROXYOPTIONS); + req.setTimeout(10000); + req.on('response', proxyagent_response); + req.on('connect', function(res, socket) { + + if (res.statusCode === 200) { + socket.$req = req; + callback(socket); + } else { + var err = new Error('Proxy could not be established (maybe a problem in auth), code: ' + res.statusCode); + err.code = 'ECONNRESET'; + options.request.emit('error', err); + req.destroy && req.destroy(); + req = null; + self.requests = null; + self.options = null; + } + }); + + req.on('error', function(err) { + var e = new Error('Request Proxy "proxy {0} --> target {1}": {2}'.format(PROXYOPTIONS.host + ':' + proxy.port, PROXYOPTIONS.path, err.toString())); + e.code = err.code; + options.request.emit('error', e); + req.destroy && req.destroy(); + req = null; + self.requests = null; + self.options = null; + }); + + req.end(); +}; + +function proxyagent_response(res) { + res.upgrade = true; +} + +PAP.addRequest = function(req, options) { + this.createConnection({ host: options.host, port: options.port, request: req }); +}; + +function createSecureSocket(options, callback) { + var self = this; + PAP.createSocket.call(self, options, function(socket) { + PROXYTLS.servername = self.options.uri.hostname; + PROXYTLS.headers = self.options.uri.headers; + PROXYTLS.socket = socket; + var tls = Tls.connect(0, PROXYTLS); + callback(tls); + }); +} + function request_call(uri, options) { + var opt; + + if (options.proxy && !options.proxy.tls) { + opt = PROXYOPTIONSHTTP; + opt.port = options.proxy.port; + opt.host = options.proxy.hostname; + opt.path = uri.href; + opt.headers = uri.headers; + opt.method = uri.method; + opt.headers.host = uri.host; + + if (options.insecure) { + opt.rejectUnauthorized = false; + opt.requestCert = true; + } + + if (options.proxy._auth) + opt.headers['Proxy-Authorization'] = options.proxy._auth; + } else + opt = uri; + var connection = uri.protocol === 'https:' ? Https : Http; - var req = options.post ? connection.request(uri, (res) => request_response(res, uri, options)) : connection.get(uri, (res) => request_response(res, uri, options)); + var req = options.post ? connection.request(opt, request_response) : connection.get(opt, request_response); + + req.$options = options; + req.$uri = uri; if (!options.callback) { req.on('error', NOOP); return; } - req.on('error', function(err) { - if (!options.callback) - return; - options.callback(err, '', 0, undefined, uri.host); - options.callback = null; - options.evt.removeAllListeners(); - options.evt = null; - }); - - req.setTimeout(options.timeout, function() { - if (!options.callback) - return; - options.callback(new Error(exports.httpStatus(408)), '', 0, undefined, uri.host); - options.callback = null; - options.evt.removeAllListeners(); - options.evt = null; - }); + req.on('error', request_process_error); + options.timeoutid && clearTimeout(options.timeoutid); + options.timeoutid = setTimeout(request_process_timeout, options.timeout, req); - req.on('response', (response) => response.req = req); + // req.on('response', (response) => response.req = req); + req.on('response', request_assign_res); if (options.upload) { options.first = true; options.files.wait(function(file, next) { - // next(); request_writefile(req, options, file, next); }, function() { - var keys = Object.keys(options.data); for (var i = 0, length = keys.length; i < length; i++) { var value = options.data[keys[i]]; if (value != null) { - req.write((options.first ? '' : NEWLINE) + '--' + options.boundary + NEWLINE + 'Content-Disposition: form-data; name="' + keys[i] + '"' + NEWLINE + NEWLINE + encodeURIComponent(value.toString())); + req.write((options.first ? '' : NEWLINE) + '--' + options.boundary + NEWLINE + 'Content-Disposition: form-data; name="' + keys[i] + '"' + NEWLINE + NEWLINE + value.toString()); if (options.first) options.first = false; } } - req.end(NEWLINE + '--' + options.boundary + '--'); }); } else req.end(options.data); } +function request_process_error(err) { + var options = this.$options; + if (options.callback && !options.done) { + if (options.timeoutid) { + clearTimeout(options.timeoutid); + options.timeoutid = null; + } + options.canceled = true; + options.callback(err, '', 0, undefined, this.$uri.host, EMPTYOBJECT, options.param); + options.callback = null; + options.evt.removeAllListeners(); + options.evt = null; + } +} + +function request_process_timeout(req) { + var options = req.$options; + if (options.callback) { + if (options.timeoutid) { + clearTimeout(options.timeoutid); + options.timeoutid = null; + } + req.socket.destroy(); + req.socket.end(); + req.abort(); + options.canceled = true; + options.callback(new Error(exports.httpStatus(408)), '', 0, undefined, req.$uri.host, EMPTYOBJECT, options.param); + options.callback = null; + options.evt.removeAllListeners(); + options.evt = null; + } +} + +function request_assign_res(response) { + response.req = this; +} + function request_writefile(req, options, file, next) { var type = typeof(file.buffer); @@ -650,8 +974,10 @@ function request_writefile(req, options, file, next) { } } +function request_response(res) { -function request_response(res, uri, options) { + var options = this.$options; + var uri = this.$uri; res._buffer = null; res._bufferlength = 0; @@ -661,8 +987,11 @@ function request_response(res, uri, options) { if (options.noredirect) { + options.timeoutid && clearTimeout(options.timeoutid); + options.canceled = true; + if (options.callback) { - options.callback(null, '', res.statusCode, res.headers, uri.host, EMPTYOBJECT); + options.callback(null, '', res.statusCode, res.headers, uri.host, EMPTYOBJECT, options.param); options.callback = null; } @@ -672,16 +1001,19 @@ function request_response(res, uri, options) { } res.req.removeAllListeners(); - res.req = null; res.removeAllListeners(); + res.req = null; res = null; return; } if (options.redirect > 3) { + options.timeoutid && clearTimeout(options.timeoutid); + options.canceled = true; + if (options.callback) { - options.callback(new Error('Too many redirects.'), '', 0, undefined, uri.host, EMPTYOBJECT); + options.callback(new Error('Too many redirects.'), '', 0, undefined, uri.host, EMPTYOBJECT, options.param); options.callback = null; } @@ -691,29 +1023,45 @@ function request_response(res, uri, options) { } res.req.removeAllListeners(); - res.req = null; res.removeAllListeners(); + res.req = null; res = null; return; } options.redirect++; - var tmp = Url.parse(res.headers['location']); + var loc = res.headers['location']; + var proto = loc.substring(0, 6); + + if (proto !== 'http:/' && proto !== 'https:') + loc = uri.protocol + '//' + uri.hostname + loc; + + var tmp = Url.parse(loc); tmp.headers = uri.headers; - tmp.agent = false; + // tmp.agent = false; tmp.method = uri.method; res.req.removeAllListeners(); res.req = null; + if (options.proxy && tmp.protocol === 'https:') { + // TLS? + options.proxy.tls = true; + options.uri = tmp; + options.uri.agent = new ProxyAgent(options); + options.uri.agent.request = Http.request; + options.uri.agent.createSocket = createSecureSocket; + options.uri.agent.defaultPort = 443; + } + if (!options.resolve) { res.removeAllListeners(); res = null; return request_call(tmp, options); } - exports.resolve(res.headers['location'], function(err, u) { + exports.resolve(tmp, function(err, u) { if (!err) tmp.host = u.host; res.removeAllListeners(); @@ -749,41 +1097,98 @@ function request_response(res, uri, options) { } } - res.on('data', function(chunk) { - var self = this; - if (options.max && self._bufferlength > options.max) - return; - if (self._buffer) { - CONCAT[0] = self._buffer; - CONCAT[1] = chunk; - self._buffer = Buffer.concat(CONCAT); - } else - self._buffer = chunk; - self._bufferlength += chunk.length; - options.evt && options.evt.$events.data && options.evt.emit('data', chunk, options.length ? (self._bufferlength / options.length) * 100 : 0); - }); + if (res.statusCode === 204) { + options.done = true; + request_process_end.call(res); + return; + } - res.on('end', function() { - var self = this; - var str = self._buffer ? self._buffer.toString(options.encoding) : ''; - self._buffer = undefined; + var encoding = res.headers['content-encoding'] || ''; + if (encoding) + encoding = encoding.split(',')[0]; - if (options.evt) { - options.evt.$events.end && options.evt.emit('end', str, self.statusCode, self.headers, uri.host, options.cookies); - options.evt.removeAllListeners(); - options.evt = null; - } + if (COMPRESS[encoding]) { + var zlib = encoding === 'gzip' ? Zlib.createGunzip() : Zlib.createInflate(); + zlib._buffer = res.buffer; + zlib.headers = res.headers; + zlib.statusCode = res.statusCode; + zlib.res = res; + zlib.on('data', request_process_data); + zlib.on('end', request_process_end); + res.pipe(zlib); + } else { + res.on('data', request_process_data); + res.on('end', request_process_end); + } - if (options.callback) { - options.callback(null, uri.method === 'HEAD' ? self.headers : str, self.statusCode, self.headers, uri.host, options.cookies); - options.callback = null; - } + res.resume(); +} + +function request_process_data(chunk) { + var self = this; + + // Is Zlib + if (!self.req) + self = self.res; + + var options = self.req.$options; + if (options.canceled || (options.max && self._bufferlength > options.max)) + return; + if (self._buffer) { + CONCAT[0] = self._buffer; + CONCAT[1] = chunk; + self._buffer = Buffer.concat(CONCAT); + } else + self._buffer = chunk; + self._bufferlength += chunk.length; + options.evt && options.evt.$events.data && options.evt.emit('data', chunk, options.length ? (self._bufferlength / options.length) * 100 : 0); +} + +function request_process_end() { + + var res = this; + + // Is Zlib + if (!res.req) + res = res.res; + var self = res; + var options = self.req.$options; + var uri = self.req.$uri; + var data; + + options.socket && options.uri.agent.destroy(); + options.timeoutid && clearTimeout(options.timeoutid); + + if (options.canceled) + return; + + var ct = self.headers['content-type']; + + if (!ct || REG_TEXTAPPLICATION.test(ct)) + data = self._buffer ? (options.encoding === 'binary' ? self._buffer : self._buffer.toString(options.encoding)) : ''; + else + data = self._buffer; + + options.canceled = true; + + self._buffer = undefined; + + if (options.evt) { + options.evt.$events.end && options.evt.emit('end', data, self.statusCode, self.headers, uri.host, options.cookies, options.param); + options.evt.removeAllListeners(); + options.evt = null; + } + + if (options.callback) { + options.callback(null, uri.method === 'HEAD' ? self.headers : data, self.statusCode, self.headers, uri.host, options.cookies, options.param); + options.callback = null; + } + + if (res.statusCode !== 204) { res.req && res.req.removeAllListeners(); res.removeAllListeners(); - }); - - res.resume(); + } } exports.$$request = function(url, flags, data, cookies, headers, encoding, timeout) { @@ -793,11 +1198,11 @@ exports.$$request = function(url, flags, data, cookies, headers, encoding, timeo }; exports.btoa = function(str) { - return (str instanceof Buffer) ? str.toString('base64') : exports.createBuffer(str.toString(), 'binary').toString('base64'); + return (str instanceof Buffer) ? str.toString('base64') : Buffer.from(str.toString(), 'utf8').toString('base64'); }; exports.atob = function(str) { - return exports.createBuffer(str, 'base64').toString('binary'); + return Buffer.from(str, 'base64').toString('utf8'); }; /** @@ -812,7 +1217,7 @@ exports.atob = function(str) { * @param {Number} timeout Request timeout. * return {Boolean} */ -exports.download = function(url, flags, data, callback, cookies, headers, encoding, timeout) { +exports.download = function(url, flags, data, callback, cookies, headers, encoding, timeout, param) { // No data (data is optional argument) if (typeof(data) === 'function') { @@ -842,8 +1247,8 @@ exports.download = function(url, flags, data, callback, cookies, headers, encodi if (typeof(encoding) !== 'string') encoding = ENCODING; + var proxy, type = 0; var method = 'GET'; - var type = 0; var options = { callback: callback, resolve: false, length: 0, evt: new EventEmitter2(), timeout: timeout || 60000, post: false, encoding: encoding }; if (headers) @@ -868,6 +1273,11 @@ exports.download = function(url, flags, data, callback, cookies, headers, encodi continue; } + if (flags[i][0] === 'p' && flags[i][4] === 'y') { + proxy = parseProxy(flags[i].substring(6)); + continue; + } + switch (flags[i].toLowerCase()) { case 'utf8': @@ -902,7 +1312,7 @@ exports.download = function(url, flags, data, callback, cookies, headers, encodi case 'get': case 'head': case 'options': - method = flags[i].toUpperCase(); + method = flags[i].charCodeAt(0) > 96 ? flags[i].toUpperCase() : flags[i]; break; case 'upload': @@ -913,7 +1323,7 @@ exports.download = function(url, flags, data, callback, cookies, headers, encodi case 'patch': case 'delete': case 'put': - method = flags[i].toUpperCase(); + method = flags[i].charCodeAt(0) > 96 ? flags[i].toUpperCase() : flags[i]; if (!headers['Content-Type']) headers['Content-Type'] = 'application/x-www-form-urlencoded'; break; @@ -921,11 +1331,21 @@ exports.download = function(url, flags, data, callback, cookies, headers, encodi case 'dnscache': options.resolve = true; break; + case 'keepalive': + options.keepalive = true; + break; + default: + // Fallback for methods (e.g. CalDAV) + method = flags[i].charCodeAt(0) > 96 ? flags[i].toUpperCase() : flags[i]; + break; } } } - options.post = method === 'POST' || method === 'PUT' || method === 'DELETE' || method === 'PATCH'; + if (!method) + method = 'GET'; + + options.post = !NOBODY[method]; if (typeof(data) !== 'string') data = type === 1 ? JSON.stringify(data) : Qs.stringify(data); @@ -948,65 +1368,127 @@ exports.download = function(url, flags, data, callback, cookies, headers, encodi var uri = Url.parse(url); uri.method = method; - uri.agent = false; + // uri.agent = false; uri.headers = headers; + options.uri = uri; + options.param = param; + + if (options.resolve && (uri.hostname === 'localhost' || uri.hostname.charCodeAt(0) < 64)) + options.resolve = null; if (data.length) { - options.data = exports.createBuffer(data, ENCODING); + options.data = Buffer.from(data, ENCODING); headers['Content-Length'] = options.data.length; } - if (options.resolve) { - exports.resolve(url, function(err, u) { - !err && (uri.host = u.host); - download_call(uri, options); - }); - } else + if (CONF.default_proxy && !proxy && !PROXYBLACKLIST[uri.hostname]) + proxy = parseProxy(CONF.default_proxy); + + options.proxy = proxy; + + if (proxy && uri.protocol === 'https:') { + proxy.tls = true; + uri.agent = new ProxyAgent(options); + uri.agent.request = Http.request; + uri.agent.createSocket = createSecureSocket; + uri.agent.defaultPort = 443; + } + + if (options.keepalive && !options.proxy && uri.protocol !== 'https:') + uri.agent = KeepAlive; + + if (global.F) + global.F.stats.performance.external++; + + if (proxy) + download_call(uri, options); + else if (options.resolve) + exports.resolve(url, download_resolve, options); + else download_call(uri, options); return options.evt; }; +function download_resolve(err, uri, options) { + if (!err) + options.uri.host = uri.host; + download_call(options.uri, options); +} + function download_call(uri, options) { + var opt; options.length = 0; + if (options.proxy && !options.proxy.tls) { + opt = PROXYOPTIONSHTTP; + opt.port = options.proxy.port; + opt.host = options.proxy.hostname; + opt.path = uri.href; + opt.headers = uri.headers; + opt.method = uri.method; + if (options.proxy._auth) + opt.headers['Proxy-Authorization'] = options.proxy._auth; + } else + opt = uri; + var connection = uri.protocol === 'https:' ? Https : Http; - var req = options.post ? connection.request(uri, (res) => download_response(res, uri, options)) : connection.get(uri, (res) => download_response(res, uri, options)); + var req = options.post ? connection.request(opt, download_response) : connection.get(opt, download_response); + + req.$options = options; + req.$uri = uri; if (!options.callback) { req.on('error', NOOP); return; } - req.on('error', function(err) { - if (!options.callback) - return; - options.callback(err); + req.on('error', download_process_error); + options.timeoutid && clearTimeout(options.timeoutid); + options.timeoutid = setTimeout(download_process_timeout, options.timeout); + req.on('response', download_assign_res); + req.end(options.data); +} + +function download_assign_res(response) { + response.req = this; + var options = this.$options; + options.length = +response.headers['content-length'] || 0; + options.evt && options.evt.$events.begin && options.evt.emit('begin', options.length); +} + +function download_process_timeout(req) { + var options = req.$options; + if (options.callback) { + options.timeoutid && clearTimeout(options.timeoutid); + options.timeoutid = null; + req.abort(); + options.callback(new Error(exports.httpStatus(408)), null, null, null, null, options.param); options.callback = null; options.evt.removeAllListeners(); options.evt = null; - }); + options.canceled = true; + } +} - req.setTimeout(options.timeout, function() { - if (!options.callback) - return; - options.callback(new Error(exports.httpStatus(408))); +function download_process_error(err) { + var options = this.$options; + if (options.callback && !options.done) { + options.timeoutid && clearTimeout(options.timeoutid); + options.timeoutid = null; + options.callback(err, null, null, null, null, options.param); options.callback = null; options.evt.removeAllListeners(); options.evt = null; - }); - - req.on('response', function(response) { - response.req = req; - options.length = +response.headers['content-length'] || 0; - options.evt && options.evt.$events.begin && options.evt.emit('begin', options.length); - }); - - req.end(options.data); + options.canceled = true; + } } -function download_response(res, uri, options) { +function download_response(res) { + + var options = this.$options; + var uri = this.$uri; res._bufferlength = 0; @@ -1014,7 +1496,9 @@ function download_response(res, uri, options) { if (res.statusCode === 301 || res.statusCode === 302) { if (options.redirect > 3) { - options.callback && options.callback(new Error('Too many redirects.')); + options.canceled = true; + options.timeoutid && clearTimeout(options.timeoutid); + options.callback && options.callback(new Error('Too many redirects.'), null, null, null, null, options.param); res.req.removeAllListeners(); res.req = null; res.removeAllListeners(); @@ -1024,20 +1508,33 @@ function download_response(res, uri, options) { options.redirect++; - var tmp = Url.parse(res.headers['location']); + var loc = res.headers['location']; + var proto = loc.substring(0, 6); + + if (proto !== 'http:/' && proto !== 'https:') + loc = uri.protocol + '//' + uri.hostname + loc; + + var tmp = Url.parse(loc); tmp.headers = uri.headers; - tmp.agent = false; + // tmp.agent = false; tmp.method = uri.method; res.req.removeAllListeners(); res.req = null; + if (options.proxy && tmp.protocol === 'https:') { + // TLS? + options.uri = tmp; + download_call(options, request_call); + return; + } + if (!options.resolve) { res.removeAllListeners(); res = null; return download_call(tmp, options); } - exports.resolve(res.headers['location'], function(err, u) { + exports.resolve(loc, function(err, u) { if (!err) tmp.host = u.host; res.removeAllListeners(); @@ -1048,38 +1545,54 @@ function download_response(res, uri, options) { return; } - res.on('data', function(chunk) { - var self = this; - self._bufferlength += chunk.length; - options.evt && options.evt.$events.data && options.evt.emit('data', chunk, options.length ? (self._bufferlength / options.length) * 100 : 0); - }); + res.on('data', download_process_data); + res.on('end', download_process_end); - res.on('end', function() { - var self = this; - var str = self._buffer ? self._buffer.toString(options.encoding) : ''; + res.resume(); + options.timeoutid && clearTimeout(options.timeoutid); + options.callback && options.callback(null, res, res.statusCode, res.headers, uri.host, options.param); +} + +exports.$$download = function(url, flags, data, cookies, headers, encoding, timeout) { + return function(callback) { + exports.download(url, flags, data, callback, cookies, headers, encoding, timeout); + }; +}; + +function download_process_end() { + var res = this; + var self = this; + var options = self.req.$options; + var uri = self.req.$uri; + + if (!options.canceled) { + var str = self._buffer ? self._buffer.toString(options.encoding) : ''; self._buffer = undefined; + options.evt && options.evt.$events.end && options.evt.emit('end', str, self.statusCode, self.headers, uri.host); + } + + if (options.evt) { + options.evt.removeAllListeners(); + options.evt = null; + } + + res.req && res.req.removeAllListeners(); + res.removeAllListeners(); +} +function download_process_data(chunk) { + var self = this; + var options = self.req.$options; + if (!options.canceled) { + self._bufferlength += chunk.length; if (options.evt) { - options.evt.$events.end && options.evt.emit('end', str, self.statusCode, self.headers, uri.host); - options.evt.removeAllListeners(); - options.evt = null; + options.evt.$events.data && options.evt.emit('data', chunk, options.length ? (self._bufferlength / options.length) * 100 : 0); + options.evt.$events.progress && options.evt.emit('progress', options.length ? (self._bufferlength / options.length) * 100 : 0); } - - res.req && res.req.removeAllListeners(); - res.removeAllListeners(); - }); - - res.resume(); - options.callback && options.callback(null, res, res.statusCode, res.headers, uri.host); + } } -exports.$$download = function(url, flags, data, cookies, headers, encoding, timeout) { - return function(callback) { - exports.download(url, flags, data, callback, cookies, headers, encoding, timeout); - }; -}; - /** * Upload a stream through HTTP * @param {String} name Filename with extension. @@ -1116,6 +1629,9 @@ exports.send = function(name, stream, url, callback, cookies, headers, method, t h['Cache-Control'] = 'max-age=0'; h['Content-Type'] = 'multipart/form-data; boundary=' + BOUNDARY; + if (global.F) + global.F.stats.performance.external++; + var e = new EventEmitter2(); var uri = Url.parse(url); var options = { protocol: uri.protocol, auth: uri.auth, method: method || 'POST', hostname: uri.hostname, port: uri.port, path: uri.path, agent: false, headers: h }; @@ -1123,7 +1639,7 @@ exports.send = function(name, stream, url, callback, cookies, headers, method, t var response = function(res) { - res.body = exports.createBufferSize(); + res.body = Buffer.alloc(0); res._bufferlength = 0; res.on('data', function(chunk) { @@ -1208,6 +1724,9 @@ exports.upload = function(files, url, callback, cookies, headers, method, timeou builder && (h['Cookie'] = builder); } + if (global.F) + global.F.stats.performance.external++; + h['Cache-Control'] = 'max-age=0'; h['Content-Type'] = 'multipart/form-data; boundary=' + BOUNDARY; @@ -1215,27 +1734,35 @@ exports.upload = function(files, url, callback, cookies, headers, method, timeou var uri = Url.parse(url); var options = { protocol: uri.protocol, auth: uri.auth, method: method || 'POST', hostname: uri.hostname, port: uri.port, path: uri.path, agent: false, headers: h }; var responseLength = 0; + var timeoutid; + var done = false; var response = function(res) { - res.body = exports.createBufferSize(); + res.body = Buffer.alloc(0); res._bufferlength = 0; res.on('data', function(chunk) { - CONCAT[0] = res.body; - CONCAT[1] = chunk; - res.body = Buffer.concat(CONCAT); - res._bufferlength += chunk.length; - e.$events.data && e.emit('data', chunk, responseLength ? (res._bufferlength / responseLength) * 100 : 0); + if (!done) { + CONCAT[0] = res.body; + CONCAT[1] = chunk; + res.body = Buffer.concat(CONCAT); + res._bufferlength += chunk.length; + e.$events.data && e.emit('data', chunk, responseLength ? (res._bufferlength / responseLength) * 100 : 0); + } }); res.on('end', function() { - var self = this; - e.$events.end && e.emit('end', self.statusCode, self.headers); - e.removeAllListeners(); - e = null; - callback && callback(null, self.body.toString('utf8'), self.statusCode, self.headers, uri.host); - self.body = null; + if (!done) { + var self = this; + e.$events.end && e.emit('end', self.statusCode, self.headers); + e.removeAllListeners(); + callback && callback(null, self.body.toString('utf8'), self.statusCode, self.headers, uri.host); + timeoutid && clearTimeout(timeoutid); + self.body = null; + e = null; + done = true; + } }); }; @@ -1247,20 +1774,31 @@ exports.upload = function(files, url, callback, cookies, headers, method, timeou e.$events.begin && e.emit('begin', responseLength); }); - req.setTimeout(timeout || 60000, function() { - req.removeAllListeners(); - req = null; - e.removeAllListeners(); - e = null; - callback && callback(new Error(exports.httpStatus(408)), '', 408, undefined, uri.host); - }); + var timeoutcallback = function() { + if (!done) { + req.removeAllListeners(); + e.removeAllListeners(); + callback && callback(new Error(exports.httpStatus(408)), '', 408, undefined, uri.host); + timeoutid && clearTimeout(timeoutid); + req = null; + e = null; + done = true; + } + }; + + if (timeout) + timeoutid = setTimeout(timeoutcallback, timeout); + + req.setTimeout(timeout || 60000, timeoutcallback); req.on('error', function(err) { + done = true; req.removeAllListeners(); - req = null; e.removeAllListeners(); - e = null; callback && callback(err, '', 0, undefined, uri.host); + timeoutid && clearTimeout(timeoutid); + req = null; + e = null; }); req.on('close', function() { @@ -1270,7 +1808,7 @@ exports.upload = function(files, url, callback, cookies, headers, method, timeou var header = NEWLINE + NEWLINE + '--' + BOUNDARY + NEWLINE + 'Content-Disposition: form-data; name="{0}"; filename="{1}"' + NEWLINE + 'Content-Type: {2}' + NEWLINE + NEWLINE; - files.waitFor(function(item, next) { + files.wait(function(item, next) { // item.name; // item.filename; @@ -1442,7 +1980,7 @@ global.CLONE = exports.clone = function(obj, skip, skipFunctions) { return obj; var type = typeof(obj); - if (type !== 'object' || obj instanceof Date) + if (type !== 'object' || obj instanceof Date || obj instanceof Error) return obj; var length; @@ -1455,7 +1993,7 @@ global.CLONE = exports.clone = function(obj, skip, skipFunctions) { for (var i = 0; i < length; i++) { type = typeof(obj[i]); - if (type !== 'object' || obj[i] instanceof Date) { + if (type !== 'object' || obj[i] instanceof Date || obj[i] instanceof Error) { if (skipFunctions && type === 'function') continue; o[i] = obj[i]; @@ -1477,14 +2015,14 @@ global.CLONE = exports.clone = function(obj, skip, skipFunctions) { var val = obj[m]; if (val instanceof Buffer) { - var copy = exports.createBufferSize(val.length); + var copy = Buffer.alloc(val.length); val.copy(copy); o[m] = copy; continue; } var type = typeof(val); - if (type !== 'object' || val instanceof Date) { + if (type !== 'object' || val instanceof Date || val instanceof Error) { if (skipFunctions && type === 'function') continue; o[m] = val; @@ -1545,7 +2083,9 @@ exports.reduce = function(source, prop, reverse) { var output = {}; - Object.keys(source).forEach(function(o) { + var keys = Object.keys(source); + for (var i = 0; i < keys.length; i++) { + var o = keys[i]; if (reverse) { if (prop.indexOf(o) === -1) output[o] = source[o]; @@ -1553,7 +2093,7 @@ exports.reduce = function(source, prop, reverse) { if (prop.indexOf(o) !== -1) output[o] = source[o]; } - }); + } return output; }; @@ -1565,6 +2105,7 @@ exports.reduce = function(source, prop, reverse) { * @param {Object or Function} fn Value or Function to update. * @return {Object} */ +// @TODO: deprecated, it will be removed in v4 exports.assign = function(obj, path, fn) { if (obj == null) @@ -1591,11 +2132,11 @@ exports.isRelative = function(url) { /** * Streamer method - * @param {String} beg - * @param {String} end + * @param {String/Buffer} beg + * @param {String/Buffer} end * @param {Function(value, index)} callback */ -exports.streamer = function(beg, end, callback, skip, stream) { +exports.streamer = function(beg, end, callback, skip, stream, raw) { if (typeof(end) === 'function') { stream = skip; @@ -1610,16 +2151,18 @@ exports.streamer = function(beg, end, callback, skip, stream) { } var indexer = 0; - var buffer = exports.createBufferSize(); + var buffer = Buffer.alloc(0); var canceled = false; var fn; if (skip === undefined) skip = 0; - beg = exports.createBuffer(beg, 'utf8'); - if (end) - end = exports.createBuffer(end, 'utf8'); + if (!(beg instanceof Buffer)) + beg = Buffer.from(beg, 'utf8'); + + if (end && !(end instanceof Buffer)) + end = Buffer.from(end, 'utf8'); if (!end) { var length = beg.length; @@ -1630,9 +2173,18 @@ exports.streamer = function(beg, end, callback, skip, stream) { CONCAT[0] = buffer; CONCAT[1] = chunk; + + var f = 0; + + if (buffer.length) { + f = buffer.length - beg.length; + if (f < 0) + f = 0; + } + buffer = Buffer.concat(CONCAT); - var index = buffer.indexOf(beg); + var index = buffer.indexOf(beg, f); if (index === -1) return; @@ -1641,7 +2193,7 @@ exports.streamer = function(beg, end, callback, skip, stream) { if (skip) skip--; else { - if (callback(buffer.toString('utf8', 0, index + length), indexer++) === false) + if (callback(raw ? buffer.slice(0, index + length) : buffer.toString('utf8', 0, index + length), indexer++) === false) canceled = true; } @@ -1675,7 +2227,10 @@ exports.streamer = function(beg, end, callback, skip, stream) { buffer = Buffer.concat(CONCAT); if (!is) { - bi = buffer.indexOf(beg); + var f = CONCAT[0].length - beg.length; + if (f < 0) + f = 0; + bi = buffer.indexOf(beg, f); if (bi === -1) return; is = true; @@ -1692,7 +2247,7 @@ exports.streamer = function(beg, end, callback, skip, stream) { if (skip) skip--; else { - if (callback(buffer.toString('utf8', bi, ei + elength), indexer++) === false) + if (callback(raw ? buffer.slice(bi, ei + elength) : buffer.toString('utf8', bi, ei + elength), indexer++) === false) canceled = true; } @@ -1715,6 +2270,10 @@ exports.streamer = function(beg, end, callback, skip, stream) { return fn; }; +exports.streamer2 = function(beg, end, callback, skip, stream) { + return exports.streamer(beg, end, callback, skip, stream, true); +}; + /** * HTML encode string * @param {String} str @@ -1765,8 +2324,8 @@ exports.isStaticFile = function(url) { * @return {Number} */ exports.parseInt = function(obj, def) { - if (obj == null) - return def || 0; + if (obj == null || obj === '') + return def === undefined ? 0 : def; var type = typeof(obj); return type === 'number' ? obj : (type !== 'string' ? obj.toString() : obj).parseInt(def); }; @@ -1785,8 +2344,8 @@ exports.parseBool = exports.parseBoolean = function(obj, def) { * @return {Number} */ exports.parseFloat = function(obj, def) { - if (obj == null) - return def || 0; + if (obj == null || obj === '') + return def === undefined ? 0 : def; var type = typeof(obj); return type === 'number' ? obj : (type !== 'string' ? obj.toString() : obj).parseFloat(def); }; @@ -1858,7 +2417,7 @@ exports.getContentType = function(ext) { */ exports.getExtension = function(filename, raw) { var end = filename.length; - for (var i = filename.length; i > 1; i--) { + for (var i = filename.length - 1; i > 0; i--) { var c = filename[i]; if (c === ' ' || c === '?') end = i; @@ -1953,7 +2512,7 @@ function rnd() { return Math.floor(Math.random() * 65536).toString(36); } -exports.GUID = function(max) { +global.GUID = exports.GUID = function(max) { max = max || 40; var str = ''; for (var i = 0; i < (max / 3) + 1; i++) @@ -1965,14 +2524,19 @@ function validate_builder_default(name, value, entity) { var type = typeof(value); - // Enum + KeyValue (8+9) + if (entity.type === 12) + return value != null && type === 'object' && !(value instanceof Array); + + if (entity.type === 11) + return type === 'number'; + + // Enum + KeyValue + Custom (8+9+10) if (entity.type > 7) return value !== undefined; switch (entity.subtype) { case 'uid': - var number = parseInt(value.substring(10, value.length - 4), 10); - return isNaN(number) ? false : value[value.length - 1] === (number % 2 ? '1' : '0'); + return value.isUID(); case 'zip': return value.isZIP(); case 'email': @@ -1983,6 +2547,8 @@ function validate_builder_default(name, value, entity) { return value.isURL(); case 'phone': return value.isPhone(); + case 'base64': + return value.isBase64(); } if (type === 'number') @@ -2003,12 +2569,12 @@ function validate_builder_default(name, value, entity) { return true; } -exports.validate_builder = function(model, error, schema, collection, path, index, fields, pluspath) { +exports.validate_builder = function(model, error, schema, path, index, fields, pluspath) { - var entity = collection[schema]; - var prepare = entity.onValidate || F.onValidate || NOOP; - var current = path === undefined ? '' : path + '.'; - var properties = entity.properties; + var prepare = schema.onValidate || F.onValidate || NOOP; + var current = path ? path + '.' : ''; + var properties = model && model.$$keys ? model.$$keys : schema.properties; + var result; if (!pluspath) pluspath = ''; @@ -2019,17 +2585,20 @@ exports.validate_builder = function(model, error, schema, collection, path, inde for (var i = 0; i < properties.length; i++) { var name = properties[i]; + if (fields && fields.indexOf(name) === -1) continue; - var TYPE = collection[schema].schema[name]; + var TYPE = schema.schema[name]; + if (!TYPE) + continue; - if (TYPE.can && !TYPE.can(model)) + if (TYPE.can && !TYPE.can(model, model.$$workflow || EMPTYOBJECT)) continue; var value = model[name]; var type = typeof(value); - var prefix = entity.resourcePrefix ? (entity.resourcePrefix + name) : name; + var prefix = schema.resourcePrefix ? (schema.resourcePrefix + name) : name; if (value === undefined) { error.push(pluspath + name, '@', current + name, undefined, prefix); @@ -2038,49 +2607,75 @@ exports.validate_builder = function(model, error, schema, collection, path, inde value = model[name](); if (TYPE.isArray) { + if (TYPE.type === 7 && value instanceof Array && value.length) { + var nestedschema = schema.parent.collection[TYPE.raw] || GETSCHEMA(TYPE.raw); + if (nestedschema) { + for (var j = 0, jl = value.length; j < jl; j++) + exports.validate_builder(value[j], error, nestedschema, current + name + '[' + j + ']', j, undefined, pluspath); + } else + throw new Error('Nested schema "{0}" not found in "{1}".'.format(TYPE.raw, schema.parent.name)); + } else { - if (!(value instanceof Array) || !value.length) { - error.push(pluspath + name, '@', current + name, index, prefix); - continue; - } + if (!TYPE.required) + continue; - for (var j = 0, jl = value.length; j < jl; j++) { - if (TYPE.type === 7) { - // Another schema - exports.validate_builder(value[j], error, TYPE.raw, collection, current + name + '[' + j + ']', j, undefined, pluspath); - } else { - // Basic types - var result = TYPE.validate ? TYPE.validate(value[j], model) : prepare(name, value, current + name + '[' + j + ']', model, schema, TYPE); - if (result === undefined) { - result = validate_builder_default(name, value[j], TYPE); - if (result) - continue; - type = typeof(result); - if (type === 'string') { - if (result[0] === '@') - error.push(pluspath + name, '@', current + name + '[' + j + ']', j, entity.resourcePrefix + result.substring(1)); - else - error.push(pluspath + name, result, current + name + '[' + j + ']', j, prefix); - } else if (type === 'boolean') { - !result && error.push(pluspath + name, '@', current + name + '[' + j + ']', j, prefix); - } else if (result.isValid === false) - error.push(pluspath + name, result.error, current + name + '[' + j + ']', j, prefix); - } + result = TYPE.validate ? TYPE.validate(value, model) : prepare(name, value, current + name, model, schema.name, TYPE); + if (result == null) { + result = value instanceof Array ? value.length > 0 : false; + if (result == null || result === true) + continue; } + + type = typeof(result); + if (type === 'string') { + if (result[0] === '@') + error.push(pluspath + name, '@', current + name, index, schema.resourcePrefix + result.substring(1)); + else + error.push(pluspath + name, result, current + name, index, prefix); + } else if (type === 'boolean') { + !result && error.push(pluspath + name, '@', current + name, index, prefix); + } else if (result.isValid === false) + error.push(pluspath + name, result.error, current + name, index, prefix); } continue; } if (TYPE.type === 7) { + + if (!value && !TYPE.required) + continue; + // Another schema - exports.validate_builder(value, error, TYPE.raw, collection, current + name, undefined, undefined, pluspath); + result = TYPE.validate ? TYPE.validate(value, model) : null; + + if (result == null) { + var nestedschema = schema.parent.collection[TYPE.raw] || GETSCHEMA(TYPE.raw); + if (nestedschema) + exports.validate_builder(value, error, nestedschema, current + name, undefined, undefined, pluspath); + else + throw new Error('Nested schema "{0}" not found in "{1}".'.format(TYPE.raw, schema.parent.name)); + } else { + type = typeof(result); + if (type === 'string') { + if (result[0] === '@') + error.push(pluspath + name, '@', current + name, index, schema.resourcePrefix + result.substring(1)); + else + error.push(pluspath + name, result, current + name, index, prefix); + } else if (type === 'boolean') { + !result && error.push(pluspath + name, '@', current + name, index, prefix); + } else if (result.isValid === false) + error.push(pluspath + name, result.error, current + name, index, prefix); + } continue; } - var result = TYPE.validate ? TYPE.validate(value, model) : prepare(name, value, current + name, model, schema, TYPE); - if (result === undefined) { + if (!TYPE.required) + continue; + + result = TYPE.validate ? TYPE.validate(value, model) : prepare(name, value, current + name, model, schema.name, TYPE); + if (result == null) { result = validate_builder_default(name, value, TYPE); - if (result) + if (result == null || result === true) continue; } @@ -2088,7 +2683,7 @@ exports.validate_builder = function(model, error, schema, collection, path, inde if (type === 'string') { if (result[0] === '@') - error.push(pluspath + name, '@', current + name, index, entity.resourcePrefix + result.substring(1)); + error.push(pluspath + name, '@', current + name, index, schema.resourcePrefix + result.substring(1)); else error.push(pluspath + name, result, current + name, index, prefix); } else if (type === 'boolean') { @@ -2137,7 +2732,7 @@ exports.removeDiacritics = function(str) { * @param {String} xml * @return {Object} */ -exports.parseXML = function(xml) { +exports.parseXML = function(xml, replace) { var beg = -1; var end = 0; @@ -2181,6 +2776,9 @@ exports.parseXML = function(xml) { var path = (current.length ? current.join('.') + '.' : '') + o; var value = xml.substring(from, beg).decode(); + if (replace) + path = path.replace(REG_XMLKEY, '_'); + if (obj[path] === undefined) obj[path] = value; else if (obj[path] instanceof Array) @@ -2223,7 +2821,10 @@ exports.parseXML = function(xml) { attr[match[i].substring(0, index - 1)] = match[i].substring(index + 1, match[i].length - 1).decode(); } - obj[current.join('.') + (isSingle ? '.' + name : '') + '[]'] = attr; + var k = current.join('.') + (isSingle ? '.' + name : '') + '[]'; + if (replace) + k = k.replace(REG_XMLKEY, '_'); + obj[k] = attr; } return obj; @@ -2233,7 +2834,6 @@ exports.parseJSON = function(value, date) { try { return JSON.parse(value, date ? jsonparser : undefined); } catch(e) { - return null; } }; @@ -2256,7 +2856,7 @@ function jsonparser(key, value) { exports.getWebSocketFrame = function(code, message, type, compress) { var messageBuffer = getWebSocketFrameMessageBytes(code, message); var lengthBuffer = getWebSocketFrameLengthBytes(messageBuffer.length); - var frameBuffer = exports.createBufferSize(1 + lengthBuffer.length + messageBuffer.length); + var frameBuffer = Buffer.alloc(1 + lengthBuffer.length + messageBuffer.length); frameBuffer[0] = 0x80 | type; compress && (frameBuffer[0] |= 0x40); lengthBuffer.copy(frameBuffer, 1, 0, lengthBuffer.length); @@ -2277,7 +2877,7 @@ function getWebSocketFrameMessageBytes(code, message) { var binary = message instanceof Int8Array || message instanceof Buffer; var length = message.length; - var messageBuffer = exports.createBufferSize(length + index); + var messageBuffer = Buffer.alloc(length + index); for (var i = 0; i < length; i++) { if (binary) @@ -2304,20 +2904,20 @@ function getWebSocketFrameLengthBytes(length) { var lengthBuffer = null; if (length <= 125) { - lengthBuffer = exports.createBufferSize(1); + lengthBuffer = Buffer.alloc(1); lengthBuffer[0] = length; return lengthBuffer; } if (length <= 65535) { - lengthBuffer = exports.createBufferSize(3); + lengthBuffer = Buffer.alloc(3); lengthBuffer[0] = 126; lengthBuffer[1] = (length >> 8) & 255; lengthBuffer[2] = (length) & 255; return lengthBuffer; } - lengthBuffer = exports.createBufferSize(9); + lengthBuffer = Buffer.alloc(9); lengthBuffer[0] = 127; lengthBuffer[1] = 0x00; @@ -2349,65 +2949,55 @@ exports.distance = function(lat1, lon1, lat2, lon2) { return (R * c).floor(3); }; -/** - * Directory listing - * @param {String} path Path. - * @param {Function(files, directories)} callback Callback - * @param {Function(filename),isDirectory or String or RegExp} filter Custom filter (optional). - */ -exports.ls = function(path, callback, filter) { - +function ls(path, callback, advanced, filter) { var filelist = new FileList(); var tmp; + filelist.advanced = advanced; filelist.onComplete = callback; if (typeof(filter) === 'string') { tmp = filter.toLowerCase(); - filter.onFilter = function(filename, is, relative) { - return is ? true : relative.toLowerCase().indexOf(tmp); + filelist.onFilter = function(filename, is) { + return is ? true : filename.toLowerCase().indexOf(tmp) !== -1; }; } else if (exports.isRegExp(filter)) { tmp = filter; - filter.onFilter = function(filename, is) { + filelist.onFilter = function(filename, is) { return is ? true : tmp.test(filename); }; } else filelist.onFilter = filter || null; filelist.walk(path); +} + +/** + * Directory listing + * @param {String} path Path. + * @param {Function(files, directories)} callback Callback + * @param {Function(filename, isDirectory) or String or RegExp} filter Custom filter (optional). + */ +exports.ls = function(path, callback, filter) { + ls(path, callback, false, filter); }; /** * Advanced Directory listing * @param {String} path Path. * @param {Function(files, directories)} callback Callback - * @param {Function(filename),isDirectory or String or RegExp} filter Custom filter (optional). + * @param {Function(filename ,isDirectory) or String or RegExp} filter Custom filter (optional). */ exports.ls2 = function(path, callback, filter) { - var filelist = new FileList(); - var tmp; - - filelist.advanced = true; - filelist.onComplete = callback; - - if (typeof(filter) === 'string') { - tmp = filter.toLowerCase(); - filter.onFilter = function(filename, is) { - return is ? true : filename.toLowerCase().indexOf(tmp); - }; - } else if (exports.isRegExp(filter)) { - tmp = filter; - filter.onFilter = function(filename, is) { - return is ? true : tmp.test(filename); - }; - } else - filelist.onFilter = filter || null; + ls(path, callback, true, filter); +}; - filelist.walk(path); +DP.setTimeZone = function(timezone) { + var dt = this.toLocaleString('en-US', { timeZone: timezone, hour12: false, dateStyle: 'short', timeStyle: 'short' }); + return new Date(Date.parse(dt)); }; -Date.prototype.add = function(type, value) { +DP.add = function(type, value) { var self = this; @@ -2428,44 +3018,44 @@ Date.prototype.add = function(type, value) { case 'sec': case 'second': case 'seconds': - dt.setSeconds(dt.getSeconds() + value); + dt.setUTCSeconds(dt.getUTCSeconds() + value); return dt; case 'm': case 'mm': case 'minute': case 'min': case 'minutes': - dt.setMinutes(dt.getMinutes() + value); + dt.setUTCMinutes(dt.getUTCMinutes() + value); return dt; case 'h': case 'hh': case 'hour': case 'hours': - dt.setHours(dt.getHours() + value); + dt.setUTCHours(dt.getUTCHours() + value); return dt; case 'd': case 'dd': case 'day': case 'days': - dt.setDate(dt.getDate() + value); + dt.setUTCDate(dt.getUTCDate() + value); return dt; case 'w': case 'ww': case 'week': case 'weeks': - dt.setDate(dt.getDate() + (value * 7)); + dt.setUTCDate(dt.getUTCDate() + (value * 7)); return dt; case 'M': case 'MM': case 'month': case 'months': - dt.setMonth(dt.getMonth() + value); + dt.setUTCMonth(dt.getUTCMonth() + value); return dt; case 'y': case 'yyyy': case 'year': case 'years': - dt.setFullYear(dt.getFullYear() + value); + dt.setUTCFullYear(dt.getUTCFullYear() + value); return dt; } return dt; @@ -2477,7 +3067,7 @@ Date.prototype.add = function(type, value) { * @param {String} type Date type: minutes, seconds, hours, days, months, years * @return {Number} */ -Date.prototype.diff = function(date, type) { +DP.diff = function(date, type) { if (arguments.length === 1) { type = date; @@ -2531,7 +3121,7 @@ Date.prototype.diff = function(date, type) { return NaN; }; -Date.prototype.extend = function(date) { +DP.extend = function(date) { var dt = new Date(this); var match = date.match(regexpDATE); @@ -2546,16 +3136,16 @@ Date.prototype.extend = function(date) { arr = m.split(':'); tmp = +arr[0]; - !isNaN(tmp) && dt.setHours(tmp); + tmp >= 0 && dt.setUTCHours(tmp); if (arr[1]) { tmp = +arr[1]; - !isNaN(tmp) && dt.setMinutes(tmp); + tmp >= 0 && dt.setUTCMinutes(tmp); } if (arr[2]) { tmp = +arr[2]; - !isNaN(tmp) && dt.setSeconds(tmp); + tmp >= 0 && dt.setUTCSeconds(tmp); } continue; @@ -2565,16 +3155,16 @@ Date.prototype.extend = function(date) { arr = m.split('-'); tmp = +arr[0]; - dt.setFullYear(tmp); - - if (arr[2]) { - tmp = +arr[2]; - !isNaN(tmp) && dt.setDate(tmp); - } + tmp && dt.setUTCFullYear(tmp); if (arr[1]) { tmp = +arr[1]; - !isNaN(tmp) && dt.setMonth(tmp - 1); + tmp >= 0 && dt.setUTCMonth(tmp - 1); + } + + if (arr[2]) { + tmp = +arr[2]; + tmp >= 0 && dt.setUTCDate(tmp); } continue; @@ -2583,18 +3173,18 @@ Date.prototype.extend = function(date) { if (m.indexOf('.') !== -1) { arr = m.split('.'); - tmp = +arr[0]; - dt.setDate(tmp); + if (arr[2]) { + tmp = +arr[2]; + !isNaN(tmp) && dt.setUTCFullYear(tmp); + } if (arr[1]) { tmp = +arr[1]; - !isNaN(tmp) && dt.setMonth(tmp - 1); + !isNaN(tmp) && dt.setUTCMonth(tmp - 1); } - if (arr[2]) { - tmp = +arr[2]; - !isNaN(tmp) && dt.setFullYear(tmp); - } + tmp = +arr[0]; + !isNaN(tmp) && dt.setUTCDate(tmp); continue; } @@ -2608,7 +3198,7 @@ Date.prototype.extend = function(date) { * @param {Date} date * @return {Number} Results: -1 = current date is earlier than @date, 0 = current date is same as @date, 1 = current date is later than @date */ -Date.prototype.compare = function(date) { +DP.compare = function(date) { var self = this; var r = self.getTime() - date.getTime(); @@ -2644,10 +3234,10 @@ Date.compare = function(d1, d2) { * @param {String} format * @return {String} */ -Date.prototype.format = function(format, resource) { +DP.format = function(format, resource) { if (!format) - return this.getFullYear() + '-' + (this.getMonth() + 1).toString().padLeft(2, '0') + '-' + this.getDate().toString().padLeft(2, '0') + 'T' + this.getHours().toString().padLeft(2, '0') + ':' + this.getMinutes().toString().padLeft(2, '0') + ':' + this.getSeconds().toString().padLeft(2, '0') + '.' + this.getMilliseconds().toString().padLeft(3, '0') + 'Z'; + return this.getUTCFullYear() + '-' + (this.getUTCMonth() + 1).toString().padLeft(2, '0') + '-' + this.getUTCDate().toString().padLeft(2, '0') + 'T' + this.getUTCHours().toString().padLeft(2, '0') + ':' + this.getUTCMinutes().toString().padLeft(2, '0') + ':' + this.getUTCSeconds().toString().padLeft(2, '0') + '.' + this.getUTCMilliseconds().toString().padLeft(3, '0') + 'Z'; if (datetimeformat[format]) return datetimeformat[format](this, resource); @@ -2671,8 +3261,10 @@ Date.prototype.format = function(format, resource) { format = format.replace(regexpDATEFORMAT, function(key) { switch (key) { case 'yyyy': + case 'YYYY': return beg + 'd.getFullYear()' + end; case 'yy': + case 'YY': return beg + 'd.getFullYear().toString().substring(2)' + end; case 'MMM': ismm = true; @@ -2685,14 +3277,18 @@ Date.prototype.format = function(format, resource) { case 'M': return beg + '(d.getMonth() + 1)' + end; case 'ddd': + case 'DDD': isdd = true; return beg + '(F.resource(resource, dd) || dd).substring(0, 2).toUpperCase()' + end; case 'dddd': + case 'DDDD': isdd = true; return beg + '(F.resource(resource, dd) || dd)' + end; case 'dd': + case 'DD': return beg + 'd.getDate().toString().padLeft(2, \'0\')' + end; case 'd': + case 'D': return beg + 'd.getDate()' + end; case 'HH': case 'hh': @@ -2720,7 +3316,7 @@ Date.prototype.format = function(format, resource) { ismm && before.push('var mm = framework_utils.MONTHS[d.getMonth()];'); isdd && before.push('var dd = framework_utils.DAYS[d.getDay()];'); - isww && before.push('var ww = new Date(+d);ww.setHours(0, 0, 0);ww.setDate(ww.getDate() + 4 - (ww.getDay() || 7));ww = Math.ceil((((ww - new Date(ww.getFullYear(), 0, 1)) / 8.64e7) + 1) / 7);'); + isww && before.push('var ww = new Date(+d);ww.setHours(0,0,0,0);ww.setDate(ww.getDate()+3-(ww.getDay()+6)%7);var ww1=new Date(ww.getFullYear(),0,4);ww=1+Math.round(((ww.getTime()-ww1.getTime())/86400000-3+(ww1.getDay()+6)%7)/7);'); datetimeformat[key] = new Function('d', 'resource', before.join('\n') + 'return \'' + format + '\';'); return datetimeformat[key](this, resource); @@ -2730,41 +3326,70 @@ exports.$pmam = function(value) { return value >= 12 ? value - 12 : value; }; -Date.prototype.toUTC = function(ticks) { +DP.toUTC = function(ticks) { var dt = this.getTime() + this.getTimezoneOffset() * 60000; return ticks ? dt : new Date(dt); }; // +v2.2.0 parses JSON dates as dates and this is the fallback for backward compatibility -Date.prototype.parseDate = function() { +DP.parseDate = function() { return this; }; -String.prototype.isJSONDate = function() { +SP.isJSONDate = function() { var l = this.length - 1; return l > 22 && l < 30 && this[l] === 'Z' && this[10] === 'T' && this[4] === '-' && this[13] === ':' && this[16] === ':'; }; -if (!String.prototype.trim) { - String.prototype.trim = function() { +SP.ROOT = function(noremap) { + + var str = this; + + str = str.replace(REG_NOREMAP, function() { + noremap = true; + return ''; + }).replace(REG_ROOT, $urlmaker); + + if (!noremap && CONF.default_root) + str = str.replace(REG_REMAP, $urlremap).replace(REG_AJAX, $urlajax); + + return str; +}; + +function $urlremap(text) { + var pos = text[0] === 'h' ? 6 : 5; + return REG_URLEXT.test(text) ? text : ((text[0] === 'h' ? 'href' : 'src') + '="' + CONF.default_root + (text[pos] === '/' ? text.substring(pos + 1) : text)); +} + +function $urlajax(text) { + return text.substring(0, text.length - 1) + CONF.default_root; +} + +function $urlmaker(text) { + var c = text[4]; + return CONF.default_root ? CONF.default_root : (c || ''); +} + +if (!SP.trim) { + SP.trim = function() { return this.replace(regexpTRIM, ''); }; } -if (!String.prototype.replaceAt) { - String.prototype.replaceAt = function(index, character) { +if (!SP.replaceAt) { + SP.replaceAt = function(index, character) { return this.substr(0, index) + character + this.substr(index + character.length); }; } /** * Checks if the string starts with the text - * @see {@link http://docs.totaljs.com/String.prototype/#String.prototype.startsWith|Documentation} + * @see {@link http://docs.totaljs.com/SP/#SP.startsWith|Documentation} * @param {String} text Text to find. * @param {Boolean/Number} ignoreCase Ingore case sensitive or position in the string. * @return {Boolean} */ -String.prototype.startsWith = function(text, ignoreCase) { +SP.startsWith = function(text, ignoreCase) { var self = this; var length = text.length; var tmp; @@ -2784,12 +3409,12 @@ String.prototype.startsWith = function(text, ignoreCase) { /** * Checks if the string ends with the text - * @see {@link http://docs.totaljs.com/String.prototype/#String.prototype.endsWith|Documentation} + * @see {@link http://docs.totaljs.com/SP/#SP.endsWith|Documentation} * @param {String} text Text to find. * @param {Boolean/Number} ignoreCase Ingore case sensitive or position in the string. * @return {Boolean} */ -String.prototype.endsWith = function(text, ignoreCase) { +SP.endsWith = function(text, ignoreCase) { var self = this; var length = text.length; var tmp; @@ -2807,7 +3432,7 @@ String.prototype.endsWith = function(text, ignoreCase) { return tmp.length === length && tmp === text; }; -String.prototype.replacer = function(find, text) { +SP.replacer = function(find, text) { var self = this; var beg = self.indexOf(find); return beg === -1 ? self : (self.substring(0, beg) + text + self.substring(beg + find.length)); @@ -2819,7 +3444,7 @@ String.prototype.replacer = function(find, text) { * @param {String} salt Optional, salt. * @return {String} */ -String.prototype.hash = function(type, salt) { +SP.hash = function(type, salt) { var str = salt ? this + salt : this; switch (type) { case 'md5': @@ -2830,11 +3455,32 @@ String.prototype.hash = function(type, salt) { return str.sha256(); case 'sha512': return str.sha512(); + case 'crc32': + return str.crc32(); + case 'crc32unsigned': + return str.crc32(true); default: - return string_hash(str); + var val = string_hash(str); + return type === true ? val >>> 0 : val; } }; +global.HASH = function(value, type) { + return value.hash(type ? type : true); +}; + +SP.makeid = function() { + return this.hash(true).toString(16); +}; + +SP.crc32 = function(unsigned) { + var crc = -1; + for (var i = 0, length = this.length; i < length; i++) + crc = (crc >>> 8) ^ CRC32TABLE[(crc ^ this.charCodeAt(i)) & 0xFF]; + var val = crc ^ (-1); + return unsigned ? val >>> 0 : val; +}; + function string_hash(s, convert) { var hash = 0; if (s.length === 0) @@ -2847,7 +3493,7 @@ function string_hash(s, convert) { return hash; } -String.prototype.count = function(text) { +SP.count = function(text) { var index = 0; var count = 0; do { @@ -2858,19 +3504,244 @@ String.prototype.count = function(text) { return count; }; -String.prototype.parseXML = function() { - return F.onParseXML(this); +SP.parseXML = function(replace) { + return F.onParseXML(this, replace); }; -String.prototype.parseJSON = function(date) { +SP.parseJSON = function(date) { return exports.parseJSON(this, date); }; -String.prototype.parseQuery = function() { +SP.parseQuery = function() { return exports.parseQuery(this); }; -String.prototype.parseTerminal = function(fields, fn, skip, take) { +SP.parseUA = function(structured) { + + var ua = this; + + if (!ua) + return ''; + + var arr = ua.match(regexpUA); + var uid = ''; + + if (arr) { + + var data = {}; + + for (var i = 0; i < arr.length; i++) { + + if (arr[i] === 'like' && arr[i + 1] === 'Gecko') { + i += 1; + continue; + } + + var key = arr[i].toLowerCase(); + if (key === 'like') + break; + + switch (key) { + case 'linux': + case 'windows': + case 'mac': + case 'symbian': + case 'symbos': + case 'tizen': + case 'android': + data[arr[i]] = 2; + if (key === 'tizen' || key === 'android') + data.Mobile = 1; + break; + case 'webos': + data.WebOS = 2; + break; + case 'media': + case 'center': + case 'tv': + case 'smarttv': + case 'smart': + data[arr[i]] = 5; + break; + case 'iemobile': + case 'mobile': + data[arr[i]] = 1; + data.Mobile = 3; + break; + case 'ipad': + case 'ipod': + case 'iphone': + data.iOS = 2; + data.Mobile = 3; + data[arr[i]] = 1; + if (key === 'ipad') + data.Tablet = 4; + break; + case 'phone': + data.Mobile = 3; + break; + case 'tizenbrowser': + case 'blackberry': + case 'mini': + data.Mobile = 3; + data[arr[i]] = 1; + break; + case 'samsungbrowser': + case 'chrome': + case 'firefox': + case 'msie': + case 'opera': + case 'brave': + case 'vivaldi': + case 'outlook': + case 'safari': + case 'mail': + case 'edge': + case 'maxthon': + case 'electron': + data[arr[i]] = 1; + break; + case 'trident': + data.MSIE = 1; + break; + case 'opr': + data.Opera = 1; + break; + case 'tablet': + data.Tablet = 4; + break; + } + } + + if (data.MSIE) { + data.IE = 1; + delete data.MSIE; + } + + if (data.WebOS || data.Android) + delete data.Linux; + + if (data.IEMobile) { + if (data.Android) + delete data.Android; + if (data.Safari) + delete data.Safari; + if (data.Chrome) + delete data.Chrome; + } else if (data.MSIE) { + if (data.Chrome) + delete data.Chrome; + if (data.Safari) + delete data.Safari; + } else if (data.Edge) { + if (data.Chrome) + delete data.Chrome; + if (data.Safari) + delete data.Safari; + } else if (data.Opera || data.Electron) { + if (data.Chrome) + delete data.Chrome; + if (data.Safari) + delete data.Safari; + } else if (data.Chrome) { + if (data.Safari) + delete data.Safari; + if (data.SamsungBrowser) + delete data.SamsungBrowser; + } else if (data.SamsungBrowser) { + if (data.Safari) + delete data.Safari; + } + + if (structured) { + var keys = Object.keys(data); + var output = { os: '', browser: '', device: 'desktop' }; + + if (data.Tablet) + output.device = 'tablet'; + else if (data.Mobile) + output.device = 'mobile'; + + for (var i = 0; i < keys.length; i++) { + var val = data[keys[i]]; + switch (val) { + case 1: + output.browser += (output.browser ? ' ' : '') + keys[i]; + break; + case 2: + output.os += (output.os ? ' ' : '') + keys[i]; + break; + case 5: + output.device = 'tv'; + break; + } + } + return output; + } + + uid = Object.keys(data).join(' '); + } + + return uid; +}; + +SP.parseCSV = function(delimiter) { + + if (!delimiter) + delimiter = ','; + + var delimiterstring = '"'; + var t = this; + var scope; + var tmp = {}; + var index = 1; + var data = []; + var current = 'a'; + + for (var i = 0; i < t.length; i++) { + var c = t[i]; + + if (!scope) { + + if (c === '\n' || c === '\r') { + tmp && data.push(tmp); + index = 1; + current = 'a'; + tmp = null; + continue; + } + + if (c === delimiter) { + current = String.fromCharCode(97 + index); + index++; + continue; + } + } + + if (c === delimiterstring) { + // Check escaped quotes + if (scope && t[i + 1] === delimiterstring) { + i++; + } else { + scope = c === scope ? '' : c; + continue; + } + } + + if (!tmp) + tmp = {}; + + if (tmp[current]) + tmp[current] += c; + else + tmp[current] = c; + } + + tmp && data.push(tmp); + return data; +}; + +SP.parseTerminal = function(fields, fn, skip, take) { var lines = this.split('\n'); @@ -2988,7 +3859,70 @@ function parseTerminal2(lines, fn, skip, take) { } } -String.prototype.parseDate = function() { +function parseDateFormat(format, val) { + + var tmp = []; + var tmpformat = []; + var prev = ''; + var prevformat = ''; + var allowed = { y: 1, Y: 1, M: 1, m: 1, d: 1, D: 1, H: 1, s: 1, a: 1, w: 1 }; + + for (var i = 0; i < format.length; i++) { + + var c = format[i]; + + if (!allowed[c]) + continue; + + if (prev !== c) { + prevformat && tmpformat.push(prevformat); + prevformat = c; + prev = c; + } else + prevformat += c; + } + + prev = ''; + + for (var i = 0; i < val.length; i++) { + var code = val.charCodeAt(i); + if (code >= 48 && code <= 57) + prev += val[i]; + } + + prevformat && tmpformat.push(prevformat); + + var f = 0; + for (var i = 0; i < tmpformat.length; i++) { + var l = tmpformat[i].length; + tmp.push(prev.substring(f, f + l)); + f += l; + } + + var dt = {}; + + for (var i = 0; i < tmpformat.length; i++) { + var type = tmpformat[i]; + if (tmp[i]) + dt[type[0]] = +tmp[i]; + } + + var h = dt.h || dt.H; + + if (h != null) { + var ampm = val.match(REG_TIME); + if (ampm && ampm[0].toLowerCase() === 'pm') + h += 12; + } + + return new Date((dt.y || dt.Y) || 0, (dt.M || 1) - 1, dt.d || dt.D || 0, h || 0, dt.m || 0, dt.s || 0); +} + +SP.parseDate = function(format) { + + if (format) + return parseDateFormat(format, this); + var self = this.trim(); var lc = self.charCodeAt(self.length - 1); @@ -3078,10 +4012,10 @@ String.prototype.parseDate = function() { } } - return new Date(parsed[0], parsed[1] - 1, parsed[2], parsed[3], parsed[4], parsed[5]); + return new Date(parsed[0], parsed[1] - 1, parsed[2], parsed[3], parsed[4] - NOW.getTimezoneOffset(), parsed[5]); }; -String.prototype.parseDateExpiration = function() { +SP.parseDateExpiration = function() { var self = this; var arr = self.split(' '); @@ -3100,7 +4034,7 @@ String.prototype.parseDateExpiration = function() { return dt; }; -String.prototype.contains = function(value, mustAll) { +SP.contains = function(value, mustAll) { var str = this; if (typeof(value) === 'string') @@ -3123,19 +4057,35 @@ String.prototype.contains = function(value, mustAll) { * @param {String} value * @return {Number} */ -String.prototype.localeCompare2 = function(value) { +SP.localeCompare2 = function(value) { return COMPARER(this, value); }; +var configurereplace = function(text) { + var val = CONF[text.substring(1, text.length - 1)]; + return val == null ? '' : val; +}; + +SP.env = function() { + return this.replace(regexpCONFIGURE, configurereplace); +}; + /** * Parse configuration from a string * @param {Object} def + * @onerr {Function} error handling * @return {Object} */ -String.prototype.parseConfig = function(def) { +SP.parseConfig = function(def, onerr) { + + if (typeof(def) === 'function') { + onerr = def; + def = null; + } + var arr = this.split('\n'); var length = arr.length; - var obj = exports.extend({}, def); + var obj = def ? exports.extend({}, def) : {}; var subtype; var name; var index; @@ -3147,7 +4097,7 @@ String.prototype.parseConfig = function(def) { if (!str || str[0] === '#' || str.substring(0, 2) === '//') continue; - index = str.indexOf(' :'); + index = str.indexOf(':'); if (index === -1) { index = str.indexOf('\t:'); if (index === -1) @@ -3172,19 +4122,26 @@ String.prototype.parseConfig = function(def) { case 'float': case 'double': case 'currency': - obj[name] = value.isNumber(true) ? value.parseFloat() : value.parseInt(); + obj[name] = value.isNumber(true) ? value.parseFloat2() : value.parseInt2(); break; case 'boolean': case 'bool': - obj[name] = value.parseBoolean(); + obj[name] = (/true|on|1|enabled/i).test(value); break; case 'config': - obj[name] = F.config[value]; + obj[name] = CONF[value]; break; case 'eval': case 'object': case 'array': - obj[name] = new Function('return ' + value)(); + try { + obj[name] = new Function('return ' + value)(); + } catch (e) { + if (onerr) + onerr(e, arr[i]); + else + throw new Error('A value of "{0}" can\'t be converted to "{1}": '.format(name, subtype) + e.toString()); + } break; case 'json': obj[name] = value.parseJSON(true); @@ -3198,6 +4155,9 @@ String.prototype.parseConfig = function(def) { case 'datetime': obj[name] = value.parseDate(); break; + case 'random': + obj[name] = GUID((value || '0').parseInt() || 10); + break; default: obj[name] = value; break; @@ -3207,7 +4167,7 @@ String.prototype.parseConfig = function(def) { return obj; }; -String.prototype.format = function() { +SP.format = function() { var arg = arguments; return this.replace(regexpSTRINGFORMAT, function(text) { var value = arg[+text.substring(1, text.length - 1)]; @@ -3215,7 +4175,15 @@ String.prototype.format = function() { }); }; -String.prototype.encode = function() { +SP.encryptUID = function(key) { + return exports.encryptUID(this, key); +}; + +SP.decryptUID = function(key) { + return exports.decryptUID(this, key); +}; + +SP.encode = function() { var output = ''; for (var i = 0, length = this.length; i < length; i++) { var c = this[i]; @@ -3243,7 +4211,7 @@ String.prototype.encode = function() { return output; }; -String.prototype.decode = function() { +SP.decode = function() { return this.replace(regexpDECODE, function(s) { if (s.charAt(1) !== '#') return ALPHA_INDEX[s] || s; @@ -3252,22 +4220,28 @@ String.prototype.decode = function() { }); }; -String.prototype.urlEncode = function() { +SP.urlEncode = function() { return encodeURIComponent(this); }; -String.prototype.urlDecode = function() { +SP.urlDecode = function() { return decodeURIComponent(this); }; -String.prototype.arg = function(obj) { +SP.arg = function(obj, encode, def) { + if (typeof(encode) === 'string') + def = encode; return this.replace(regexpARG, function(text) { - var val = obj[text.substring(1, text.length - 1).trim()]; - return val == null ? text : val; + // Is double? + var l = text[1] === '{' ? 2 : 1; + var val = obj[text.substring(l, text.length - l).trim()]; + if (encode && encode === 'json') + return JSON.stringify(val); + return val == null ? (def == null ? text : def) : encode ? encode === 'html' ? (val + '').encode() : encodeURIComponent(val + '') : val; }); }; -String.prototype.params = function(obj) { +SP.params = function(obj) { OBSOLETE('String.params()', 'The method is deprecated instead of it use F.viewCompile() or String.format().'); @@ -3338,14 +4312,14 @@ String.prototype.params = function(obj) { }); }; -String.prototype.max = function(length, chars) { +SP.max = function(length, chars) { var str = this; if (typeof(chars) !== 'string') chars = '...'; return str.length > length ? str.substring(0, length - chars.length) + chars : str; }; -String.prototype.isJSON = function() { +SP.isJSON = function() { var self = this; if (self.length <= 1) return false; @@ -3369,62 +4343,203 @@ String.prototype.isJSON = function() { break; } - return (a === '"' && b === '"') || (a === '[' && b === ']') || (a === '{' && b === '}'); + return (a === '"' && b === '"') || (a === '[' && b === ']') || (a === '{' && b === '}') || (a.charCodeAt(0) > 47 && b.charCodeAt(0) < 57); }; -String.prototype.isURL = function() { +SP.isURL = function() { return this.length <= 7 ? false : F.validators.url.test(this); }; -String.prototype.isZIP = function() { +SP.isZIP = function() { return F.validators.zip.test(this); }; -String.prototype.isEmail = function() { +SP.isEmail = function() { return this.length <= 4 ? false : F.validators.email.test(this); }; -String.prototype.isPhone = function() { +SP.isPhone = function() { return this.length < 6 ? false : F.validators.phone.test(this); }; -String.prototype.isUID = function() { - return this.length < 18 ? false : F.validators.uid.test(this); +SP.isBase64 = function() { + var str = this; + return str.length % 4 === 0 && regexpBASE64.test(str); +}; + +SP.isUID = function() { + var str = this; + + if (str.length < 12) + return false; + + var is = DEF.validators.uid.test(str); + if (is) { + + var sum; + var beg; + var end; + var e = str[str.length - 1]; + + if (e === 'b' || e === 'c' || e === 'd') { + sum = str[str.length - 2]; + beg = +str[str.length - 3]; + end = str.length - 5; + var tmp = e === 'c' || e === 'd' ? (+str.substring(beg, end)) : parseInt(str.substring(beg, end), 16); + return sum === (tmp % 2 ? '1' : '0'); + } else if (e === 'a') { + sum = str[str.length - 2]; + beg = 6; + end = str.length - 4; + } else { + sum = str[str.length - 1]; + beg = 10; + end = str.length - 4; + } + + while (beg++ < end) { + if (str[beg] !== '0') { + if (((+str.substring(beg, end)) % 2 ? '1' : '0') === sum) + return true; + } + } + } + return false; +}; + +SP.parseUID = function() { + var self = this; + var obj = {}; + var hash; + var e = self[self.length - 1]; + + if (e === 'b' || e === 'c' || e === 'd') { + end = +self[self.length - 3]; + var ticks = ((e === 'b' ? (+self.substring(0, end)) : parseInt(self.substring(0, end), e=== 'd' ? 36 : 16)) * 1000 * 60) + 1580511600000; // 1.1.2020 + obj.date = new Date(ticks); + beg = end; + end = self.length - 5; + hash = +self.substring(end + 3, end + 4); + obj.century = Math.floor((obj.date.getFullYear() - 1) / 100) + 1; + obj.hash = self.substring(end, end + 2); + } else if (e === 'a') { + var ticks = ((+self.substring(0, 6)) * 1000 * 60) + 1548975600000; // old 1.1.2019 + obj.date = new Date(ticks); + beg = 7; + end = self.length - 4; + hash = +self.substring(end + 2, end + 3); + obj.century = Math.floor((obj.date.getFullYear() - 1) / 100) + 1; + obj.hash = self.substring(end, end + 2); + } else { + var y = self.substring(0, 2); + var M = self.substring(2, 4); + var d = self.substring(4, 6); + var H = self.substring(6, 8); + var m = self.substring(8, 10); + + obj.date = new Date(+('20' + y), (+M) - 1, +d, +H, +m, 0); + + var beg = 0; + var end = 0; + var index = 10; + + while (true) { + + var c = self[index]; + + if (!c) + break; + + if (!beg && c !== '0') + beg = index; + + if (c.charCodeAt(0) > 96) { + end = index; + break; + } + + index++; + } + + obj.century = self.substring(end + 4); + + if (obj.century) { + obj.century = 20 + (+obj.century); + obj.date.setYear(obj.date.getFullYear() + 100); + } else + obj.century = 21; + + hash = +self.substring(end + 3, end + 4); + obj.hash = self.substring(end, end + 3); + } + + obj.index = +self.substring(beg, end); + obj.valid = (obj.index % 2 ? 1 : 0) === hash; + return obj; +}; + +SP.parseENV = function() { + + var arr = this.split(regexpLINES); + var obj = {}; + + for (var i = 0; i < arr.length; i++) { + var line = arr[i]; + if (!line || line.substring(0, 2) === '//' || line[0] === '#') + continue; + + var index = line.indexOf('='); + if (index === -1) + continue; + + var key = line.substring(0, index); + var val = line.substring(index + 1).replace(/\\n/g, '\n'); + var end = val.length - 1; + + if ((val[0] === '"' && val[end] === '"') || (val[0] === '\'' && val[end] === '\'')) + val = val.substring(1, end); + else + val = val.trim(); + + obj[key] = val; + } + + return obj; }; -String.prototype.parseInt = function(def) { +SP.parseInt = function(def) { var str = this.trim(); var num = +str; - return isNaN(num) ? (def || 0) : num; + return isNaN(num) ? (def === undefined ? 0 : def) : num; }; -String.prototype.parseInt2 = function(def) { +SP.parseInt2 = function(def) { var num = this.match(regexpINTEGER); - return num ? +num[0] : def || 0; + return num ? +num[0] : (def === undefined ? 0 : def); }; -String.prototype.parseFloat2 = function(def) { +SP.parseFloat2 = function(def) { var num = this.match(regexpFLOAT); - return num ? +num[0].toString().replace(/,/g, '.') : def || 0; + return num ? +num[0].toString().replace(/,/g, '.') : (def === undefined ? 0 : def); }; -String.prototype.parseBool = String.prototype.parseBoolean = function() { +SP.parseBool = SP.parseBoolean = function() { var self = this.toLowerCase(); return self === 'true' || self === '1' || self === 'on'; }; -String.prototype.parseFloat = function(def) { +SP.parseFloat = function(def) { var str = this.trim(); if (str.indexOf(',') !== -1) str = str.replace(',', '.'); var num = +str; - return isNaN(num) ? (def || 0) : num; + return isNaN(num) ? (def === undefined ? 0 : def) : num; }; -String.prototype.capitalize = function(first) { +SP.capitalize = function(first) { if (first) - return this[0].toUpperCase() + this.substring(1); + return (this[0] || '').toUpperCase() + this.substring(1); var builder = ''; var c; @@ -3441,49 +4556,55 @@ String.prototype.capitalize = function(first) { return builder; }; - -String.prototype.toUnicode = function() { - var result = ''; - var self = this; - var length = self.length; - for(var i = 0; i < length; ++i){ - if(self.charCodeAt(i) > 126 || self.charCodeAt(i) < 32) - result += '\\u' + self.charCodeAt(i).hex(4); +SP.toUnicode = function() { + var output = ''; + for (var i = 0; i < this.length; i++) { + var c = this[i].charCodeAt(0); + if(c > 126 || c < 32) + output += '\\u' + ('000' + c.toString(16)).substr(-4); else - result += self[i]; + output += this[i]; } - return result; + return output; }; -String.prototype.fromUnicode = function() { - return unescape(this.replace(regexpUNICODE, (match, v) => String.fromCharCode(parseInt(v, 16)))); +SP.fromUnicode = function() { + var output = ''; + for (var i = 0; i < this.length; i++) { + if (this[i] === '\\' && this[i + 1] === 'u') { + output += String.fromCharCode(parseInt(this[i + 2] + this[i + 3] + this[i + 4] + this[i + 5], 16)); + i += 5; + } else + output += this[i]; + } + return output; }; -String.prototype.sha1 = function(salt) { +SP.sha1 = function(salt) { var hash = Crypto.createHash('sha1'); hash.update(this + (salt || ''), ENCODING); return hash.digest('hex'); }; -String.prototype.sha256 = function(salt) { +SP.sha256 = function(salt) { var hash = Crypto.createHash('sha256'); hash.update(this + (salt || ''), ENCODING); return hash.digest('hex'); }; -String.prototype.sha512 = function(salt) { +SP.sha512 = function(salt) { var hash = Crypto.createHash('sha512'); hash.update(this + (salt || ''), ENCODING); return hash.digest('hex'); }; -String.prototype.md5 = function(salt) { +SP.md5 = function(salt) { var hash = Crypto.createHash('md5'); hash.update(this + (salt || ''), ENCODING); return hash.digest('hex'); }; -String.prototype.toSearch = function() { +SP.toSearch = function() { var str = this.replace(regexpSEARCH, '').trim().toLowerCase().removeDiacritics(); var buf = []; var prev = ''; @@ -3500,11 +4621,18 @@ String.prototype.toSearch = function() { return buf.join(''); }; -String.prototype.toKeywords = String.prototype.keywords = function(forSearch, alternative, max_count, max_length, min_length) { +SP.toKeywords = SP.keywords = function(forSearch, alternative, max_count, max_length, min_length) { return exports.keywords(this, forSearch, alternative, max_count, max_length, min_length); }; -String.prototype.encrypt = function(key, isUnique) { +function checksum(val) { + var sum = 0; + for (var i = 0; i < val.length; i++) + sum += val.charCodeAt(i); + return sum; +} + +SP.encrypt = function(key, isUnique, secret) { var str = '0' + this; var data_count = str.length; var key_count = key.length; @@ -3514,6 +4642,7 @@ String.prototype.encrypt = function(key, isUnique) { var index = 0; values[0] = String.fromCharCode(random); + var counter = this.length + key.length; for (var i = count - 1; i > 0; i--) { @@ -3521,23 +4650,34 @@ String.prototype.encrypt = function(key, isUnique) { values[i] = String.fromCharCode(index ^ (key.charCodeAt(i % key_count) ^ random)); } - var hash = exports.createBuffer(counter + '=' + values.join(''), ENCODING).toString('base64').replace(regexpENCRYPT, text => text === '+' ? '_' : '-'); - index = hash.indexOf('='); - return index > 0 ? hash.substring(0, index) : hash; + str = Buffer.from(counter + '=' + values.join(''), ENCODING).toString('hex'); + var sum = 0; + + for (var i = 0; i < str.length; i++) + sum += str.charCodeAt(i); + + return (sum + checksum((secret || CONF.secret) + key)) + '-' + str; }; -String.prototype.decrypt = function(key) { +SP.decrypt = function(key, secret) { + + var index = this.indexOf('-'); + if (index === -1) + return null; - var values = this.replace(regexpDECRYPT, text => text === '-' ? '/' : '+'); - var mod = values.length % 4; + var cs = +this.substring(0, index); + if (!cs || isNaN(cs)) + return null; - if (mod) { - for (var i = 0; i < mod; i++) - values += '='; - } + var hash = this.substring(index + 1); + var sum = checksum((secret || CONF.secret) + key); + for (var i = 0; i < hash.length; i++) + sum += hash.charCodeAt(i); - values = exports.createBuffer(values, 'base64').toString(ENCODING); + if (sum !== cs) + return null; + var values = Buffer.from(hash, 'hex').toString(ENCODING); var index = values.indexOf('='); if (index === -1) return null; @@ -3550,7 +4690,6 @@ String.prototype.decrypt = function(key) { var count = values.length; var random = values.charCodeAt(0); - var key_count = key.length; var data_count = count - (random % key_count); var decrypt_data = []; @@ -3561,23 +4700,50 @@ String.prototype.decrypt = function(key) { } var val = decrypt_data.join(''); - return counter !== val.length + key.length ? null : val; + return counter !== (val.length + key.length) ? null : val; }; -String.prototype.base64ToFile = function(filename, callback) { - var self = this; +exports.encryptUID = function(val, key) { + + var num = typeof(val) === 'number'; + var sum = 0; + + if (!key) + key = CONF.secret; + + val = val + ''; + + for (var i = 0; i < val.length; i++) + sum += val.charCodeAt(i); + + for (var i = 0; i < key.length; i++) + sum += key.charCodeAt(i); + return (num ? 'n' : 'x') + (CONF.secret_uid + val + sum + key).crc32(true).toString(16) + 'x' + val; +}; + +exports.decryptUID = function(val, key) { + var num = val[0] === 'n'; + var raw = val.substring(val.indexOf('x', 1) + 1); + + if (num) + raw = +raw; + + return exports.encryptUID(raw, key) === val ? raw : null; +}; + +SP.base64ToFile = function(filename, callback) { + var self = this; var index = self.indexOf(','); if (index === -1) index = 0; else index++; - Fs.writeFile(filename, self.substring(index), 'base64', callback || exports.noop); return this; }; -String.prototype.base64ToBuffer = function() { +SP.base64ToBuffer = function() { var self = this; var index = self.indexOf(','); @@ -3586,20 +4752,20 @@ String.prototype.base64ToBuffer = function() { else index++; - return exports.createBuffer(self.substring(index), 'base64'); + return Buffer.from(self.substring(index), 'base64'); }; -String.prototype.base64ContentType = function() { +SP.base64ContentType = function() { var self = this; var index = self.indexOf(';'); return index === -1 ? '' : self.substring(5, index); }; -String.prototype.removeDiacritics = function() { +SP.removeDiacritics = function() { return exports.removeDiacritics(this); }; -String.prototype.indent = function(max, c) { +SP.indent = function(max, c) { var plus = ''; if (c === undefined) c = ' '; @@ -3608,7 +4774,7 @@ String.prototype.indent = function(max, c) { return plus + this; }; -String.prototype.isNumber = function(isDecimal) { +SP.isNumber = function(isDecimal) { var self = this; var length = self.length; @@ -3635,8 +4801,8 @@ String.prototype.isNumber = function(isDecimal) { return true; }; -if (!String.prototype.padLeft) { - String.prototype.padLeft = function(max, c) { +if (!SP.padLeft) { + SP.padLeft = function(max, c) { var self = this; var len = max - self.length; if (len < 0) @@ -3650,8 +4816,8 @@ if (!String.prototype.padLeft) { } -if (!String.prototype.padRight) { - String.prototype.padRight = function(max, c) { +if (!SP.padRight) { + SP.padRight = function(max, c) { var self = this; var len = max - self.length; if (len < 0) @@ -3664,7 +4830,7 @@ if (!String.prototype.padRight) { }; } -String.prototype.insert = function(index, value) { +SP.insert = function(index, value) { var str = this; var a = str.substring(0, index); var b = value.toString() + str.substring(index); @@ -3676,7 +4842,7 @@ String.prototype.insert = function(index, value) { * @param {Number} max A maximum length, default: 60 and optional. * @return {String} */ -String.prototype.slug = String.prototype.toSlug = String.prototype.toLinker = String.prototype.linker = function(max) { +SP.slug = SP.toSlug = SP.toLinker = SP.linker = function(max) { max = max || 60; var self = this.trim().toLowerCase().removeDiacritics(); @@ -3687,6 +4853,11 @@ String.prototype.slug = String.prototype.toSlug = String.prototype.toLinker = St var c = self[i]; var code = self.charCodeAt(i); + if (code > 540){ + builder = ''; + break; + } + if (builder.length >= max) break; @@ -3699,15 +4870,24 @@ String.prototype.slug = String.prototype.toSlug = String.prototype.toLinker = St if ((code > 47 && code < 58) || (code > 94 && code < 123)) builder += c; } - var l = builder.length - 1; - return builder[l] === '-' ? builder.substring(0, l) : builder; + + if (builder.length > 1) { + length = builder.length - 1; + return builder[length] === '-' ? builder.substring(0, length) : builder; + } else if (!length) + return ''; + + length = self.length; + self = self.replace(/\s/g, ''); + builder = self.crc32(true).toString(36) + ''; + return self[0].charCodeAt(0).toString(32) + builder + self[self.length - 1].charCodeAt(0).toString(32) + length; }; -String.prototype.pluralize = function(zero, one, few, other) { +SP.pluralize = function(zero, one, few, other) { return this.parseInt().pluralize(zero, one, few, other); }; -String.prototype.isBoolean = function() { +SP.isBoolean = function() { var self = this.toLowerCase(); return (self === 'true' || self === 'false') ? true : false; }; @@ -3716,11 +4896,11 @@ String.prototype.isBoolean = function() { * Check if the string contains only letters and numbers. * @return {Boolean} */ -String.prototype.isAlphaNumeric = function() { +SP.isAlphaNumeric = function() { return regexpALPHA.test(this); }; -String.prototype.soundex = function() { +SP.soundex = function() { var arr = this.toLowerCase().split(''); var first = arr.shift(); @@ -3744,29 +4924,43 @@ String.prototype.soundex = function() { * Remove all Html Tags from a string * @return {string} */ -String.prototype.removeTags = function() { +SP.removeTags = function() { return this.replace(regexpTags, ''); }; -Number.prototype.floor = function(decimals) { +NP.floor = function(decimals) { return Math.floor(this * Math.pow(10, decimals)) / Math.pow(10, decimals); }; -Number.prototype.padLeft = function(max, c) { +NP.fixed = function(decimals) { + return +this.toFixed(decimals); +}; + +NP.padLeft = function(max, c) { return this.toString().padLeft(max, c || '0'); }; -Number.prototype.padRight = function(max, c) { +NP.padRight = function(max, c) { return this.toString().padRight(max, c || '0'); }; +NP.round = function(precision) { + var m = Math.pow(10, precision) || 1; + return Math.round(this * m) / m; +}; + +NP.currency = function(currency, a, b, c) { + var curr = DEF.currencies[currency]; + return curr ? curr(this, a, b, c) : this.format(2); +}; + /** * Async decrements * @param {Function(index, next)} fn * @param {Function} callback * @return {Number} */ -Number.prototype.async = function(fn, callback) { +NP.async = function(fn, callback) { var number = this; if (number) fn(number--, () => setImmediate(() => number.async(fn, callback))); @@ -3782,7 +4976,7 @@ Number.prototype.async = function(fn, callback) { * @param {String} separatorDecimal Decimal separator, default '.' if number separator is ',' or ' '. * @return {String} */ -Number.prototype.format = function(decimals, separator, separatorDecimal) { +NP.format = function(decimals, separator, separatorDecimal) { var self = this; @@ -3833,7 +5027,7 @@ Number.prototype.format = function(decimals, separator, separatorDecimal) { return minus + output + (dec.length ? separatorDecimal + dec : ''); }; -Number.prototype.add = function(value, decimals) { +NP.add = function(value, decimals) { if (value == null) return this; @@ -3903,7 +5097,7 @@ Number.prototype.add = function(value, decimals) { return num; }; -Number.prototype.format2 = function(format) { +NP.format2 = function(format) { var index = 0; var num = this.toString(); var beg = 0; @@ -4012,7 +5206,7 @@ Number.prototype.format2 = function(format) { return this.format(output); }; -Number.prototype.pluralize = function(zero, one, few, other) { +NP.pluralize = function(zero, one, few, other) { var num = this; var value = ''; @@ -4035,14 +5229,14 @@ Number.prototype.pluralize = function(zero, one, few, other) { return num.format(format) + value.replace(format, ''); }; -Number.prototype.hex = function(length) { +NP.hex = function(length) { var str = this.toString(16).toUpperCase(); while(str.length < length) str = '0' + str; return str; }; -Number.prototype.VAT = function(percentage, decimals, includedVAT) { +NP.VAT = function(percentage, decimals, includedVAT) { var num = this; var type = typeof(decimals); @@ -4061,29 +5255,28 @@ Number.prototype.VAT = function(percentage, decimals, includedVAT) { if (!percentage || !num) return num; - - return includedVAT ? (num / ((percentage / 100) + 1)).floor(decimals) : (num * ((percentage / 100) + 1)).floor(decimals); + return includedVAT ? (num / ((percentage / 100) + 1)).round(decimals) : (num * ((percentage / 100) + 1)).round(decimals); }; -Number.prototype.discount = function(percentage, decimals) { +NP.discount = function(percentage, decimals) { var num = this; if (decimals === undefined) decimals = 2; return (num - (num / 100) * percentage).floor(decimals); }; -Number.prototype.parseDate = function(plus) { +NP.parseDate = function(plus) { return new Date(this + (plus || 0)); }; -if (!Number.prototype.toRad) { - Number.prototype.toRad = function () { +if (!NP.toRad) { + NP.toRad = function () { return this * Math.PI / 180; }; } -Number.prototype.filesize = function(decimals, type) { +NP.filesize = function(decimals, type) { if (typeof(decimals) === 'string') { var tmp = type; @@ -4151,12 +5344,14 @@ function filesizehelper(number, count) { return number; } +var AP = Array.prototype; + /** * Take items from array * @param {Number} count * @return {Array} */ -Array.prototype.take = function(count) { +AP.take = function(count) { var arr = []; var self = this; var length = self.length; @@ -4174,7 +5369,7 @@ Array.prototype.take = function(count) { * @param {Boolean} rewrite Default: false. * @return {Array} Returns self */ -Array.prototype.extend = function(obj, rewrite) { +AP.extend = function(obj, rewrite) { var isFn = typeof(obj) === 'function'; for (var i = 0, length = this.length; i < length; i++) { if (isFn) @@ -4190,7 +5385,7 @@ Array.prototype.extend = function(obj, rewrite) { * @param {Object} def Default value. * @return {Object} */ -Array.prototype.first = function(def) { +AP.first = function(def) { var item = this[0]; return item === undefined ? def : item; }; @@ -4200,7 +5395,7 @@ Array.prototype.first = function(def) { * @param {String} name Optional, property name. * @return {Object} */ -Array.prototype.toObject = function(name) { +AP.toObject = function(name) { var self = this; var obj = {}; @@ -4222,7 +5417,7 @@ Array.prototype.toObject = function(name) { * @param {Array} b Second array. * @param {Function(itemA, itemB, indexA, indexB)} executor */ -Array.prototype.compare = function(id, b, executor) { +AP.compare = function(id, b, executor) { var a = this; var ak = {}; @@ -4274,6 +5469,8 @@ Array.prototype.compare = function(id, b, executor) { executor(a[index], bv, index, i); } } + + OBSOLETE('Array.compare()', 'Use U.diff() insteadof Array.compare()'); }; /** @@ -4284,7 +5481,7 @@ Array.prototype.compare = function(id, b, executor) { * @param {Boolean} remove Optional, remove item from this array if the item doesn't exist int arr (default: false). * @return {Array} */ -Array.prototype.pair = function(property, arr, fn, remove) { +AP.pair = function(property, arr, fn, remove) { if (property instanceof Array) { var tmp = property; @@ -4320,6 +5517,7 @@ Array.prototype.pair = function(property, arr, fn, remove) { this.splice(index, 1); } + OBSOLETE('Array.pair()', 'The method will be removed in Total.js v4'); return this; }; @@ -4328,12 +5526,12 @@ Array.prototype.pair = function(property, arr, fn, remove) { * @param {Object} def Default value. * @return {Object} */ -Array.prototype.last = function(def) { +AP.last = function(def) { var item = this[this.length - 1]; return item === undefined ? def : item; }; -Array.prototype.quicksort = Array.prototype.orderBy = function(name, asc) { +AP.quicksort = AP.orderBy = function(name, asc) { var length = this.length; if (!length || length === 1) @@ -4342,10 +5540,20 @@ Array.prototype.quicksort = Array.prototype.orderBy = function(name, asc) { if (typeof(name) === 'boolean') { asc = name; name = undefined; - } - - if (asc === undefined) + } else if (asc === undefined) asc = true; + else { + switch (asc) { + case 'asc': + case 'ASC': + asc = true; + break; + case 'desc': + case 'DESC': + asc = false; + break; + } + } var self = this; var type = 0; @@ -4400,7 +5608,7 @@ Array.prototype.quicksort = Array.prototype.orderBy = function(name, asc) { return self; }; -Array.prototype.trim = function() { +AP.trim = function() { var self = this; var output = []; for (var i = 0, length = self.length; i < length; i++) { @@ -4416,7 +5624,7 @@ Array.prototype.trim = function() { * @param {Number} count * @return {Array} */ -Array.prototype.skip = function(count) { +AP.skip = function(count) { var arr = []; var self = this; var length = self.length; @@ -4431,7 +5639,7 @@ Array.prototype.skip = function(count) { * @param {Object} value Optional. * @return {Array} */ -Array.prototype.where = function(cb, value) { +AP.where = AP.findAll = function(cb, value) { var self = this; var selected = []; @@ -4462,7 +5670,23 @@ Array.prototype.where = function(cb, value) { * @param {Object} value Optional. * @return {Array} */ -Array.prototype.findItem = Array.prototype.find = function(cb, value) { +AP.findItem = function(cb, value) { + var self = this; + var index = self.findIndex(cb, value); + if (index === -1) + return null; + return self[index]; +}; + +var arrfindobsolete; + +AP.find = function(cb, value) { + + if (!arrfindobsolete) { + arrfindobsolete = true; + OBSOLETE('Array.prototype.find()', 'will be removed in v4, use alternative "Array.prototype.findItem()"'); + } + var self = this; var index = self.findIndex(cb, value); if (index === -1) @@ -4470,7 +5694,7 @@ Array.prototype.findItem = Array.prototype.find = function(cb, value) { return self[index]; }; -Array.prototype.findIndex = function(cb, value) { +AP.findIndex = function(cb, value) { var self = this; var isFN = typeof(cb) === 'function'; @@ -4503,7 +5727,7 @@ Array.prototype.findIndex = function(cb, value) { * @param {Object} value Optional. * @return {Array} */ -Array.prototype.remove = function(cb, value) { +AP.remove = function(cb, value) { var self = this; var arr = []; @@ -4527,7 +5751,7 @@ Array.prototype.remove = function(cb, value) { return arr; }; -Array.prototype.wait = Array.prototype.waitFor = function(onItem, callback, thread, tmp) { +AP.wait = AP.waitFor = function(onItem, callback, thread, tmp) { var self = this; var init = false; @@ -4581,7 +5805,7 @@ function next_wait(self, onItem, callback, thread, tmp) { * @param {Function} callback Optional * @return {Array} */ -Array.prototype.async = function(thread, callback, pending) { +AP.async = function(thread, callback, pending) { var self = this; @@ -4620,13 +5844,13 @@ Array.prototype.async = function(thread, callback, pending) { return self; }; -Array.prototype.randomize = function() { +AP.randomize = function() { OBSOLETE('Array.randomize()', 'Use Array.random().'); return this.random(); }; // Fisher-Yates shuffle -Array.prototype.random = function() { +AP.random = function() { for (var i = this.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = this[i]; @@ -4636,7 +5860,7 @@ Array.prototype.random = function() { return this; }; -Array.prototype.limit = function(max, fn, callback, index) { +AP.limit = function(max, fn, callback, index) { if (index === undefined) index = 0; @@ -4681,7 +5905,7 @@ Array.prototype.limit = function(max, fn, callback, index) { * Get unique elements from Array * @return {[type]} [description] */ -Array.prototype.unique = function(property) { +AP.unique = function(property) { var self = this; var result = []; @@ -4860,21 +6084,23 @@ Async.prototype = { } }; -Async.prototype.__proto__ = Object.create(Events.EventEmitter.prototype, { +const ACP = Async.prototype; + +ACP.__proto__ = Object.create(Events.EventEmitter.prototype, { constructor: { value: Async, enumberable: false } }); -Async.prototype.reload = function() { +ACP.reload = function() { var self = this; self.tasksAll = Object.keys(self.tasksPending); self.emit('percentage', self.percentage); return self; }; -Async.prototype.cancel = function(name) { +ACP.cancel = function(name) { var self = this; @@ -4900,7 +6126,7 @@ Async.prototype.cancel = function(name) { return true; }; -Async.prototype.await = function(name, fn, cb) { +ACP.await = function(name, fn, cb) { var self = this; @@ -4923,7 +6149,7 @@ Async.prototype.await = function(name, fn, cb) { return true; }; -Async.prototype.wait = function(name, waitingFor, fn, cb) { +ACP.wait = function(name, waitingFor, fn, cb) { var self = this; @@ -4946,34 +6172,34 @@ Async.prototype.wait = function(name, waitingFor, fn, cb) { return true; }; -Async.prototype.complete = function(fn) { +ACP.complete = function(fn) { return this.run(fn); }; -Async.prototype.run = function(fn) { +ACP.run = function(fn) { this._isRunning = true; fn && this.onComplete.push(fn); this.refresh(); return this; }; -Async.prototype.isRunning = function(name) { +ACP.isRunning = function(name) { if (!name) return this._isRunning; var task = this.tasksPending[name]; return task ? task.isRunning === 1 : false; }; -Async.prototype.isWaiting = function(name) { +ACP.isWaiting = function(name) { var task = this.tasksPending[name]; return task ? task.isRunning === 0 : false; }; -Async.prototype.isPending = function(name) { +ACP.isPending = function(name) { return this.tasksPending[name] ? true : false; }; -Async.prototype.timeout = function(name, timeout) { +ACP.timeout = function(name, timeout) { if (timeout) this.tasksTimeout[name] = timeout; else @@ -4981,7 +6207,7 @@ Async.prototype.timeout = function(name, timeout) { return this; }; -Async.prototype.refresh = function(name) { +ACP.refresh = function(name) { var self = this; @@ -5048,14 +6274,16 @@ function FileList() { this.advanced = false; } -FileList.prototype.reset = function() { +const FLP = FileList.prototype; + +FLP.reset = function() { this.file.length = 0; this.directory.length = 0; this.pendingDirectory.length = 0; return this; }; -FileList.prototype.walk = function(directory) { +FLP.walk = function(directory) { var self = this; @@ -5077,7 +6305,7 @@ FileList.prototype.walk = function(directory) { }); }; -FileList.prototype.stat = function(path) { +FLP.stat = function(path) { var self = this; Fs.stat(path, function(err, stats) { @@ -5098,11 +6326,11 @@ FileList.prototype.stat = function(path) { }); }; -FileList.prototype.clean = function(path) { - return path[path.length - 1] === '/' ? path : path + '/'; +FLP.clean = function(path) { + return path[path.length - 1] === Path.sep ? path : path + Path.sep; }; -FileList.prototype.next = function() { +FLP.next = function() { var self = this; if (self.pending.length) { @@ -5367,21 +6595,16 @@ exports.queue = function(name, max, fn) { return true; }; -exports.minifyStyle = function(value) { - return require('./internal').compile_css(value); -}; - -exports.minifyScript = function(value) { - return require('./internal').compile_javascript(value); +exports.minifyStyle = function(val) { + return Internal.compile_css(val); }; -exports.minifyHTML = function(value) { - return require('./internal').compile_html(value); +exports.minifyScript = function(val) { + return Internal.compile_javascript(val); }; -exports.restart = function() { - exports.queuecache = {}; - dnscache = {}; +exports.minifyHTML = function(val) { + return Internal.compile_html(val); }; exports.parseTheme = function(value) { @@ -5391,39 +6614,35 @@ exports.parseTheme = function(value) { if (index === -1) return ''; value = value.substring(1, index); - return value === '?' ? F.config['default-theme'] : value; + return value === '?' ? CONF.default_theme : value; }; + exports.set = function(obj, path, value) { var cachekey = 'S+' + path; if (F.temporary.other[cachekey]) return F.temporary.other[cachekey](obj, value); - var arr = path.split('.'); - var builder = []; - var p = ''; - - for (var i = 0, length = arr.length; i < length; i++) { - p += (p !== '' ? '.' : '') + arr[i]; - var type = arr[i] instanceof Array ? '[]' : '{}'; - - if (i !== length - 1) { - builder.push('if(typeof(w.' + p + ')!=="object"||w.' + p + '===null)w.' + p + '=' + type); - continue; - } + if ((/__proto__|constructor|prototype|eval|function|\*|\+|;|\s|\(|\)|!/).test(path)) + return value; - if (type === '{}') - break; + var arr = parsepath(path); + var builder = []; - p = p.substring(0, p.lastIndexOf('[')); - builder.push('if(!(w.' + p + ' instanceof Array))w.' + p + '=' + type); - break; + for (var i = 0; i < arr.length - 1; i++) { + var type = arr[i + 1] ? (REGISARR.test(arr[i + 1]) ? '[]' : '{}') : '{}'; + var p = 'w' + (arr[i][0] === '[' ? '' : '.') + arr[i]; + builder.push('if(typeof(' + p + ')!==\'object\'||' + p + '==null)' + p + '=' + type + ';'); } - var fn = (new Function('w', 'a', 'b', builder.join(';') + ';w.' + path.replace(/'/, '\'') + '=a;return a')); + var v = arr[arr.length - 1]; + var ispush = v.lastIndexOf('[]') !== -1; + var a = builder.join(';') + ';var v=typeof(a)===\'function\'?a(U.get(b)):a;w' + (v[0] === '[' ? '' : '.') + (ispush ? v.replace(REGREPLACEARR, '.push(v)') : (v + '=v')) + ';return v'; + + var fn = new Function('w', 'a', 'b', a); F.temporary.other[cachekey] = fn; - fn(obj, value, path); + return fn(obj, value, path); }; exports.get = function(obj, path) { @@ -5433,24 +6652,58 @@ exports.get = function(obj, path) { if (F.temporary.other[cachekey]) return F.temporary.other[cachekey](obj); - var arr = path.split('.'); + if ((/__proto__|constructor|prototype|eval|function|\*|\+|;|\s|\(|\)|!/).test(path)) + return; + + var arr = parsepath(path); var builder = []; - var p = ''; - for (var i = 0, length = arr.length - 1; i < length; i++) { - var tmp = arr[i]; - var index = tmp.lastIndexOf('['); - if (index !== -1) - builder.push('if(!w.' + (p ? p + '.' : '') + tmp.substring(0, index) + ')return'); - p += (p !== '' ? '.' : '') + arr[i]; - builder.push('if(!w.' + p + ')return'); - } + for (var i = 0, length = arr.length - 1; i < length; i++) + builder.push('if(!w' + (!arr[i] || arr[i][0] === '[' ? '' : '.') + arr[i] + ')return'); - var fn = (new Function('w', builder.join(';') + ';return w.' + path.replace(/'/, '\''))); + var v = arr[arr.length - 1]; + var fn = (new Function('w', builder.join(';') + ';return w' + (v[0] === '[' ? '' : '.') + v)); F.temporary.other[cachekey] = fn; return fn(obj); }; +function parsepath(path) { + + var arr = path.split('.'); + var builder = []; + var all = []; + + for (var i = 0; i < arr.length; i++) { + var p = arr[i]; + var index = p.indexOf('['); + if (index === -1) { + if (p.indexOf('-') === -1) { + all.push(p); + builder.push(all.join('.')); + } else { + var a = all.splice(all.length - 1); + all.push(a + '[\'' + p + '\']'); + builder.push(all.join('.')); + } + } else { + if (p.indexOf('-') === -1) { + all.push(p.substring(0, index)); + builder.push(all.join('.')); + all.splice(all.length - 1); + all.push(p); + builder.push(all.join('.')); + } else { + all.push('[\'' + p.substring(0, index) + '\']'); + builder.push(all.join('')); + all.push(p.substring(index)); + builder.push(all.join('')); + } + } + } + + return builder; +} + global.Async = global.async = exports.async; global.sync = global.SYNCHRONIZE = exports.sync; global.sync2 = exports.sync2; @@ -5482,11 +6735,21 @@ function shellsort(arr, fn) { return arr; } -function EventEmitter2() { - this.$events = {}; +function EventEmitter2(obj) { + if (obj) { + !obj.emit && EventEmitter2.extend(obj); + return obj; + } else + this.$events = {}; } -EventEmitter2.prototype.emit = function(name, a, b, c, d, e, f, g) { +const EE2P = EventEmitter2.prototype; + +EE2P.emit = function(name, a, b, c, d, e, f, g) { + + if (!this.$events) + return this; + var evt = this.$events[name]; if (evt) { var clean = false; @@ -5506,7 +6769,9 @@ EventEmitter2.prototype.emit = function(name, a, b, c, d, e, f, g) { return this; }; -EventEmitter2.prototype.on = function(name, fn) { +EE2P.on = function(name, fn) { + if (!this.$events) + this.$events = {}; if (this.$events[name]) this.$events[name].push(fn); else @@ -5514,48 +6779,65 @@ EventEmitter2.prototype.on = function(name, fn) { return this; }; -EventEmitter2.prototype.once = function(name, fn) { +EE2P.once = function(name, fn) { fn.$once = true; return this.on(name, fn); }; -EventEmitter2.prototype.removeListener = function(name, fn) { - var evt = this.$events[name]; - if (evt) { - evt = evt.remove(n => n === fn); - if (evt.length) - this.$events[name] = evt; - else - this.$events[name] = undefined; +EE2P.removeListener = function(name, fn) { + if (this.$events) { + var evt = this.$events[name]; + if (evt) { + evt = evt.remove(n => n === fn); + if (evt.length) + this.$events[name] = evt; + else + this.$events[name] = undefined; + } } return this; }; -EventEmitter2.prototype.removeAllListeners = function(name) { - if (name === true) - this.$events = EMPTYOBJECT; - else if (name) - this.$events[name] = undefined; - else - this.$events = {}; +EE2P.removeAllListeners = function(name) { + if (this.$events) { + if (name === true) + this.$events = EMPTYOBJECT; + else if (name) + this.$events[name] = undefined; + else + this.$events = {}; + } return this; }; +EventEmitter2.extend = function(obj) { + obj.emit = EE2P.emit; + obj.on = EE2P.on; + obj.once = EE2P.once; + obj.removeListener = EE2P.removeListener; + obj.removeAllListeners = EE2P.removeAllListeners; +}; + exports.EventEmitter2 = EventEmitter2; function Chunker(name, max) { this.name = name; this.max = max || 50; this.index = 0; - this.filename = 'chunker_{0}-'.format(name); + this.filename = '{0}-'.format(name); this.stack = []; this.flushing = 0; this.pages = 0; this.count = 0; + this.percentage = 0; + this.autoremove = true; + this.compress = true; this.filename = F.path.temp(this.filename); } -Chunker.prototype.append = Chunker.prototype.write = function(obj) { +const CHP = Chunker.prototype; + +CHP.append = CHP.write = function(obj) { var self = this; self.stack.push(obj); @@ -5563,48 +6845,70 @@ Chunker.prototype.append = Chunker.prototype.write = function(obj) { var tmp = self.stack.length; if (tmp >= self.max) { + self.flushing++; self.pages++; self.count += tmp; - Fs.writeFile(self.filename + (self.index++) + '.json', JSON.stringify(self.stack), () => self.flushing--); + + var index = (self.index++); + + if (self.compress) { + Zlib.deflate(Buffer.from(JSON.stringify(self.stack), ENCODING), function(err, buffer) { + Fs.writeFile(self.filename + index + '.chunker', buffer, () => self.flushing--); + }); + } else + Fs.writeFile(self.filename + index + '.chunker', JSON.stringify(self.stack), () => self.flushing--); + self.stack = []; } return self; }; -Chunker.prototype.end = function() { +CHP.end = function() { var self = this; var tmp = self.stack.length; if (tmp) { self.flushing++; self.pages++; self.count += tmp; - Fs.writeFile(self.filename + (self.index++) + '.json', JSON.stringify(self.stack), () => self.flushing--); + + var index = (self.index++); + + if (self.compress) { + Zlib.deflate(Buffer.from(JSON.stringify(self.stack), ENCODING), function(err, buffer) { + Fs.writeFile(self.filename + index + '.chunker', buffer, () => self.flushing--); + }); + } else + Fs.writeFile(self.filename + index + '.chunker', JSON.stringify(self.stack), () => self.flushing--); + self.stack = []; } return self; }; -Chunker.prototype.each = function(onItem, onEnd, indexer) { +CHP.each = function(onItem, onEnd, indexer) { var self = this; - if (indexer === undefined) + if (indexer == null) { + self.percentage = 0; indexer = 0; + } if (indexer >= self.index) return onEnd && onEnd(); self.read(indexer++, function(err, items) { + self.percentage = Math.ceil((indexer / self.pages) * 100); onItem(items, () => self.each(onItem, onEnd, indexer), indexer - 1); }); return self; }; -Chunker.prototype.read = function(index, callback) { +CHP.read = function(index, callback) { var self = this; if (self.flushing) { @@ -5612,24 +6916,42 @@ Chunker.prototype.read = function(index, callback) { return; } - Fs.readFile(self.filename + index + '.json', function(err, data) { - if (err) + var filename = self.filename + index + '.chunker'; + + Fs.readFile(filename, function(err, data) { + + if (err) { callback(null, EMPTYARRAY); - else + return; + } + + if (self.compress) { + Zlib.inflate(data, function(err, data) { + if (err) { + callback(null, EMPTYARRAY); + } else { + self.autoremove && Fs.unlink(filename, NOOP); + callback(null, data.toString('utf8').parseJSON(true)); + } + }); + } else { + self.autoremove && Fs.unlink(filename, NOOP); callback(null, data.toString('utf8').parseJSON(true)); + } }); + return self; }; -Chunker.prototype.clear = function() { +CHP.clear = function() { var files = []; for (var i = 0; i < this.index; i++) - files.push(this.filename + i + '.json'); + files.push(this.filename + i + '.chunker'); files.wait((filename, next) => Fs.unlink(filename, next)); return this; }; -Chunker.prototype.destroy = function() { +CHP.destroy = function() { this.clear(); this.indexer = 0; this.flushing = 0; @@ -5662,5 +6984,86 @@ if (NODEVERSION > 699) { exports.createBuffer = (val, type) => new Buffer(val || '', type); } +function Callback(count, callback) { + this.pending = count; + this.$callback = callback; +} +const CP = Callback.prototype; + +CP.done = function(callback) { + this.$callback = callback; + return this; +}; + +CP.next = function() { + var self = this; + self.pending--; + if (!self.pending && self.$callback) { + self.$callback(); + self.$callback = null; + } + return self; +}; + +global.Callback = Callback; + +exports.Callback = function(count, callback) { + return new Callback(count, callback); +}; + +function Reader() { + var t = this; + t.$add = function(builder) { + if (t.reader) + t.reader.add(builder); + else + t.reader = new framework_nosql.NoSQLReader(builder); + }; +} +const RP = Reader.prototype; + +RP.done = function() { + var self = this; + self.reader.done(); + return self; +}; + +RP.reset = function() { + var self = this; + self.reader.reset(); + return self; +}; + +RP.push = function(data) { + if (data == null) + this.reader.done(); + else + this.reader.compare(data instanceof Array ? data : [data]); + return this; +}; + +RP.find = function() { + var self = this; + var builder = new framework_nosql.DatabaseBuilder(); + setImmediate(self.$add, builder); + return builder; +}; + +RP.count = function() { + var builder = this.find(); + builder.$options.readertype = 1; + return builder; +}; + +RP.scalar = function(type, field) { + return this.find().scalar(type, field); +}; + +exports.reader = function() { + return new Reader(); +}; + +const BUFEMPTYJSON = Buffer.from('{}'); + global.WAIT = exports.wait; !global.F && require('./index'); diff --git a/websocketclient.js b/websocketclient.js index 00715f668..eb5a03c1f 100644 --- a/websocketclient.js +++ b/websocketclient.js @@ -1,4 +1,4 @@ -// Copyright 2012-2018 (c) Peter Širka +// Copyright 2012-2020 (c) Peter Širka // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the @@ -21,7 +21,7 @@ /** * @module WebSocketClient - * @version 2.9.0 + * @version 3.4.4 */ if (!global.framework_utils) @@ -33,8 +33,7 @@ const Http = require('http'); const Url = require('url'); const Zlib = require('zlib'); const ENCODING = 'utf8'; -const REG_WEBSOCKET_ERROR = /ECONNRESET|EHOSTUNREACH|EPIPE|is closed/i; -const WEBSOCKET_COMPRESS = U.createBuffer([0x00, 0x00, 0xFF, 0xFF]); +const WEBSOCKET_COMPRESS = Buffer.from([0x00, 0x00, 0xFF, 0xFF]); const WEBSOCKET_COMPRESS_OPTIONS = { windowBits: Zlib.Z_DEFAULT_WINDOWBITS }; const CONCAT = [null, null]; @@ -51,7 +50,9 @@ function WebSocketClient() { this.headers = {}; } -WebSocketClient.prototype.connect = function(url, protocol, origin) { +const WebSocketClientProto = WebSocketClient.prototype; + +WebSocketClientProto.connect = function(url, protocol, origin) { var self = this; var options = {}; @@ -70,6 +71,7 @@ WebSocketClient.prototype.connect = function(url, protocol, origin) { options.path = url.path; options.query = url.query; options.headers = {}; + options.headers['User-Agent'] = 'Total.js/v' + F.version_header; options.headers['Sec-WebSocket-Version'] = '13'; options.headers['Sec-WebSocket-Key'] = key; options.headers['Sec-Websocket-Extensions'] = (self.options.compress ? 'permessage-deflate, ' : '') + 'client_max_window_bits'; @@ -86,12 +88,13 @@ WebSocketClient.prototype.connect = function(url, protocol, origin) { if (keys.length) { var tmp = []; for (var i = 0, length = keys.length; i < length; i++) - tmp.push(keys[i] + '=' + self.headers[keys[i]]); + tmp.push(keys[i] + '=' + self.cookies[keys[i]]); options.headers['Cookie'] = tmp.join(', '); } self.req = (isSecure ? Https : Http).get(options); self.req.$main = self; + F.stats.performance.online++; self.req.on('error', function(e) { self.$events.error && self.emit('error', e); @@ -99,7 +102,10 @@ WebSocketClient.prototype.connect = function(url, protocol, origin) { self.req.on('response', function() { self.$events.error && self.emit('error', new Error('Unexpected server response.')); - self.free(); + if (self.options.reconnectserver) + self.connect(url, protocol, origin); + else + self.free(); }); self.req.on('upgrade', function(response, socket) { @@ -157,13 +163,16 @@ function websocket_close() { var ws = this.$websocket; ws.closed = true; ws.$onclose(); + F.stats.performance.online--; ws.options.reconnect && setTimeout(function(ws) { + ws.isClosed = false; + ws._isClosed = false; ws.reconnect++; ws.connect(ws.url, ws.protocol, ws.origin); }, ws.options.reconnect, ws); } -WebSocketClient.prototype.emit = function(name, a, b, c, d, e, f, g) { +WebSocketClientProto.emit = function(name, a, b, c, d, e, f, g) { var evt = this.$events[name]; if (evt) { var clean = false; @@ -183,7 +192,7 @@ WebSocketClient.prototype.emit = function(name, a, b, c, d, e, f, g) { return this; }; -WebSocketClient.prototype.on = function(name, fn) { +WebSocketClientProto.on = function(name, fn) { if (this.$events[name]) this.$events[name].push(fn); else @@ -191,12 +200,12 @@ WebSocketClient.prototype.on = function(name, fn) { return this; }; -WebSocketClient.prototype.once = function(name, fn) { +WebSocketClientProto.once = function(name, fn) { fn.$once = true; return this.on(name, fn); }; -WebSocketClient.prototype.removeListener = function(name, fn) { +WebSocketClientProto.removeListener = function(name, fn) { var evt = this.$events[name]; if (evt) { evt = evt.remove(n => n === fn); @@ -208,7 +217,7 @@ WebSocketClient.prototype.removeListener = function(name, fn) { return this; }; -WebSocketClient.prototype.removeAllListeners = function(name) { +WebSocketClientProto.removeAllListeners = function(name) { if (name === true) this.$events = EMPTYOBJECT; else if (name) @@ -218,7 +227,7 @@ WebSocketClient.prototype.removeAllListeners = function(name) { return this; }; -WebSocketClient.prototype.free = function() { +WebSocketClientProto.free = function() { var self = this; self.socket && self.socket.destroy(); self.socket = null; @@ -232,7 +241,7 @@ WebSocketClient.prototype.free = function() { * @param {Buffer} data * @return {Framework} */ -WebSocketClient.prototype.$ondata = function(data) { +WebSocketClientProto.$ondata = function(data) { if (this.isClosed) return; @@ -254,12 +263,13 @@ WebSocketClient.prototype.$ondata = function(data) { current.type2 = current.type; var tmp; + var decompress = current.compressed && this.inflate; switch (current.type === 0x00 ? current.type2 : current.type) { case 0x01: // text - if (this.inflate) { + if (decompress) { current.final && this.parseInflate(); } else { tmp = this.$readbody(); @@ -274,7 +284,7 @@ WebSocketClient.prototype.$ondata = function(data) { case 0x02: // binary - if (this.inflate) { + if (decompress) { current.final && this.parseInflate(); } else { tmp = this.$readbody(); @@ -290,8 +300,13 @@ WebSocketClient.prototype.$ondata = function(data) { break; case 0x08: - // close - this.close(); + this.closemessage = current.buffer.slice(4).toString('utf8'); + this.closecode = current.buffer[2] << 8 | current.buffer[3]; + + if (this.closemessage && this.options.encodedecode) + this.closemessage = $decodeURIComponent(this.closemessage); + + this.close(true); break; case 0x09: @@ -315,7 +330,7 @@ WebSocketClient.prototype.$ondata = function(data) { }; function buffer_concat(buffers, length) { - var buffer = U.createBufferSize(length); + var buffer = Buffer.alloc(length); var offset = 0; for (var i = 0, n = buffers.length; i < n; i++) { buffers[i].copy(buffer, offset); @@ -327,7 +342,7 @@ function buffer_concat(buffers, length) { // MIT // Written by Jozef Gula // Optimized by Peter Sirka -WebSocketClient.prototype.$parse = function() { +WebSocketClientProto.$parse = function() { var self = this; var current = self.current; @@ -336,8 +351,9 @@ WebSocketClient.prototype.$parse = function() { if (!current.buffer || current.buffer.length <= 2 || ((current.buffer[0] & 0x80) >> 7) !== 1) return; - // webSocked - Opcode + // WebSocket - Opcode current.type = current.buffer[0] & 0x0f; + current.compressed = (current.buffer[0] & 0x40) === 0x40; // is final message? current.final = ((current.buffer[0] & 0x80) >> 7) === 0x01; @@ -366,13 +382,13 @@ WebSocketClient.prototype.$parse = function() { // does frame contain mask? if (current.isMask) { - current.mask = U.createBufferSize(4); + current.mask = Buffer.alloc(4); current.buffer.copy(current.mask, 0, index - 4, index); } - if (this.inflate) { + if (current.compressed && this.inflate) { - var buf = U.createBufferSize(length); + var buf = Buffer.alloc(length); current.buffer.copy(buf, 0, index, mlength); // does frame contain mask? @@ -385,7 +401,7 @@ WebSocketClient.prototype.$parse = function() { buf.$continue = current.final === false; this.inflatepending.push(buf); } else { - current.data = U.createBufferSize(length); + current.data = Buffer.alloc(length); current.buffer.copy(current.data, 0, index, mlength); } } @@ -393,7 +409,7 @@ WebSocketClient.prototype.$parse = function() { return true; }; -WebSocketClient.prototype.$readbody = function() { +WebSocketClientProto.$readbody = function() { var current = this.current; var length = current.data.length; @@ -401,7 +417,7 @@ WebSocketClient.prototype.$readbody = function() { if (current.type === 1) { - buf = U.createBufferSize(length); + buf = Buffer.alloc(length); for (var i = 0; i < length; i++) { if (current.isMask) buf[i] = current.data[i] ^ current.mask[i % 4]; @@ -413,7 +429,7 @@ WebSocketClient.prototype.$readbody = function() { } else { - buf = U.createBufferSize(length); + buf = Buffer.alloc(length); for (var i = 0; i < length; i++) { // does frame contain mask? if (current.isMask) @@ -426,13 +442,19 @@ WebSocketClient.prototype.$readbody = function() { }; -WebSocketClient.prototype.$decode = function() { +WebSocketClientProto.$decode = function() { var data = this.current.body; + if (global.F) + global.F.stats.performance.message++; + switch (this.options.type) { - case 'binary': // BINARY - this.emit('message', new Uint8Array(data).buffer); + case 'buffer': // Buffer + case 'binary': // Binary + // this.emit('message', Buffer.from(new Uint8Array(data))); + // break; + this.emit('message', data); break; case 'json': // JSON @@ -452,7 +474,7 @@ WebSocketClient.prototype.$decode = function() { this.current.body = null; }; -WebSocketClient.prototype.parseInflate = function() { +WebSocketClientProto.parseInflate = function() { var self = this; if (self.inflatelock) @@ -464,7 +486,7 @@ WebSocketClient.prototype.parseInflate = function() { self.inflatechunkslength = 0; self.inflatelock = true; self.inflate.write(buf); - !buf.$continue && self.inflate.write(U.createBuffer(WEBSOCKET_COMPRESS)); + !buf.$continue && self.inflate.write(Buffer.from(WEBSOCKET_COMPRESS)); self.inflate.flush(function() { if (!self.inflatechunks) @@ -488,19 +510,16 @@ WebSocketClient.prototype.parseInflate = function() { } }; -WebSocketClient.prototype.$onerror = function(err) { - - if (this.isClosed) - return; - - if (REG_WEBSOCKET_ERROR.test(err.stack)) { +WebSocketClientProto.$onerror = function(err) { + this.$events.error && this.emit('error', err); + if (!this.isClosed) { this.isClosed = true; this.$onclose(); - } else - this.$events.error && this.emit('error', err); + } }; -WebSocketClient.prototype.$onclose = function() { +WebSocketClientProto.$onclose = function() { + if (this._isClosed) return; @@ -519,8 +538,8 @@ WebSocketClient.prototype.$onclose = function() { this.deflatechunks = null; } - this.$events.close && this.emit('close'); - this.socket.removeAllListeners(); + this.$events.close && this.emit('close', this.closecode, this.closemessage); + this.socket && this.socket.removeAllListeners(); }; /** @@ -529,34 +548,68 @@ WebSocketClient.prototype.$onclose = function() { * @param {Boolean} raw The message won't be converted e.g. to JSON. * @return {WebSocketClient} */ -WebSocketClient.prototype.send = function(message, raw, replacer) { +WebSocketClientProto.send = function(message, raw, replacer) { if (this.isClosed) return this; var t = this.options.type; - if (t !== 'binary') { - var data = t === 'json' ? (raw ? message : JSON.stringify(message, replacer)) : (message || '').toString(); + if (t !== 'binary' && t !== 'buffer') { + var data = t === 'json' ? (raw ? message : JSON.stringify(message, replacer)) : ((message == null ? '' : message) + ''); + if (this.options.encodedecode && data) data = encodeURIComponent(data); + if (this.deflate) { - this.deflatepending.push(U.createBuffer(data)); + this.deflatepending.push(Buffer.from(data, ENCODING)); this.sendDeflate(); } else this.socket.write(U.getWebSocketFrame(0, data, 0x01)); + } else if (message) { if (this.deflate) { - this.deflatepending.push(U.createBuffer(message)); + this.deflatepending.push(message); this.sendDeflate(); } else - this.socket.write(U.getWebSocketFrame(0, new Int8Array(message), 0x02)); + this.socket.write(U.getWebSocketFrame(0, message, 0x02)); } return this; }; -WebSocketClient.prototype.sendDeflate = function() { +/** + * Sends a message + * @param {String/Object} message + * @param {Boolean} raw The message won't be converted e.g. to JSON. + * @return {WebSocketClient} + */ +WebSocketClientProto.sendcustom = function(type, message) { + + if (this.isClosed) + return this; + + if (type === 'binary' || type === 'buffer') { + if (this.deflate) { + this.deflatepending.push(message); + this.sendDeflate(); + } else + this.socket.write(U.getWebSocketFrame(0, message, 0x02)); + } else { + var data = (message == null ? '' : message) + ''; + if (this.options.encodedecode && data) + data = encodeURIComponent(data); + if (this.deflate) { + this.deflatepending.push(Buffer.from(data)); + this.sendDeflate(); + } else + this.socket.write(U.getWebSocketFrame(0, data, 0x01)); + } + + return this; +}; + +WebSocketClientProto.sendDeflate = function() { var self = this; if (self.deflatelock) @@ -585,7 +638,7 @@ WebSocketClient.prototype.sendDeflate = function() { * Ping message * @return {WebSocketClient} */ -WebSocketClient.prototype.ping = function() { +WebSocketClientProto.ping = function() { if (!this.isClosed) { this.socket.write(U.getWebSocketFrame(0, '', 0x09)); this.$ping = false; @@ -609,7 +662,13 @@ function websocket_deflate(data) { * @param {Number} code WebSocket code. * @return {WebSocketClient} */ -WebSocketClient.prototype.close = function(message, code) { +WebSocketClientProto.close = function(message, code) { + + if (message !== true) { + this.options.reconnect = 0; + } else + message = undefined; + if (!this.isClosed) { this.isClosed = true; this.socket.end(U.getWebSocketFrame(code || 1000, message ? (this.options.encodedecode ? encodeURIComponent(message) : message) : '', 0x08));