diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..80da00465 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# http://editorconfig.org/ + +# Top-most EditorConfig file +root = true + +[*.js] +indent_style = tab +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = false +max_line_length = 120 + +[*.json] +indent_style = space +indent_size = 2 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 c8670d33f..0b4524423 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ *.sublime-project *.sublime-workspace .DS_Store -*.log \ No newline at end of file +*.log +.idea +test/tmp/* +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 cc3e6c53f..f823ea1f3 100644 --- a/.npmignore +++ b/.npmignore @@ -1,12 +1,19 @@ Makefile empty-project/ examples/ +merged/ +tools/ +helpers/ test/ *.markdown *.md +.editorconfig +.gitingore .git* +*.yml *.DS_Store *.sublime-project *.sublime-workspace +*.sh changes.txt roadmap.txt \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..14c12bc01 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: node_js +cache: + - npm +node_js: + - "8.9.1" +install: + - npm install +script: + - cd test + - bash run.sh \ No newline at end of file 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/total b/bin/total deleted file mode 100755 index 649bafc12..000000000 --- a/bin/total +++ /dev/null @@ -1,539 +0,0 @@ -var exec = require('child_process').exec; -var fs = require('fs'); -var path = require('path'); -var os = require('os'); - -var EOF = os.platform() === 'win32' ? '\r\n' : '\n'; -var $type = 1; - -// $type == 0 - full -// $type == 1 - normal -// $type == 2 - minimum -// $type == 3 - angular - -function createDirectory(directory) { - var arr = []; - - arr.push('controllers'); - arr.push('node_modules'); - - if ($type < 2) { - - arr.push('views'); - arr.push('templates'); - arr.push('databases'); - arr.push('definitions'); - - if ($type == 0) { - arr.push('components'); - arr.push('workers'); - arr.push('tests'); - arr.push('contents'); - arr.push('logs'); - arr.push('modules'); - arr.push('source'); - } - - arr.push('resources'); - arr.push('public'); - arr.push('tmp'); - } - - arr.forEach(function(o) { - var dir = path.join(directory, o); - if (!fs.existsSync(dir)) - fs.mkdirSync(dir); - }); -} - -function createDirectoryAngular(directory) { - var arr = []; - - arr.push('app'); - arr.push('controllers'); - arr.push('node_modules'); - arr.push('views'); - - arr.forEach(function(o) { - var dir = path.join(directory, o); - if (!fs.existsSync(dir)) - fs.mkdirSync(dir); - }); - - arr = []; - arr.push('common'); - arr.push('controllers'); - arr.push('css'); - arr.push('directives'); - arr.push('filters'); - arr.push('resources'); - arr.push('services'); - arr.push('templates'); - arr.push('views'); - - arr.forEach(function(o) { - var dir = path.join(directory, 'app', o); - if (!fs.existsSync(dir)) - fs.mkdirSync(dir); - }); -} - -function createFiles(directory) { - createFileIndex(directory); - createFileConfig(directory); - - if ($type < 2) { - createFileViews(directory); - createFileResource(directory); - createFilePublic(directory); - } - - if ($type === 3) { - createFileViews(directory); - createFilePublicAngular(directory); - createFileControllerAngular(directory); - } else - createFileController(directory); - - if ($type === 0) { - createFileTest(directory); - createFileModules(directory); - createFileComponent(directory); - } -} - -function createFileIndex(directory) { - fs.writeFileSync(path.join(directory, 'debug.js'), 'Ly8gPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQovLyBJTVBPUlRBTlQ6IG9ubHkgZm9yIGRldmVsb3BtZW50IG1vZGUNCi8vIHRvdGFsLmpzIC0gd2ViIGFwcGxpY2F0aW9uIGZyYW1ld29yayBmb3Igbm9kZS5qcw0KLy8gaHR0cDovL3d3dy50b3RhbGpzLmNvbQ0KLy8gPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQoNCnZhciBmcyA9IHJlcXVpcmUoImZzIik7DQoNCnZhciBodHRwcyA9IGZhbHNlOw0KdmFyIG9wdGlvbnMgPSB7IGtleTogbnVsbCwgY2VydDogbnVsbCB9Ow0KDQp2YXIgaXNEZWJ1Z2dpbmc9cHJvY2Vzcy5hcmd2W3Byb2Nlc3MuYXJndi5sZW5ndGgtMV09PT0iZGVidWdnaW5nIjt2YXIgZGlyZWN0b3J5PXByb2Nlc3MuY3dkKCk7dmFyIHBhdGg9cmVxdWlyZSgicGF0aCIpO2Z1bmN0aW9uIGRlYnVnKCl7dmFyIGZyYW1ld29yaz1yZXF1aXJlKCJ0b3RhbC5qcyIpO3ZhciBwb3J0PXBhcnNlSW50KHByb2Nlc3MuYXJndlsyXSk7ZnJhbWV3b3JrLnJ1bihyZXF1aXJlKGh0dHBzPyJodHRwcyI6Imh0dHAiKSx0cnVlLHBvcnQsIiIsb3B0aW9ucyYmb3B0aW9ucy5rZXkmJm9wdGlvbnMuY2VydD9vcHRpb25zOm51bGwpfWZ1bmN0aW9uIGFwcCgpe3ZhciBmb3JrPXJlcXVpcmUoImNoaWxkX3Byb2Nlc3MiKS5mb3JrO3ZhciB1dGlscz1yZXF1aXJlKCJ0b3RhbC5qcy91dGlscyIpO3ZhciBkaXJlY3Rvcmllcz1bZGlyZWN0b3J5KyIvY29udHJvbGxlcnMiLGRpcmVjdG9yeSsiL2RlZmluaXRpb25zIixkaXJlY3RvcnkrIi9tb2R1bGVzIixkaXJlY3RvcnkrIi9yZXNvdXJjZXMiLGRpcmVjdG9yeSsiL2NvbXBvbmVudHMiLGRpcmVjdG9yeSsiL21vZGVscyIsZGlyZWN0b3J5KyIvc291cmNlIl07dmFyIGZpbGVzPXt9O3ZhciBmb3JjZT1mYWxzZTt2YXIgY2hhbmdlcz1bXTt2YXIgYXBwPW51bGw7dmFyIHN0YXR1cz0wO3ZhciBhc3luYz1uZXcgdXRpbHMuQXN5bmM7dmFyIHBpZD0iIjt2YXIgcGlkSW50ZXJ2YWw9bnVsbDt2YXIgcHJlZml4PSItLS0tLS0tLS0tLS0+ICI7dmFyIGlzTG9hZGVkPWZhbHNlO2Z1bmN0aW9uIG9uRmlsdGVyKHBhdGgsaXNEaXJlY3Rvcnkpe3JldHVybiBpc0RpcmVjdG9yeT90cnVlOnBhdGguaW5kZXhPZigiLmpzIikhPT0tMXx8cGF0aC5pbmRleE9mKCIucmVzb3VyY2UiKSE9PS0xfWZ1bmN0aW9uIG9uQ29tcGxldGUoKXt2YXIgc2VsZj10aGlzO2ZzLnJlYWRkaXIoZGlyZWN0b3J5LGZ1bmN0aW9uKGVycixhcnIpe3ZhciBsZW5ndGg9YXJyLmxlbmd0aDtmb3IodmFyIGk9MDtpPGxlbmd0aDtpKyspe3ZhciBuYW1lPWFycltpXTtpZihuYW1lPT09ImNvbmZpZyJ8fG5hbWU9PT0iY29uZmlnLWRlYnVnInx8bmFtZT09PSJjb25maWctcmVsZWFzZSJ8fG5hbWU9PT0idmVyc2lvbnMifHxuYW1lLmluZGV4T2YoIi5qcyIpIT09LTF8fG5hbWUuaW5kZXhPZigiLnJlc291cmNlIikhPT0tMSlzZWxmLmZpbGUucHVzaChuYW1lKX1sZW5ndGg9c2VsZi5maWxlLmxlbmd0aDtmb3IodmFyIGk9MDtpPGxlbmd0aDtpKyspe3ZhciBuYW1lPXNlbGYuZmlsZVtpXTtpZighZmlsZXNbbmFtZV0pZmlsZXNbbmFtZV09aXNMb2FkZWQ/MDpudWxsfXJlZnJlc2goKX0pfWZ1bmN0aW9uIHJlZnJlc2goKXt2YXIgZmlsZW5hbWVzPU9iamVjdC5rZXlzKGZpbGVzKTt2YXIgbGVuZ3RoPWZpbGVuYW1lcy5sZW5ndGg7Zm9yKHZhciBpPTA7aTxsZW5ndGg7aSsrKXt2YXIgZmlsZW5hbWU9ZmlsZW5hbWVzW2ldOyhmdW5jdGlvbihmaWxlbmFtZSl7YXN5bmMuYXdhaXQoZnVuY3Rpb24obmV4dCl7ZnMuc3RhdChmaWxlbmFtZSxmdW5jdGlvbihlcnIsc3RhdCl7aWYoIWVycil7dmFyIHRpY2tzPXN0YXQubXRpbWUuZ2V0VGltZSgpO2lmKGZpbGVzW2ZpbGVuYW1lXSE9PW51bGwmJmZpbGVzW2ZpbGVuYW1lXSE9PXRpY2tzKXtjaGFuZ2VzLnB1c2gocHJlZml4K2ZpbGVuYW1lLnJlcGxhY2UoZGlyZWN0b3J5LCIiKSsoZmlsZXNbZmlsZW5hbWVdPT09MD8iIChhZGRlZCkiOiIgKG1vZGlmaWVkKSIpKTtmb3JjZT10cnVlfWZpbGVzW2ZpbGVuYW1lXT10aWNrc31lbHNle2RlbGV0ZSBmaWxlc1tmaWxlbmFtZV07Y2hhbmdlcy5wdXNoKHByZWZpeCtmaWxlbmFtZS5yZXBsYWNlKGRpcmVjdG9yeSwiIikrIiAocmVtb3ZlZCkiKTtmb3JjZT10cnVlfW5leHQoKX0pfSl9KShmaWxlbmFtZSl9YXN5bmMuY29tcGxldGUoZnVuY3Rpb24oKXtpc0xvYWRlZD10cnVlO3NldFRpbWVvdXQocmVmcmVzaF9kaXJlY3RvcnksMmUzKTtpZihzdGF0dXMhPT0xKXJldHVybjtpZighZm9yY2UpcmV0dXJuO3Jlc3RhcnQoKTt2YXIgbGVuZ3RoPWNoYW5nZXMubGVuZ3RoO2Zvcih2YXIgaT0wO2k8bGVuZ3RoO2krKyljb25zb2xlLmxvZyhjaGFuZ2VzW2ldKTtjaGFuZ2VzPVtdO2ZvcmNlPWZhbHNlfSl9ZnVuY3Rpb24gcmVmcmVzaF9kaXJlY3RvcnkoKXt1dGlscy5scyhkaXJlY3RvcmllcyxvbkNvbXBsZXRlLG9uRmlsdGVyKX1mdW5jdGlvbiByZXN0YXJ0KCl7aWYoYXBwIT09bnVsbCl7dHJ5e3Byb2Nlc3Mua2lsbChhcHAucGlkKX1jYXRjaChlcnIpe31hcHA9bnVsbH12YXIgYXJyPXByb2Nlc3MuYXJndjthcnIucG9wKCk7YXJyLnB1c2goImRlYnVnZ2luZyIpO2FwcD1mb3JrKHBhdGguam9pbihkaXJlY3RvcnksImRlYnVnLmpzIiksYXJyKTthcHAub24oIm1lc3NhZ2UiLGZ1bmN0aW9uKG1zZyl7aWYobXNnLnN1YnN0cmluZygwLDUpPT09Im5hbWU6Iil7cHJvY2Vzcy50aXRsZT0iZGVidWc6ICIrbXNnLnN1YnN0cmluZyg2KTtyZXR1cm59fSk7YXBwLm9uKCJleGl0IixmdW5jdGlvbigpe2lmKHN0YXR1cyE9PTI1NSlyZXR1cm47YXBwPW51bGx9KTtpZihzdGF0dXM9PT0wKWFwcC5zZW5kKCJkZWJ1Z2dpbmciKTtzdGF0dXM9MX1wcm9jZXNzLm9uKCJTSUdURVJNIixlbmQpO3Byb2Nlc3Mub24oIlNJR0lOVCIsZW5kKTtwcm9jZXNzLm9uKCJleGl0IixlbmQpO2Z1bmN0aW9uIGVuZCgpe2lmKGFyZ3VtZW50cy5jYWxsZWUuaXNFbmQpcmV0dXJuO2FyZ3VtZW50cy5jYWxsZWUuaXNFbmQ9dHJ1ZTtmcy51bmxpbmsocGlkLG5vb3ApO2lmKGFwcD09PW51bGwpe3Byb2Nlc3MuZXhpdCgwKTtyZXR1cm59cHJvY2Vzcy5raWxsKGFwcC5waWQpO2FwcD1udWxsO3Byb2Nlc3MuZXhpdCgwKX1mdW5jdGlvbiBub29wKCl7fWlmKHByb2Nlc3MucGlkPjApe2NvbnNvbGUubG9nKHByZWZpeCsiUElEOiAiK3Byb2Nlc3MucGlkKTtwaWQ9cGF0aC5qb2luKGRpcmVjdG9yeSwiZGVidWcucGlkIik7ZnMud3JpdGVGaWxlU3luYyhwaWQscHJvY2Vzcy5waWQpO3BpZEludGVydmFsPXNldEludGVydmFsKGZ1bmN0aW9uKCl7ZnMuZXhpc3RzKHBpZCxmdW5jdGlvbihleGlzdCl7aWYoZXhpc3QpcmV0dXJuO2ZzLnVubGluayhwaWQsbm9vcCk7aWYoYXBwIT09bnVsbClwcm9jZXNzLmtpbGwoYXBwLnBpZCk7cHJvY2Vzcy5leGl0KDApfSl9LDJlMyl9cmVzdGFydCgpO3JlZnJlc2hfZGlyZWN0b3J5KCl9aWYoaXNEZWJ1Z2dpbmcpZGVidWcoKTtlbHNlIGlmKCFmcy5leGlzdHNTeW5jKHBhdGguam9pbihkaXJlY3RvcnksImRlYnVnLnBpZCIpKSlhcHAoKTs=', 'base64'); - fs.writeFileSync(path.join(directory, 'release.js'), 'dmFyIGZyYW1ld29yayA9IHJlcXVpcmUoJ3RvdGFsLmpzJyk7DQp2YXIgaHR0cCA9IHJlcXVpcmUoJ2h0dHAnKTsNCg0KZnJhbWV3b3JrLnJ1bihodHRwLCBmYWxzZSwgcGFyc2VJbnQocHJvY2Vzcy5hcmd2WzJdKSk7', 'base64'); - fs.writeFileSync(path.join(directory, 'test.js'), 'dmFyIGZyYW1ld29yayA9IHJlcXVpcmUoJ3RvdGFsLmpzJyk7DQp2YXIgaHR0cCA9IHJlcXVpcmUoJ2h0dHAnKTsNCg0KZnJhbWV3b3JrLnJ1bihodHRwLCB0cnVlLCBwYXJzZUludChwcm9jZXNzLmFyZ3ZbMl0pKTsNCg0KZnJhbWV3b3JrLnRlc3QodHJ1ZSwgZnVuY3Rpb24oKSB7DQoJY29uc29sZS5sb2coJycpOw0KCWNvbnNvbGUubG9nKCc9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09Jyk7DQoJY29uc29sZS5sb2coJ0NvbmdyYXR1bGF0aW9ucywgdGhlIHRlc3Qgd2FzIHN1Y2Nlc3NmdWwhJyk7DQoJY29uc29sZS5sb2coJz09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0nKTsNCgljb25zb2xlLmxvZygnJyk7DQp9KTs=', 'base64'); -} - -function createFileConfig(directory) { - var buffer = []; - - buffer.push("name : My web site"); - buffer.push("author : Your company name"); - buffer.push(""); - buffer.push("// Mail settings"); - buffer.push("mail.smtp : smtp.yourwebsite.com"); - buffer.push('mail.smtp.options : "{"secure":false,"port":25,"user":"","password":"","timeout":10000}"'); - buffer.push("mail.address.from : your@yourwebsite.com"); - buffer.push("mail.address.reply : your@yourwebsite.com"); - buffer.push("mail.address.bcc : "); - - fs.writeFileSync(path.join(directory, 'config'), buffer.join(EOF)); - buffer = []; - - buffer.push("default-ip : 127.0.0.1"); - buffer.push("default-port : 8000"); - buffer.push(""); - buffer.push("secret : your-secret-key"); - buffer.push("version : 1.01"); - - if ($type === 3) { - buffer.push(''); - buffer.push('// CloudFlare cdnjs'); - buffer.push("angular-version : 1.2.16"); - buffer.push(''); - buffer.push('// CloudFlare cdnjs'); - buffer.push("angular-i18n-version : 1.2.15"); - } - - fs.writeFileSync(path.join(directory, 'config-release'), buffer.join(EOF)); - fs.writeFileSync(path.join(directory, 'config-debug'), buffer.join(EOF).replace(/true/g, 'false')); -} - -function createFileViews(directory) { - var buffer = []; - var dir = path.join(directory, 'views'); - - if (!fs.existsSync(dir)) - fs.mkdirSync(dir); - - buffer.push("@{meta('title', 'description', 'keywords')}"); - - if ($type === 3) { - buffer.push(''); - buffer.push('') - buffer.push("@{ngStyle('app')}"); - buffer.push(''); - buffer.push('') - buffer.push("@{ngLocale('en-us')}"); - buffer.push(''); - buffer.push('') - buffer.push("@{ngController('home', 'user')}"); - buffer.push(""); - buffer.push('
{{ name }}
'); - } else { - buffer.push(""); - buffer.push('
Hello world!
'); - } - - if ($type === 3) - fs.writeFileSync(path.join(dir, 'app.html'), buffer.join('\n')); - else - fs.writeFileSync(path.join(dir, 'homepage.html'), buffer.join('\n')); - - buffer = []; - - if ($type === 3) { - buffer.push(""); - buffer.push("@{ng('angular', 'resource', 'route')}"); - buffer.push(""); - } - - buffer.push(''); - - if ($type === 3) - buffer.push(''); - else - buffer.push(''); - - buffer.push(''); - buffer.push(' @{meta}'); - buffer.push(' '); - buffer.push(' '); - buffer.push(' '); - buffer.push(' '); - buffer.push(' '); - buffer.push(' @{head}'); - if ($type !== 3) { - buffer.push(' @{css(\'default.css\')}'); - buffer.push(' '); - buffer.push(' @{js(\'default.js\')}'); - } - buffer.push(' @{favicon(\'favicon.ico\')}'); - buffer.push(''); - buffer.push(''); - buffer.push(''); - buffer.push(' @{body}'); - buffer.push(''); - buffer.push(''); - buffer.push(''); - - fs.writeFileSync(path.join(dir, '_layout.html'), buffer.join(EOF)); -} - -function createFileResource(directory) { - var dir = path.join(directory, 'resources'); - fs.writeFileSync(path.join(dir, 'default.resource'), 'name : value' + EOF); -} - -function createFilePublicAngular(directory) { - - var dir = path.join(directory, 'app'); - var css = path.join(dir, 'css'); - var controllers = path.join(dir, 'controllers'); - var buffer = []; - - buffer.push('User-agent: *'); - buffer.push('Allow: /'); - buffer.push(''); - - fs.writeFileSync(path.join(dir, 'robots.txt'), buffer.join(EOF)); - fs.writeFileSync(path.join(css, 'app.css'), 'LyphdXRvKi8NCg0KLyoNCiAgICB2YXIgY29sb3IgPSAnY29sb3I6cmVkJzsNCiAgICB2YXIgd2lkdGggPSAnOTQwcHgnOw0KKi8NCg0KYm9keSB7IHBhZGRpbmc6MjBweDsgbWFyZ2luOjA7IGZvbnQ6bm9ybWFsIDEycHggQXJpYWw7IGNvbG9yOiM1MDUwNTA7IH0NCg0KLmNvbnRlbnQgeyBtYXJnaW46MCBhdXRvOyB3aWR0aDogJHdpZHRoOyBwYWRkaW5nOiAxMHB4OyBib3JkZXItcmFkaXVzOiA1cHg7ICRjb2xvcjsgYm94LXNoYWRvdzogMCAwIDIwcHggcmdiYSgwLDAsMCwwLjUpOyBhbmltYXRpb246IG15QW5pbWF0aW9uIDVzIGFsdGVybmF0ZTsgfQ0KDQoubGlzdCB7IGJvcmRlci1ib3R0b206MXB4IHNvbGlkICNFMEUwRTA7IHBhZGRpbmctYm90dG9tOjVweDsgbWFyZ2luLWJvdHRvbTo1cHg7IH0NCi5saXN0ID4gZGl2OmZpcnN0LWNoaWxkIHsgZm9udC1zaXplOiAxNXB4OyB9DQoNCkBrZXlmcmFtZXMgbXlBbmltYXRpb24NCnsNCgkwJSAgIHsgYmFja2dyb3VuZDogd2hpdGU7IH0NCgkyNSUgIHsgYmFja2dyb3VuZDogI0YwRjBGMDsgfQ0KCTUwJSAgeyBiYWNrZ3JvdW5kOiAjRDBEMEQwOyB9DQoJMTAwJSB7IGJhY2tncm91bmQ6ICNFMEUwRTA7IH0NCn0=', 'base64'); - - buffer = []; - buffer.push("var app = angular.module('app', []);"); - buffer.push(''); - fs.writeFileSync(path.join(dir, 'app.js'), buffer.join(EOF)); - - buffer = []; - buffer.push('function HomeCtrl($scope) {\n $scope.name = \'total.js + angular.js = awesome\';\n}'); - fs.writeFileSync(path.join(controllers, 'home.js'), buffer.join(EOF)); - - buffer = []; - buffer.push('function UserCtrl($scope) {\n // example\n}'); - fs.writeFileSync(path.join(controllers, 'user.js'), buffer.join(EOF)); -} - -function createFilePublic(directory) { - - var dir = path.join(directory, 'public'); - var css = path.join(dir, 'css'); - var js = path.join(dir, 'js'); - var img = path.join(dir, 'img'); - - fs.writeFileSync(path.join(dir, 'favicon.ico'), 'AAABAAIAGBgAAAEAIAAoCQAAJgAAABAQAAABACAAKAQAAE4JAAAoAAAAGAAAADAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGsAAADxAAAA/wAAAP4AAAD+AAAA/gAAAP4AAAD+AAAA/gAAAP4AAAD+AAAA/gAAAP4AAAD+AAAA/gAAAP4AAAD+AAAA/gAAAP8AAADwAAAAZAAAAAAAAAAAAAAAAAAAAPEAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA7wAAAAAAAAAAAAAAAAAAAP8AAAD/AAEB/wMJBv8ECwj/BAsI/wQLCP8ECwj/BAsI/wQLCP8ECwj/BAsI/wQLCP8ECwj/BAsI/wQLCP8ECwj/AwkG/wAAAP8AAAD/AAAA/wAAAAAAAAAAAAAAAAAAAP8AAAD/AwoH/yyGXf83qHT/NqRx/zakcf82pHH/NqRx/zakcf82pHH/NqRx/zakcf82pHH/NqRx/zakcf83qHT/LIVc/wMJBv8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAwI/ziodP9F0pL/Q82O/0TPj/9Ez4//RM+P/0TPj/9Ez4//RM+P/0TPj/9Ez4//RM+P/0PNjv9F0pL/N6h0/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9DzY7/QseK/0DChv8/wYX/P8GF/z/Ahf8/wIX/P8CF/z/Bhf8/v4P+QMKG/0LHiv9DzY7/NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CG/xU+K/8LIhf/DCUa/wwlGv8MJRr/DCUa/wwlGv8LIhf/FT8s/z/Bhv9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CE/wofFf8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/CiAW/z/Ahf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CE/wsiGP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/DCMY/z/Bhf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CE/wsiGP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/DCMY/z/Bhf9Ez4//NqRx/wQLCP8AAAD+AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CF/wsiGP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/DCMY/z/Bhf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CE/wsiGP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/DCMY/z/Bhf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CE/wsiGP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/DCMY/z/Bhf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CF/wsiGP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/DCMY/z/Bhf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CE/wsiGP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/DCMY/z/Bhf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CE/wsiGP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/DCMY/z/Bhf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CE/wofFf8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/CiAW/z/Bhf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8GF/xQ9Kv8LIRf/DCQZ/wwlGf8MJRn/DCUZ/wwkGf8LIRf/FT4r/z/Bhv9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9DzY7/QseK/0DChf8/wYT/P8GF/z/Bhf8/wYX/P8GF/z/Bhf8/wYT/QMKF/0LHiv9DzY7/NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAwI/ziodf9F0pL/Q82O/0TOj/9Ez4//RM+P/0TPj/9Ez4//RM+P/0TPj/9Ez4//RM6P/0PNjv9F0pL/N6h0/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/AwoH/yyHXv83qHT/NqRx/zakcf82pHH/NqVx/zalcf82pXH/NqVx/zalcf82pHH/NqRx/zakcf83qHX/LIZd/wMIBv8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/AAEB/wMJBv8ECwj/BAsI/wQLCP8ECwj/BAsI/wQLCP8ECwj/BAsI/wQLCP8ECwj/BAsI/wQLCP8ECwj/AwkG/wAAAf8AAAD/AAAA/wAAAAAAAAAAAAAAAAAAAOMAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA6wAAAAAAAAAAAAAAAAAAAEcAAADgAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADuAAAAWAAAAAAAAAAAKAAAABAAAAAgAAAAAQAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoAAADPAAAA7QAAAO0AAADtAAAA7QAAAO0AAADtAAAA7QAAAO0AAADtAAAA7QAAAO0AAADlAAAAYgAAAAAAAABYAAAA/wEEA/8FDwv/BQ8L/wUPC/8FDwv/BQ8L/wUPC/8FDwv/BQ8L/wUQC/8DCQb/AAAA/wAAAK4AAAAAAAAAVQAAAP8QMiP/OKl1/zmtd/85rnj/Oa54/zmueP85rnj/Oa54/zisdv86sXv/IGFD/wAAAP8AAACqAAAAAAAAAFUAAAD/FT4r/0XTkv9E0JD/QMSH/0DEh/9AxIj/QMSI/0DChf5Dyov/Sd2Z/yh6Vf8AAAD/AAAAqQAAAAAAAABVAAAA/xQ7Kf9G1JP/M5pr/wshFv8LIRf/CyIY/wsiGP8JHRT/ImZI/0jcmP8ndlH/AAAA/wAAAKkAAAAAAAAAVQAAAP8UOyn/RtaU/y+QYv8AAAD/AAAA/wAAAP8AAAD/AAAA/xpROP9J3pn/J3ZR/wAAAP8AAACpAAAAAAAAAFUAAAD/FDsp/0bWlP8wkmT/AAAA/wAAAP8AAAD/AAAA/wAAAP8cVjv/Sd6Z/yd2Uf8AAAD/AAAAqQAAAAAAAABVAAAA/xQ7Kf9G1pT/MJJk/wAAAP8AAAD/AAAA/wAAAP8AAAD/HFY7/0nemf8ndlH/AAAA/wAAAKkAAAAAAAAAVQAAAP8UOyn/RtaU/zCSZP8AAAD/AAAA/wAAAP8AAAD/AAAA/xxWO/9J3pn/J3ZR/wAAAP8AAACpAAAAAAAAAFUAAAD/FDsp/0bWlP8wkmT/AAAA/wAAAP8AAAD/AAAA/wAAAP8cVjv/Sd6Z/yd2Uf8AAAD/AAAAqQAAAAAAAABVAAAA/xQ7Kf9G1pT/L5Bi/wAAAP8AAAD/AAAA/wAAAP8AAAD/GlI4/0nemf8ndlH/AAAA/wAAAKkAAAAAAAAAVQAAAP8UOyn/RtST/zOaav8LIBb/CyEW/wsjGP8LIhj/CRwT/yJnR/9I3Jj/J3ZR/wAAAP8AAACpAAAAAAAAAFUAAAD/FT4r/0XTk/9E0JD/QMSH/0DEh/9AxIf/QMSH/0DDhv9Dyov/Sd2Z/yh6VP8AAAD/AAAAqQAAAAAAAABWAAAA/xAyI/84qXb/Oa13/zmueP85rnj/Oa54/zmueP85rnj/OKx2/zqxe/8gYUT/AAAA/wAAAKoAAAAAAAAAVgAAAP8BBAP/BQ8L/wUPC/8FDwv/BQ8L/wUPC/8FDwv/BQ8L/wUPC/8FEAv/AwkG/wAAAP8AAACuAAAAAAAAACAAAADAAAAA7QAAAO0AAADtAAAA7QAAAO0AAADtAAAA7QAAAO0AAADtAAAA7QAAAO0AAADkAAAAXQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'base64'); - - if ($type === 3) - return; - - if (!fs.existsSync(js)) - fs.mkdirSync(js); - - if (!fs.existsSync(css)) - fs.mkdirSync(css); - - if (!fs.existsSync(img)) - fs.mkdirSync(img); - - var buffer = []; - buffer.push('User-agent: *'); - buffer.push('Allow: /'); - buffer.push(''); - - fs.writeFileSync(path.join(dir, 'robots.txt'), buffer.join(EOF)); - fs.writeFileSync(path.join(css, 'default.css'), 'LyphdXRvKi8NCg0KLyoNCiAgICB2YXIgY29sb3IgPSAnY29sb3I6cmVkJzsNCiAgICB2YXIgd2lkdGggPSAnOTQwcHgnOw0KKi8NCg0KYm9keSB7IHBhZGRpbmc6MjBweDsgbWFyZ2luOjA7IGZvbnQ6bm9ybWFsIDEycHggQXJpYWw7IGNvbG9yOiM1MDUwNTA7IH0NCg0KLmNvbnRlbnQgeyBtYXJnaW46MCBhdXRvOyB3aWR0aDogJHdpZHRoOyBwYWRkaW5nOiAxMHB4OyBib3JkZXItcmFkaXVzOiA1cHg7ICRjb2xvcjsgYm94LXNoYWRvdzogMCAwIDIwcHggcmdiYSgwLDAsMCwwLjUpOyBhbmltYXRpb246IG15QW5pbWF0aW9uIDVzIGFsdGVybmF0ZTsgfQ0KDQoubGlzdCB7IGJvcmRlci1ib3R0b206MXB4IHNvbGlkICNFMEUwRTA7IHBhZGRpbmctYm90dG9tOjVweDsgbWFyZ2luLWJvdHRvbTo1cHg7IH0NCi5saXN0ID4gZGl2OmZpcnN0LWNoaWxkIHsgZm9udC1zaXplOiAxNXB4OyB9DQoNCkBrZXlmcmFtZXMgbXlBbmltYXRpb24NCnsNCgkwJSAgIHsgYmFja2dyb3VuZDogd2hpdGU7IH0NCgkyNSUgIHsgYmFja2dyb3VuZDogI0YwRjBGMDsgfQ0KCTUwJSAgeyBiYWNrZ3JvdW5kOiAjRDBEMEQwOyB9DQoJMTAwJSB7IGJhY2tncm91bmQ6ICNFMEUwRTA7IH0NCn0=', 'base64'); - - buffer = []; - buffer.push('$(document).ready(function() {'); - buffer.push(''); - buffer.push('});'); - - fs.writeFileSync(path.join(js, 'default.js'), buffer.join(EOF)); -} - -function createFileModules(directory) { - - var dir = path.join(directory, 'modules'); - - var buffer = []; - buffer.push('exports.yourcode = function () {'); - buffer.push(' return \'Hello World\';'); - buffer.push('};'); - - fs.writeFileSync(path.join(dir, 'example.js'), buffer.join(EOF)); -} - -function createFileControllerAngular(directory) { - - var dir = path.join(directory, 'controllers'); - var buffer = []; - - buffer.push('exports.install = function(framework) {'); - buffer.push(' framework.route(\'/*\', view_app);'); - buffer.push('};'); - buffer.push(''); - buffer.push('function view_app() {'); - buffer.push(' var self = this;'); - buffer.push(' self.view(\'app\');'); - buffer.push('}'); - - fs.writeFileSync(path.join(dir, 'default.js'), buffer.join(EOF)); -} - -function createFileController(directory) { - - var dir = path.join(directory, 'controllers'); - var buffer = []; - - buffer.push('exports.install = function(framework) {'); - buffer.push(' framework.route(\'/\', view_homepage);'); - buffer.push(' framework.route(\'#400\', error400);'); - buffer.push(' framework.route(\'#401\', error401);'); - buffer.push(' framework.route(\'#403\', error403);'); - buffer.push(' framework.route(\'#404\', error404);'); - buffer.push(' framework.route(\'#408\', error408);'); - buffer.push(' framework.route(\'#431\', error431);'); - buffer.push(' framework.route(\'#500\', error500);'); - buffer.push('};'); - buffer.push(''); - buffer.push('// Bad Request'); - buffer.push('function error400() {'); - buffer.push(' var self = this;'); - buffer.push(' self.status = 400;'); - buffer.push(' self.plain(utils.httpStatus(self.status));'); - buffer.push('}'); - buffer.push(''); - buffer.push('// Unauthorized'); - buffer.push('function error401() {'); - buffer.push(' var self = this;'); - buffer.push(' self.status = 401;'); - buffer.push(' self.plain(utils.httpStatus(self.status));'); - buffer.push('}'); - buffer.push(''); - buffer.push('// Forbidden'); - buffer.push('function error403() {'); - buffer.push(' var self = this;'); - buffer.push(' self.status = 403;'); - buffer.push(' self.plain(utils.httpStatus(self.status));'); - buffer.push('}'); - buffer.push(''); - buffer.push('// Not Found'); - buffer.push('function error404() {'); - buffer.push(' var self = this;'); - buffer.push(' self.status = 404;'); - buffer.push(' self.plain(utils.httpStatus(self.status));'); - buffer.push('}'); - buffer.push(''); - buffer.push('// Request Timeout'); - buffer.push('function error408() {'); - buffer.push(' var self = this;'); - buffer.push(' self.status = 408;'); - buffer.push(' self.plain(utils.httpStatus(self.status));'); - buffer.push('}'); - buffer.push(''); - buffer.push('// Request Header Fields Too Large'); - buffer.push('function error431() {'); - buffer.push(' var self = this;'); - buffer.push(' self.status = 431;'); - buffer.push(' self.plain(utils.httpStatus(self.status));'); - buffer.push('}'); - buffer.push(''); - buffer.push('// Internal Server Error'); - buffer.push('function error500() {'); - buffer.push(' var self = this;'); - buffer.push(' self.status = 500;'); - buffer.push(' self.plain(utils.httpStatus(self.status));'); - buffer.push('}'); - buffer.push(''); - buffer.push('function view_homepage() {'); - buffer.push(' var self = this;'); - - if ($type !== 2) - buffer.push(' self.view(\'homepage\');'); - else - buffer.push(' self.plain(\'homepage\');'); - - buffer.push('}'); - - fs.writeFileSync(path.join(dir, 'default.js'), buffer.join(EOF)); -} - -function createFileTest(directory) { - var dir = path.join(directory, 'tests'); - var buffer = []; - - buffer.push("var assert = require('assert');"); - buffer.push(""); - buffer.push("exports.run = function(framework) {"); - buffer.push(""); - buffer.push(" framework.assert('Number validation', function(next, name) {"); - buffer.push(" assert.ok('1' === '1', name);"); - buffer.push(" next();"); - buffer.push(" });"); - buffer.push(""); - buffer.push(" framework.assert('Homepage', '/1/', ['xhr', 'json'], function(error, data, code, headers, cookies, name) {"); - buffer.push(" assert.ok(code === 200, name);"); - buffer.push(" }, { name: 'total.js (optional)' }, { cookie: 'value (optional)' }, { 'X-My-Header': 'optional' });"); - buffer.push(""); - buffer.push("};"); - - fs.writeFileSync(path.join(dir, 'default.js'), buffer.join(EOF)); -} - -function createFileComponent(directory) { - var dir = path.join(directory, 'components'); - var buffer = []; - - buffer.push("// optional"); - buffer.push("exports.install = function(framework) {"); - buffer.push(" // component doesn't support routing"); - buffer.push(" console.log('OK');"); - buffer.push("};"); - buffer.push(""); - buffer.push("// optional"); - buffer.push("exports.usage = function(isDetailed) {"); - buffer.push(" return '';"); - buffer.push("};"); - buffer.push(""); - buffer.push("// optional"); - buffer.push("exports.configure = function(setup) {"); - buffer.push(""); - buffer.push("};"); - buffer.push(""); - buffer.push("// REQUIRED"); - buffer.push("exports.render = function(data) {"); - buffer.push(" // this === controller or this === framework"); - buffer.push(" return '';"); - buffer.push("}; "); - - fs.writeFileSync(path.join(dir, 'example.js'), buffer.join(EOF)); -} - -function install(directory) { - exec('npm install total.js', { cwd: directory }, function (error, stdout, stderr) { - - }).on('exit', function() { - console.log('total.js: success'); - }); -} - -function display_help() { - console.log(''); - console.log('-m or -minimal = minimal'); - console.log('-n or -normal = normal (default)'); - console.log('-f or -full = full'); - console.log('-a or -angular = for angular.js application'); - console.log('-v or -version = total.js version'); - console.log('/path/ = target (default current directory)'); - console.log(''); -} - -function main() { - var dir = process.cwd(); - - for (var i = 2; i < process.argv.length; i++) { - var arg = process.argv[i]; - var cmd = arg.toLowerCase(); - - if (cmd === '-v' || cmd === '-version') { - console.log(require('total.js').version); - return; - } - - if (cmd === '-f' || cmd === '-full' || cmd === 'full') - continue; - - if (cmd === '-a' || cmd === '-angular' || cmd === 'angular') { - $type = 3; - continue; - } - - if (cmd === '-m' || cmd === '-minimal' || cmd === '-minimum' || cmd === 'minimum') { - $type = 2; - continue; - } - - if (cmd === '-n' || cmd === '-normal' || cmd === 'normal') { - $type = 1; - continue; - } - - if (cmd === '-h' || cmd === '-help' || cmd === '--help' || cmd === 'help') { - display_help(); - return; - } - - dir = arg; - break; - } - - if (!fs.existsSync(dir)) { - console.log('total.js: error / directory not exists'); - return; - } - - var files = fs.readdirSync(dir); - if (files.length > 0) { - - var can = true; - for (var i = 0; i < files.length; i++) { - var name = files[i]; - if (name[0] === '.') - continue; - can = false; - } - - if (!can) { - console.log('total.js: error / directory is not empty'); - return; - } - } - - console.log('total.js: creating directories'); - - if ($type !== 3) - createDirectory(dir); - else - createDirectoryAngular(dir); - - console.log('total.js: creating files'); - createFiles(dir); - - console.log('total.js: installing current version total.js'); - install(dir); -} - -main(); \ No newline at end of file diff --git a/bin/totaljs b/bin/totaljs new file mode 100755 index 000000000..8ce0e9ab4 --- /dev/null +++ b/bin/totaljs @@ -0,0 +1,971 @@ +#! /usr/bin/env node + +var exec = require('child_process').exec; +var fs = require('fs'); +var path = require('path'); +var os = require('os'); +var Utils = require('total.js/utils'); +var Internal = require('total.js/internal'); +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('-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'); + console.log('-translatecsv : parses and creates CSV with localization in the current directory'); + console.log('-csv filename : parses CSV and creates resources from CSV file'); + console.log('-diff source target : creates differences between two resources "-diff source target"'); + 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('-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(''); +} + +function translateFile(a) { + + if (!fs.existsSync(a)) + return false; + + var arr = fs.readFileSync(a).toString('utf8').split('\n'); + var builder = []; + var count = 0; + + for (var i = 0, length = arr.length; i < length; i++) { + var line = arr[i].trim(); + if (line.length === 0) + continue; + builder.push('T' + line.hash().padRight(17, ' ') + ': ' + line); + count++; + } + + fs.writeFileSync('translate.resource', '// Total.js translation file\n// Created: ' + new Date().format('yyyy-MM-dd HH:mm') + '\n' + builder.join('\n')); + console.log('Total.js: the translation was created (' + count + ' texts)'); + return true; +} + +function diff(a, b) { + + if (!fs.existsSync(a)) { + console.log('The translation file does not exist: ' + a); + return; + } + + if (!fs.existsSync(b)) { + console.log('The translation file does not exist: ' + b); + return; + } + + var ba = fs.readFileSync(a).toString('utf8'); + var bb = fs.readFileSync(b).toString('utf8'); + var ca = ba.parseConfig(); + var cb = bb.parseConfig(); + var ka = Object.keys(ca); + var kb = Object.keys(cb); + + ba = ba.split('\n'); + bb = bb.split('\n'); + + var output = ''; + var items = []; + var add = 0; + var rem = 0; + var padding = 0; + + for (var i = 0, length = ba.length; i < length; i++) { + if (ba[i].indexOf(ka[0]) !== -1) { + padding = ba[i].indexOf(':'); + break; + } + } + + if (padding <= 0) + padding = 17; + + function find_comment(arr, id) { + var comment = ''; + for (var i = 0, length = arr.length; i < length; i++) { + if (arr[i].indexOf(id) !== -1) + return comment; + var line = arr[i]; + if (line[0] !== '/' && line[1] !== '/') + continue; + comment = line; + } + return ''; + } + + var comment = ''; + var prev = ''; + + for (var i = 0, length = ka.length; i < length; i++) { + var key = ka[i]; + + if (cb[key] !== undefined) + continue; + + comment = find_comment(ba, key); + + if (comment) { + if (items[items.length - 1] !== '') + items.push(''); + items.push(comment); + } + + var empty = comment === prev; + + prev = comment; + items.push(key.padRight(padding) + ': ' + ca[key]); + + if (!empty) + items.push(''); + + add++; + } + + if (items.length > 0) { + output += '\n'; + output += 'Add to "' + b + '" these:\n'; + output += '\n'; + output += items.join('\n'); + output += '\n'; + } + + items = []; + + for (var i = 0, length = kb.length; i < length; i++) { + var key = kb[i]; + + if (ca[key] !== undefined) + continue; + + comment = find_comment(bb, key); + + if (comment) { + if (items[items.length - 1] !== '') + items.push(''); + items.push(comment); + } + else if (prev !== '') + items.push(''); + + var empty = comment === prev; + + prev = comment; + items.push(key.padRight(padding) + ': ' + cb[key]); + + if (!empty) + items.push(''); + + rem++; + } + + if (items.length) { + output += '\n'; + output += 'Remove from "' + b + '" these:\n'; + output += '\n'; + output += items.join('\n'); + output += '\n'; + } + + var filename = path.join(path.dirname(b), path.basename(b, '.resource') + '-diff.txt'); + fs.writeFileSync(filename, '// Total.js diff file\n// Created: ' + new Date().format('yyyy-MM-dd HH:mm') + '\n' + clean_resource(output)); + console.log('========================================'); + console.log('Translation files differences:'); + console.log('========================================'); + console.log(''); + console.log('added : ' + add); + console.log('removed : ' + rem); + console.log('output : ' + filename); + console.log(''); +} + +function merge(a, b) { + if (!fs.existsSync(a)) { + console.log('The translation file does not exist: ' + a); + return; + } + + if (!fs.existsSync(b)) { + console.log('The translation file does not exist: ' + b); + return; + } + + var ba = fs.readFileSync(b).toString('utf8'); + var bb = fs.readFileSync(a).toString('utf8'); + var arr = ba.split('\n'); + var output = []; + var cb = bb.parseConfig(); + var upd = 0; + + for (var i = 0, length = arr.length; i < length; i++) { + + var line = arr[i]; + if (!line || line[0] === '#' || line.startsWith('//')) { + output.push(line); + continue; + } + + var index = line.indexOf(' :'); + if (index === -1) { + index = line.indexOf('\t:'); + if (index === -1) { + output.push(line); + continue; + } + } + + var key = line.substring(0, index).trim(); + var val = cb[key]; + if (!val) { + output.push(line); + continue; + } + + upd++; + output.push(key.padRight(index) + ' : ' + val); + } + + var filename = path.join(path.dirname(b), path.basename(b, '.resource') + '-merged.txt'); + fs.writeFileSync(filename, '// Total.js merged file\n// Created: ' + new Date().format('yyyy-MM-dd HH:mm') + '\n' + clean_resource(output.join('\n'))); + console.log('========================================'); + console.log('Merged result:'); + console.log('========================================'); + console.log(''); + console.log('merged : ' + upd); + console.log('output : ' + filename); + console.log(''); +} + +function clean_resource(content) { + var lines = content.split('\n'); + var output = []; + var max = 0; + + for (var i = 0, length = lines.length; i < length; i++) { + var line = lines[i]; + if (!line || line[0] === '#' || line.startsWith('//')) + continue; + + var index = line.indexOf(' :'); + if (index === -1) { + index = line.indexOf('\t:'); + if (index === -1) + continue; + } + + max = Math.max(max, index); + } + + for (var i = 0, length = lines.length; i < length; i++) { + var line = lines[i]; + if (!line || line[0] === '#' || line.startsWith('//')) { + output.push(line); + continue; + } + + var index = line.indexOf(' :'); + if (index === -1) { + index = line.indexOf('\t:'); + if (index === -1) { + output.push(line); + continue; + } + } + + var key = line.substring(0, index).trim(); + output.push(key.padRight(max, ' ') + ' : ' + line.substring(index + 2).trim()); + } + + return output.join('\n'); +} + +function parse_csv(content) { + + var output = {}; + var max = 0; + 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; + + Object.keys(output).forEach(function(key) { + builder.push('{0}: {1}'.format(key.padRight(max, ' '), output[key])); + }); + + return '\n' + builder.join('\n'); +} + +function main() { + + console.log(''); + console.log('|==================================================|'); + console.log('| Total.js - www.totaljs.com |'); + console.log('| Version: v' + require('total.js').version_header.padRight(39) + '|'); + console.log('|==================================================|'); + console.log(''); + + var dir = process.cwd(); + for (var i = 2; i < process.argv.length; i++) { + var arg = process.argv[i]; + var cmd = arg.toLowerCase(); + + if (cmd.substring(0, 2) === '--') + cmd = cmd.substring(1); + + if (i === 2) { + var port = cmd.parseInt(); + if (port) { + + 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'); + + F.http('debug', { port: port, directory: dir }); + + F.route('/*', function() { + + var self = this; + var dir = F.path.public(self.url.substring(1)); + var filename = path.join(self.url, 'index.html').substring(1); + + F.path.exists(filename, function(e) { + + if (e) + return self.file(filename, ''); + + fs.readdir(dir, function(err, items) { + + var render = function(controller, directories, files) { + controller.content('Directory listing: {0}
{1}
{2}
'.format(controller.url, directories.join(''), files.join('')), 'text/html'); + }; + + var directories = []; + var files = []; + + if (self.url !== '/') + directories.push('..'); + + if (err) + return render(self, directories, files); + + items.wait(function(item, next) { + var filename = path.join(dir, item); + fs.stat(filename, function(err, info) { + + if (info.isFile()) + files.push('{0}{2}'.format(item, self.url + item, info.size.filesize())); + else + directories.push('{0}'.format(item, self.url + item)); + + next(); + }); + }, () => render(self, directories, files)); + }); + + }); + }); + + 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 ? headers['content-type'] : 'text/plain')); + }, ['get', 'post', 'put', 'delete'], 5120); + + return; + } + } + + if (!$type && (cmd === '-v' || cmd === '-version')) + return; + + if (!$type && (cmd === '-t' || cmd === '-translate')) { + $type = 4; + continue; + } + + if (!$type && cmd === '-merge') { + merge(process.argv[i + 1] || '', process.argv[i + 2] || ''); + return; + } + + if (!$type && (cmd === '-translate-csv' || cmd === '-translatecsv' || cmd === '-c')) { + $type = 6; + continue; + } + + 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'))); + console.log('========================================'); + console.log('Parsed CSV:'); + console.log('========================================'); + console.log(''); + console.log('output : ' + tt); + console.log(''); + continue; + } + + if (!$type && (cmd === '-i' || cmd === '-install')) { + + 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, spa'); + } + + 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/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 'spa': + case 'spa.min': + url = 'https://rawgit.com/totaljs/components/master/0dependencies/spa.min.js'; + break; + case 'spa@14': + case 'spa.min@14': + url = 'https://rawgit.com/totaljs/components/master/0dependencies/spa.min@14.js'; + break; + case 'spa@15': + case 'spa.min@15': + url = 'https://rawgit.com/totaljs/components/master/0dependencies/spa.min@15.js'; + break; + case 'ta': + url = 'https://rawgit.com/totaljs/Tangular/master/Tangular.js'; + break; + case 'jr': + url = 'https://rawgit.com/totaljs/jRouting/master/jrouting.js'; + break; + case 'jr.jc': + url = 'https://rawgit.com/totaljs/jRouting/master/jrouting.jcomponent.js'; + break; + } + + U.download(url, [], function callback(err,response) { + var target = fs.createWriteStream(path.join(dir, './' + tmp + '.js')); + response.pipe(target); + console.log('Done!'); + }); + return; + } + + if (cmd === '-minify' || cmd === '-compress' || cmd === '-compile') { + $type = 5; + break; + } + + 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'))); + console.log('========================================'); + console.log('Cleaned result:'); + console.log('========================================'); + console.log(''); + console.log('output : ' + tt); + console.log(''); + return; + } + + 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; + } + + if (cmd === '-a' || cmd === '-angular' || cmd === 'angular') { + $type = 3; + continue; + } + + if (!$type && (cmd === '-m' || cmd === '-minimal' || cmd === '-minimum' || cmd === 'minimum')) { + $type = 2; + continue; + } + + if (!$type && (cmd === '-n' || cmd === '-normal' || cmd === 'normal')) { + $type = 1; + continue; + } + + if (!$type && (cmd === '-h' || cmd === '-help' || cmd === '--help' || cmd === 'help')) { + display_help(); + return; + } + + dir = arg; + isDirectory = true; + } + + if (!$type) + $type = 1; + + if (dir === '.') + dir = process.cwd(); + + if ($type === 5) { + + if (!fs.existsSync(dir)) { + console.log('ERROR: file not found'); + console.log(''); + return; + } + + var content = fs.readFileSync(dir).toString('utf8'); + var extension = U.getExtension(dir); + var filename = dir.replace('.' + extension, '.min.' + extension); + + switch (extension.toLowerCase()) { + case 'html': + fs.writeFileSync(filename, Utils.minifyHTML(content)); + break; + case 'js': + fs.writeFileSync(filename, Utils.minifyScript(content)); + break; + case 'css': + fs.writeFileSync(filename, Utils.minifyStyle(content)); + break; + } + + console.log('Minified: ' + filename); + return; + } + + if ($type !== 4) { + if (!fs.existsSync(dir)) { + console.log('ERROR: directory does not exist'); + console.log(''); + return; + } + } + + if ($type === 4) { + + if (isDirectory) { + if (translateFile(dir)) + return; + console.log('T' + dir.hash().padRight(17, ' ') + ': ' + dir); + return; + } + + console.log('Total.js: creating translation'); + Utils.ls(dir, function(files) { + + var resource = {}; + 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]; + var ext = Utils.getExtension(filename); + + if (filename.indexOf('sitemap') === -1 && ext !== 'html' && ext !== 'js') + continue; + + var content = fs.readFileSync(filename).toString('utf8'); + var command = Internal.findLocalization(content, 0); + while (command !== null) { + + // Skip for direct reading + if (command.command[0] === '#' && command.command[1] !== ' ') { + command = Internal.findLocalization(content, command.end); + continue; + } + + 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 + 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); + var builder = []; + var output = {}; + + for (var i = 0, length = keys.length; i < length; i++) { + if (!output[resource[keys[i]]]) + output[resource[keys[i]]] = []; + output[resource[keys[i]]].push(keys[i].padRight(max + 5, ' ') + ': ' + texts[keys[i]]); + } + + keys = Object.keys(output); + for (var i = 0, length = keys.length; i < length; i++) + builder.push('\n// ' + keys[i] + '\n' + output[keys[i]].join('\n')); + + fs.writeFileSync('translate.resource', '// Total.js translation file\n// Created: ' + new Date().format('yyyy-MM-dd HH:mm') + '\n' + builder.join('\n')); + console.log('Total.js: the translation was created (' + count + ' texts)'); + }, (path, dir) => dir ? (path.endsWith('/node_modules') || path.endsWith('/tmp') || path.endsWith('/.git')) ? false : true : true); + return; + } + + if ($type === 6) { + console.log('Total.js: creating translation to CSV'); + Utils.ls(dir, function(files) { + + var resource = {}; + var texts = {}; + var count = 0; + var output = ['Hash;Text;Translation']; + + for (var i = 0, length = files.length; i < length; i++) { + var filename = files[i]; + var ext = Utils.getExtension(filename); + + if (ext !== 'html' && ext !== 'js') + continue; + + var content = fs.readFileSync(filename).toString('utf8'); + var command = Internal.findLocalization(content, 0); + while (command !== null) { + + // Skip for direct reading + if (command.command[0] === '#' && command.command[1] !== ' ') { + command = Internal.findLocalization(content, command.end); + continue; + } + + var key = 'T' + command.command.hash(); + + texts[key] = command.command; + + if (!resource[key]) { + output.push(key + ';"' + command.command.replace(/"/g, '""') + '";'); + resource[key] = true; + count++; + } + + command = Internal.findLocalization(content, command.end); + } + } + + fs.writeFileSync('translate.csv', output.join('\n')); + console.log('Total.js: the translation was created (' + count + ' texts)'); + }, (path, dir) => dir ? (path.endsWith('/node_modules') && path.endsWith('/tmp') && path.endsWith('/.git')) ? false : true : true); + return; + } + + var files = fs.readdirSync(dir); + if (files.length > 0) { + + var can = true; + for (var i = 0; i < files.length; i++) { + var name = files[i]; + if (name[0] !== '.') + can = false; + } + + if (!can) { + console.log('ERROR: directory is not empty'); + console.log(''); + return; + } + } + + git(dir, 'emptyproject'); +} + +function git(dir, type) { + + var done = function() { + console.log('Installed: {0}'.format(type)); + console.log(); + }; + + U.ls(dir, function(fol, fil) { + + if (fol.length || fil.length) { + console.log('Directory "{0}"" is not empty.'.format(dir)); + console.log(); + return; + } + + 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); + }); + }); + }); + }); + }); +} + +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 d671e18c1..a55e7e928 100755 --- a/bin/tpm +++ b/bin/tpm @@ -1,922 +1,1060 @@ +#! /usr/bin/env node + 'use strict'; -var VERSION = 'v1.5.1'; - -var fs = require('fs'); -var ph = require('path'); -var parser = require('url'); -var zlib = require('zlib'); -var http = require('http'); -var https = require('https'); -var padding = 120; -var exec = require('child_process').exec; -var os = require('os'); -var EOF = os.platform() === 'win32' ? '\r\n' : '\n'; +const VERSION = 'v4.0.0'; +const PADDING = 120; + +const Fs = require('fs'); +const Path = require('path'); +const parser = require('url'); +const Zlib = require('zlib'); +const Http = require('http'); +const Https = require('https'); +const Os = require('os'); +const REG_EMPTY = /\s/g; +const isWindows = Os.platform().substring(0, 3).toLowerCase() === 'win'; +const NODEVERSION = parseFloat(process.version.toString().replace('v', '').replace(/\./g, '')); + var $type = 0; var settings = {}; -var colors = { reset: '\x1b[0m', underscore: '\x1b[4m', dim: '\x1b[2m', reverse: '\x1b[7m', white: '\x1b[37m', red: '\x1b[31m', bgRed: '\x1b[41m', bgGreen: '\x1b[42m' }; +var colors = { reset: '\x1b[0m', underscore: '\x1b[4m', dim: '\x1b[33m', reverse: '\x1b[7m', white: '\x1b[37m', red: '\x1b[31m', bgRed: '\x1b[41m', bgGreen: '\x1b[42m' }; var current_repository = 'default'; var current_package = ''; +if (NODEVERSION > 699) { + global.createBufferSize = (size) => Buffer.alloc(size || 0); + global.createBuffer = (val, type) => Buffer.from(val || '', type); +} else { + global.createBufferSize = (size) => new Buffer(size || 0); + global.createBuffer = (val, type) => new Buffer(val || '', type); +} + + String.prototype.padRight = function(max, c) { - var self = this; - return self + new Array(Math.max(0, max - self.length + 1)).join(c || ' '); + var self = this; + return self + new Array(Math.max(0, max - self.length + 1)).join(c || ' '); }; function Backup() { - this.file = []; - this.directory = []; - this.path = ''; - this.fileName = ''; - - this.read = { key: '', value: '', status: 0 }; - - this.complete = function() {}; - this.filter = function(path) { - return true; - }; + this.file = []; + this.directory = []; + this.path = ''; + this.filename = ''; + this.pending = 0; + + this.read = { key: createBufferSize(), value: createBufferSize(), status: 0 }; + this.bufKey = createBuffer(':'); + this.bufNew = createBuffer('\n'); + + this.complete = function() {}; + this.filter = () => true; } function Walker() { - this.pending = []; - this.pendingDirectory = []; - this.directory = []; - this.file = []; - this.options = { sort: true, addEmptyDirectory: false }; - this.onComplete = null; - this.onFilter = null; + this.pending = []; + this.pendingDirectory = []; + this.directory = []; + this.file = []; + this.options = { sort: true, addEmptyDirectory: false }; + this.onComplete = null; + this.onFilter = null; } Walker.prototype.reset = function() { - var self = this; - self.file = []; - self.directory = []; - self.pendingDirectory = []; + var self = this; + self.file = []; + self.directory = []; + self.pendingDirectory = []; }; Walker.prototype.walk = function(path) { - var self = this; + var self = this; - if (path instanceof Array) { - var length = path.length; + if (path instanceof Array) { + var length = path.length; - for (var i = 0; i < length; i++) - self.pendingDirectory.push(path[i]); + for (var i = 0; i < length; i++) + self.pendingDirectory.push(path[i]); - self.next(); - return; - } + self.next(); + return; + } - fs.readdir(path, function(err, arr) { + Fs.readdir(path, function(err, arr) { - if (err) - return self.next(); + if (err) + return self.next(); - if (arr.length === 0 || self.options.addEmptyDirectory) { - if (self.onFilter === null || self.onFilter(path)) - self.directory.push(path); - } + if (arr.length === 0 || self.options.addEmptyDirectory) { + if (self.onFilter === null || self.onFilter(path)) + self.directory.push(path); + } - var length = arr.length; - for (var i = 0; i < length; i++) - self.pending.push(ph.join(path, arr[i])); + var length = arr.length; + for (var i = 0; i < length; i++) + self.pending.push(Path.join(path, arr[i])); - self.next(); - }); + self.next(); + }); }; Walker.prototype.stat = function(path) { - var self = this; + var self = this; - fs.stat(path, function(err, stats) { + Fs.stat(path, function(err, stats) { - if (err) - return self.next(); + if (err) + return self.next(); - if (stats.isDirectory()) - self.pendingDirectory.push(path); - else if (self.onFilter === null || self.onFilter(path)) - self.file.push(path); + if (stats.isDirectory()) + self.pendingDirectory.push(path); + else if (!self.onFilter || self.onFilter(path)) + self.file.push(path); - self.next(); - }); + self.next(); + }); }; Walker.prototype.next = function() { - var self = this; - - if (self.pending.length > 0) { - var item = self.pending.shift(); - self.stat(item); - return; - } - - if (self.pendingDirectory.length > 0) { - var directory = self.pendingDirectory.shift(); - self.walk(directory); - return; - } - - if (self.options.sort) { - self.file.sort(function(a, b) { - return a.localeCompare(b); - }); - } - - self.onComplete(self.directory, self.file); -}; - -Backup.prototype.backup = function(path, fileName, callback, filter) { - - if (fs.existsSync(fileName)) - fs.unlinkSync(fileName); + var self = this; - var walker = new Walker(); - var self = this; + if (self.pending.length) { + var item = self.pending.shift(); + self.stat(item); + return; + } - self.fileName = fileName; - self.path = path; + if (self.pendingDirectory.length) { + var directory = self.pendingDirectory.shift(); + self.walk(directory); + return; + } - if (callback) - self.complete = callback; + self.options.sort && self.file.sort(function(a, b) { + return a.localeCompare(b); + }); - if (filter) - self.filter = filter; - - walker.onComplete = function(directory, files) { - self.directory = directory; - self.file = files; - self.$compress(); - }; - - walker.walk(path); + self.onComplete(self.directory, self.file); }; +Backup.prototype.backup = function(path, filename, callback, filter) { + + Fs.existsSync(filename) && Fs.unlinkSync(filename); + + var walker = new Walker(); + var self = this; + + self.filename = filename; + self.path = path; + + if (callback) + self.complete = callback; + + if (filter) + self.filter = filter; + + walker.onComplete = function(directory, files) { + self.directory = directory; + + var ignore = []; + var index = 0; + + while (true) { + var file = files[index++]; + if (file === undefined) + break; + if (file.indexOf('.tpmignore') === -1) + continue; + index--; + ignore.push(file); + files.splice(index, 1); + } + + var skip = ['.DS_Store', '.git', 'thumbs.db']; + for (var i = 0, length = ignore.length; i < length; i++) + skip = skip.concat(Fs.readFileSync(ignore[i]).toString('utf8').split('\n')); + + index = 0; + while (true) { + var file = directory[index++]; + if (file === undefined) + break; + for (var i = 0, length = skip.length; i < length; i++) { + if (!skip[i]) + continue; + if (file.indexOf(skip[i]) === -1) + continue; + index--; + directory.splice(index, 1); + break; + } + } + + index = 0; + while (true) { + var file = files[index++]; + if (file === undefined) + break; + for (var i = 0, length = skip.length; i < length; i++) { + if (!skip[i] || (file.indexOf(skip[i]) === -1 && file.substring(self.path.length).indexOf('/tmp/') !== 0)) + continue; + index--; + files.splice(index, 1); + break; + } + } + + self.file = files; + self.$compress(); + }; + + walker.walk(path); +}; Backup.prototype.$compress = function() { - var self = this; - var length = self.path.length; - var len = 0; + var self = this; + var length = self.path.length; + var len = 0; - if (self.directory.length > 0) { + if (self.directory.length) { - len = self.directory.length; + len = self.directory.length; - for (var i = 0; i < len; i++) { - var o = self.directory[i]; - if (self.filter(o.substring(length))) - fs.appendFileSync(self.fileName, (o.replace(self.path, '').replace(/\\/g, '/') + '/').padRight(padding) + ':#\n'); - } + for (var i = 0; i < len; i++) { + var o = self.directory[i]; + self.filter(o.substring(length)) && Fs.appendFileSync(self.filename, (o.replace(self.path, '').replace(/\\/g, '/') + '/').padRight(PADDING) + ':#\n'); + } - self.directory = []; - } + self.directory = []; + } - var fileName = self.file.shift(); + var filename = self.file.shift(); - if (typeof(fileName) === 'undefined') { - self.complete(null, self.fileName); - return; - } + if (!filename) { + self.complete(null, self.filename); + return; + } - if (!self.filter(fileName.substring(length))) { - self.$compress(); - return; - } + if (!self.filter(filename.substring(length))) { + self.$compress(); + return; + } - var buffer = ''; + Fs.readFile(filename, function(err, data) { + Zlib.gzip(data, function(err, data) { - fs.readFile(fileName, function(err, data) { - zlib.gzip(data, function(err, data) { + if (err) + return; - if (err) - return; - - var name = fileName.replace(self.path, '').replace(/\\/g, '/'); - fs.appendFile(self.fileName, name.padRight(padding) + ':' + data.toString('base64') + '\n', function(err) { - self.$compress(); - }); - }); - }); + var name = filename.replace(self.path, '').replace(/\\/g, '/'); + Fs.appendFile(self.filename, name.padRight(PADDING) + ':' + data.toString('base64') + '\n', () => self.$compress()); + }); + }); }; Backup.prototype.restoreKey = function(data) { - var self = this; - var read = self.read; - - if (read.status === 1) { - self.restoreValue(data); - return; - } - - var index = data.indexOf(':'); - - if (index === -1) { - read.key += data; - return; - } - - read.status = 1; - read.key = data.substring(0, index); - self.restoreValue(data.substring(index + 1)); + var self = this; + var read = self.read; + + if (read.status === 1) { + self.restoreValue(data); + return; + } + + var index = -1; + var tmp = data; + + if (read.status === 2) { + tmp = Buffer.concat([read.key, tmp]); + index = tmp.indexOf(self.bufKey); + } else + index = tmp.indexOf(self.bufKey); + + if (index === -1) { + read.key = Buffer.concat([read.key, data]); + read.status = 2; + return; + } + + read.status = 1; + read.key = tmp.slice(0, index); + self.restoreValue(tmp.slice(index + 1)); + tmp = null; }; Backup.prototype.restoreValue = function(data) { - var self = this; - var read = self.read; + var self = this; + var read = self.read; - if (read.status !== 1) { - self.restoreKey(data); - return; - } + if (read.status !== 1) { + self.restoreKey(data); + return; + } - var index = data.indexOf('\n'); - if (index === -1) { - read.value += data; - return; - } + var index = data.indexOf(self.bufNew); + if (index === -1) { + read.value = Buffer.concat([read.value, data]); + return; + } - read.value += data.substring(0, index); - self.restoreFile(read.key.replace(/\s/g, ''), read.value.replace(/\s/g, '')); + read.value = Buffer.concat([read.value, data.slice(0, index)]); + self.restoreFile(read.key.toString('utf8').replace(REG_EMPTY, ''), read.value.toString('utf8').replace(REG_EMPTY, '')); - read.status = 0; - read.value = ''; - read.key = ''; + read.status = 0; + read.value = createBufferSize(); + read.key = createBufferSize(); - self.restoreKey(data.substring(index + 1)); + self.restoreKey(data.slice(index + 1)); }; Backup.prototype.restore = function(filename, path, callback, filter) { - if (!fs.existsSync(filename)) { - if (callback) - callback(new Error('Backup file not found.'), path); - return; - } - - var self = this; - self.filter = filter; - self.createDirectory(path, true); + if (!Fs.existsSync(filename)) { + callback && callback(new Error('Package not found.'), path); + return; + } - var stream = fs.createReadStream(filename); - var key = ''; - var value = ''; - var status = 0; + var self = this; - self.path = path; + self.filter = filter; + self.cache = {}; + self.createDirectory(path, true); + self.path = path; - stream.on('data', function(buffer) { + var stream = Fs.createReadStream(filename); + stream.on('data', buffer => self.restoreKey(buffer)); - var data = buffer.toString('utf8'); - self.restoreKey(data); + if (!callback) { + stream.resume(); + return; + } - }); + callback.path = path; - if (callback) { - stream.on('end', function() { - callback(null, path); - stream = null; - }); - } + stream.on('end', function() { + self.callback(callback); + stream = null; + }); - stream.resume(); + stream.resume(); }; -Backup.prototype.restoreFile = function(key, value) { - var self = this; - - if (typeof(self.filter) === 'function' && !self.filter(key)) - return; - - if (value === '#') { - self.createDirectory(key); - return; - } - - var path = key; - var index = key.lastIndexOf('/'); - - if (index !== -1) { - path = key.substring(0, index).trim(); - if (path.length > 0) - self.createDirectory(path); - } - - var buffer = new Buffer(value, 'base64'); - zlib.gunzip(buffer, function(err, data) { - fs.writeFileSync(ph.join(self.path, key), data); - buffer = null; - }); +Backup.prototype.callback = function(cb) { + var self = this; + if (self.pending <= 0) + return cb(null, cb.path); + setTimeout(() => self.callback(cb), 100); }; -Backup.prototype.createDirectory = function(path, root) { - - if (path[0] === '/') - path = path.substring(1); - - if (path[path.length - 1] === '/') - path = path.substring(0, path.length - 1); +Backup.prototype.restoreFile = function(key, value) { + var self = this; - var arr = path.split('/'); - var directory = ''; - var self = this; - var length = arr.length; + if (typeof(self.filter) === 'function' && !self.filter(key)) + return; - for (var i = 0; i < length; i++) { + if (value === '#') { + self.createDirectory(key); + return; + } - var name = arr[i]; - directory += (directory.length > 0 ? '/' : '') + name; - var dir = ph.join(self.path, directory); + var p = key; + var index = key.lastIndexOf('/'); - if (root) - dir = '/' + dir; + if (index !== -1) { + p = key.substring(0, index).trim(); + p && self.createDirectory(p); + } - if (fs.existsSync(dir)) - continue; + var buffer = createBuffer(value, 'base64'); + self.pending++; - fs.mkdirSync(dir); - } + Zlib.gunzip(buffer, function(err, data) { + Fs.writeFile(Path.join(self.path, key), data, () => self.pending--); + buffer = null; + }); }; -Backup.prototype.clear = function(path, callback, filter) { - - var self = this; - var walker = new Walker(); - walker.options.addEmptyDirectory = true; - - if (callback) - self.complete = callback; +Backup.prototype.createDirectory = function(p, root) { - if (filter) - self.filter = filter; + var self = this; - walker.onComplete = function(directory, files) { + if (p[0] === '/') + p = p.substring(1); - self.file = []; - self.directory = []; + var is = isWindows; - if (typeof(filter) !== 'function') - filter = function(o) { return true; }; + if (is) { + if (p[p.length - 1] === '\\') + p = p.substring(0, p.length - 1); + } else { + if (p[p.length - 1] === '/') + p = p.substring(0, p.length - 1); + } - var length = files.length; + var arr = is ? p.replace(/\//g, '\\').split('\\') : p.split('/'); + var directory = ''; - for (var i = 0; i < length; i++) { - var o = files[i]; - if (filter(o)) - self.file.push(o); - } + if (is && arr[0].indexOf(':') !== -1) + arr.shift(); - length = directory.length; - for (var i = 0; i < length; i++) { + var length = arr.length; - var o = files[i]; + for (var i = 0; i < length; i++) { - if (o === path) - return; + var name = arr[i]; - if (filter(o)) - self.directory.push(o); - } + if (is) + directory += (directory.length > 0 ? '\\' : '') + name; + else + directory += (directory.length > 0 ? '/' : '') + name; - self.directory.sort(function(a, b) { - if (a.length < b.length) - return 1; - else - return -1; - }); + var dir = Path.join(self.path, directory); + if (root) + dir = (is ? '\\' : '/') + dir; - self.removeFile(); - }; - - walker.walk(path); + !Fs.existsSync(dir) && Fs.mkdirSync(dir); + } }; -Backup.prototype.removeFile = function() { +Backup.prototype.clear = function(path, callback, filter) { - var self = this; - var filename = self.file.shift(); + var self = this; + var walker = new Walker(); + walker.options.addEmptyDirectory = true; - if (typeof(filename) === 'undefined') { - self.removeDirectory(); - return; - } + if (callback) + self.complete = callback; - fs.unlink(filename, function() { - self.removeFile(); - }); -}; + if (filter) + self.filter = filter; -Backup.prototype.removeDirectory = function() { + walker.onComplete = function(directory, files) { - var self = this; - var directory = self.directory.shift(); + self.file = []; + self.directory = []; - if (typeof(directory) === 'undefined') { - self.complete(); - return; - } + if (typeof(filter) !== 'function') + filter = function() { return true; }; - fs.rmdir(directory, function() { - self.removeDirectory(); - }); -}; + var length = files.length; -function download(url, callback) { + for (var i = 0; i < length; i++) { + var o = files[i]; + filter(o) && self.file.push(o); + } - var uri = parser.parse(url); - var h = {}; - var encoding = 'utf8'; + length = directory.length; + for (var i = 0; i < length; i++) { + var o = files[i]; + if (o === path) + return; + filter(o) && self.directory.push(o); + } - h['X-Powered-By'] = 'total.js package manager'; + self.directory.sort(function(a, b) { + if (a.length < b.length) + return 1; + else + return -1; + }); - var options = { protocol: uri.protocol, auth: uri.auth, method: 'GET', hostname: uri.hostname, port: uri.port, path: uri.path, agent: false, headers: h }; + self.removeFile(); + }; - var response = function(res) { - callback(null, res); - }; + walker.walk(path); +}; - var con = options.protocol === 'https:' ? https : http; +Backup.prototype.removeFile = function() { - try - { + var self = this; + var filename = self.file.shift(); + if (filename) + Fs.unlink(filename, () => self.removeFile()); + else + self.removeDirectory(); +}; - var req = con.get(options, response); +Backup.prototype.removeDirectory = function() { + var self = this; + var directory = self.directory.shift(); + if (directory) + Fs.rmdir(directory, () => self.removeDirectory()); + else + self.complete(); +}; - if (callback) { - req.on('error', function(error) { - callback(error, null); - }); - } +function download(url, callback) { + var uri = parser.parse(url); + var h = {}; - req.end(); + h['X-Powered-By'] = 'Total.js Package Manager'; - } catch (ex) { - if (callback) - callback(ex, null); - return false; - } + var options = { protocol: uri.protocol, auth: uri.auth, method: 'GET', hostname: uri.hostname, port: uri.port, path: uri.path, agent: false, headers: h }; + var con = options.protocol === 'https:' ? Https : Http; + var req = con.get(options, res => callback(null, res)); - return true; + req.on('error', error => callback(error, null)); + req.end(); + return true; } function display_help() { - log(colors.reset); - log('====== Total.js Package Manager ' + VERSION +' ======'); - log(''); - log('Documentation: http://docs.totaljs.com/total-package-manager/'); - log(''); - log(colors.underscore + '===== INSTALL PACKAGE TO THE CURRENT PATH' + colors.reset); - log(''); - log(colors.red + 'tpm install [optional: package_name] [optional: repository_name]' + colors.reset); - log(''); - log('EXAMPLE: tpm install ddos'); - log('EXAMPLE: tpm install ddos local-repository'); - log(''); - log(colors.dim + 'Install all total.js packages from package.json:' + colors.reset); - log('EXAMPLE: tpm install'); - log(''); - log(colors.underscore + '===== UNINSTALL PACKAGE FROM THE CURRENT PATH' + colors.reset); - log(''); - log(colors.red + 'tpm uninstall [optional: package_name] [optional: repository_name]' + colors.reset); - log(''); - log('EXAMPLE: tpm uninstall ddos'); - log('EXAMPLE: tpm uninstall ddos local-repository'); - log(''); - log(colors.dim + 'Uninstall all total.js packages from package.json:' + colors.reset); - log('EXAMPLE: tpm uninstall'); - log(''); - log(colors.underscore + '===== CREATE PACKAGE FROM THE CURRENT PATH CREATE' + colors.reset); - log(''); - log(colors.red + 'tpm create [important: package_name]' + colors.reset); - log(''); - log('EXAMPLE: tpm create my-project-template'); - log('EXAMPLE: tpm create my-module'); - log(''); - log(colors.underscore + '===== ADD A NEW PACKAGE REPOSITORY' + colors.reset); - log(''); - log(colors.red + 'tpm repository [repository_name] [repository_url]' + colors.reset); - log(''); - log('EXAMPLE: tpm repository local http://127.0.0.1:8000/'); - log('EXAMPLE: tpm repository enterprise http://repository.yourcompany.com/packages/totaljs/'); - log(''); - log(colors.underscore + '===== SHOW ALL REGISTERED REPOSITORIES' + colors.reset); - log(''); - log(colors.red + 'tpm repositories' + colors.reset); - log(''); + log(colors.reset); + log('====== Total.js Package Manager ' + VERSION +' ======'); + log(''); + log(colors.red + '$ tpm install [optional: package_name] [optional: repository_name]' + colors.reset); + log(''); + log(colors.dim + 'EXAMPLE: tpm install ddos'); + log('EXAMPLE: tpm install ddos local-repository' + colors.reset); + log(''); + log('Install all Total.js packages from "package.json":'); + log(colors.dim + 'EXAMPLE: tpm install' + colors.reset); + log(''); + log('--- --- --- --- ---'); + log(''); + log(colors.red + '$ tpm uninstall [optional: package_name] [optional: repository_name]' + colors.reset); + log(''); + log(colors.dim + 'EXAMPLE: tpm uninstall ddos'); + log('EXAMPLE: tpm uninstall ddos local-repository' + colors.reset); + log(''); + log('Uninstall all Total.js packages from package.json:'); + log(colors.dim + 'EXAMPLE: tpm uninstall' + colors.reset); + 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'); + log('EXAMPLE: tpm create my-module'); + log('EXAMPLE: tpm create my-module /users/packages/my-package/' + colors.reset); + 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/'); + log('EXAMPLE: tpm repository enterprise http://repository.yourcompany.com/packages/totaljs/' + colors.reset); + log(''); + log('--- --- --- --- ---'); + log(''); + log(colors.red + '$ tpm repositories' + colors.reset); + log(''); + log('--- --- --- --- ---'); + log(''); + log(colors.red + '$ tpm unpack [package_name] [optional: target_directory]' + colors.reset); + log(''); } function display_repositories() { - log(''); - log('--- REPOSITORIES ---'); - log(''); - log('Filename: ' + ph.join(ph.dirname(process.argv[1]), 'tpm.json')); - log(''); - Object.keys(settings).forEach(function(key) { - log(key.padRight(33) + ': ' + settings[key]); - }); - log(''); + log(''); + log('--- REPOSITORIES ---'); + log(''); + log('Filename: ' + Path.join(Path.dirname(process.argv[1]), 'tpm.json')); + log(''); + Object.keys(settings).forEach(function(key) { + log(key.padRight(33) + ': ' + settings[key]); + }); + log(''); } function repository_add() { - if (current_package.length === 0 || current_repository.length === 0) { - display_repositories(); - return; - } + if (!current_package.length || !current_repository.length) { + display_repositories(); + return; + } - var filename = ph.join(ph.dirname(process.argv[1]), 'tpm.json'); + var filename = Path.join(Path.dirname(process.argv[1]), 'tpm.json'); + var last = current_repository[current_repository.length - 1]; + if (last !== '=' && last !== '?' && last !== '/') + current_repository += '/'; - var last = current_repository[current_repository.length - 1]; - if (last !== '=' && last !== '?' && last !== '/') - current_repository += '/'; - - settings[current_package] = current_repository; - fs.writeFileSync(filename, JSON.stringify(settings)); - - display_repositories(); + settings[current_package] = current_repository; + Fs.writeFileSync(filename, JSON.stringify(settings)); + display_repositories(); } function install(repository, pkgname, cb, append) { - var name = pkgname; + var name = pkgname; + if (name.toLowerCase().lastIndexOf('.package') === -1) + name += '.package'; - if (name.toLowerCase().lastIndexOf('.package') === -1) - name += '.package'; + var h = name.substring(0, 6); + if (h === 'http:/' || h === 'https:') { + var n = Path.basename(name); + pkgname = n; + repository = name.substring(0, name.length - n.length); + name = n; + } - log(''); - log(colors.red + '--- INSTALL PACKAGE --' + colors.reset); - log(''); - log('Package :', pkgname); - log('Directory :', process.cwd()); - log('Download :', repository + name); + log(''); + log(colors.red + '--- INSTALL PACKAGE --' + colors.reset); + log(''); + log('Package :', pkgname); + log('Directory :', process.cwd()); + log('Download :', repository + name); - download(repository + name, function (err, response) { + download(repository + name, function (err, response) { - if (err) { + if (err) { - log(colors.bgRed + colors.white + 'Error :', err + colors.reset); + log(colors.bgRed + colors.white + 'Error :', err + colors.reset); - if (cb) - cb(); + if (cb) + cb(); - return; - } + return; + } - if (response.statusCode !== 200) { - log(colors.bgRed + colors.white + 'Error :', response.statusCode + colors.reset); - log(''); + if (response.statusCode !== 200) { + log(colors.bgRed + colors.white + 'Error :', response.statusCode + colors.reset); + log(''); - if (cb) - cb(); + if (cb) + cb(); - return; - } + return; + } - var filename = ph.join(process.cwd(), name); - var stream = fs.createWriteStream(filename); + var filename = Path.join(process.cwd(), name); + var stream = Fs.createWriteStream(filename); - response.on('end', function () { + response.on('end', function () { - log('Installing :', filename); + log('Installing :', filename); - var backup = new Backup(); - backup.restore(filename, ph.dirname(filename), function() { - log('Removing :', filename); - fs.unlinkSync(filename); - log(colors.bgGreen + colors.white + 'Status :', 'Success' + colors.reset); - log(''); - if (append) { - filename = ph.join(process.cwd(), 'package.json'); + var backup = new Backup(); + backup.restore(filename, Path.dirname(filename), function() { + log('Removing :', filename); + Fs.unlinkSync(filename); + log(colors.bgGreen + colors.white + 'Status :', 'Success' + colors.reset); + log(''); + if (append) { + filename = Path.join(process.cwd(), 'package.json'); - if (fs.existsSync(filename)) { - var obj = JSON.parse(fs.readFileSync(filename).toString('utf8')); - if (!obj.tpm) - obj.tpm = {}; - obj.tpm[pkgname] = repository; - } else { - obj = { tpm: {} }; - obj.tpm[pkgname] = repository; - } + if (Fs.existsSync(filename)) { + var obj = JSON.parse(Fs.readFileSync(filename).toString('utf8')); + if (!obj.tpm) + obj.tpm = {}; + obj.tpm[pkgname] = repository; + } else { + obj = { tpm: {} }; + obj.tpm[pkgname] = repository; + } - fs.writeFileSync(filename, JSON.stringify(obj, null, 4)); - } + Fs.writeFileSync(filename, JSON.stringify(obj, null, 4)); + } - if (cb) - cb(); + if (cb) + cb(); - }); + }); - }); + }); - response.pipe(stream); - - }); + response.pipe(stream); + }); +} +function unpack(filename, target) { + var backup = new Backup(); + backup.restore(filename, target ? target : Path.dirname(filename), function() { + log(colors.bgGreen + colors.white + 'Status :', 'Success' + colors.reset); + log(''); + }); } function uninstall(repository, pkgname, cb, remove) { - var name = pkgname; + var name = pkgname; - if (name.toLowerCase().lastIndexOf('.package') === -1) - name += '.package'; + if (name.toLowerCase().lastIndexOf('.package') === -1) + name += '.package'; - log(''); - log(colors.red + '--- UNINSTALL PACKAGE --' + colors.reset); - log(''); - log('Package :', pkgname); - log('Directory :', process.cwd()); - log('Download :', repository + name); + log(''); + log(colors.red + '--- UNINSTALL PACKAGE --' + colors.reset); + log(''); + log('Package :', pkgname); + log('Directory :', process.cwd()); + log('Download :', repository + name); - download(repository + name, function (err, response) { + download(repository + name, function (err, response) { - if (err) { + if (err) { - log(colors.bgRed + colors.white + 'Error :', err + colors.reset); + log(colors.bgRed + colors.white + 'Error :', err + colors.reset); - if (cb) - cb(); + if (cb) + cb(); - return; - } + return; + } - if (response.statusCode !== 200) { - log(colors.bgRed + colors.white + 'Error :', response.statusCode + colors.reset); - log(''); + if (response.statusCode !== 200) { + log(colors.bgRed + colors.white + 'Error :', response.statusCode + colors.reset); + log(''); - if (cb) - cb(); + if (cb) + cb(); - return; - } + return; + } - var filename = ph.join(process.cwd(), name); - var stream = fs.createWriteStream(filename); - var arr = []; + var filename = Path.join(process.cwd(), name); + var stream = Fs.createWriteStream(filename); + var arr = []; - response.on('end', function () { + response.on('end', function () { - log('Reading :', filename); + log('Reading :', filename); - var backup = new Backup(); - backup.restore(filename, ph.dirname(filename), function() { + var backup = new Backup(); + backup.restore(filename, Path.dirname(filename), function() { - arr.sort(function(a, b) { - if (a.length > b.length) - return -1; - if (a.length < b.length) - return 1; - return a.localeCompare(b); - }); + arr.sort(function(a, b) { + if (a.length > b.length) + return -1; + if (a.length < b.length) + return 1; + return a.localeCompare(b); + }); - arr.forEach(function(path) { - try - { - log('Removing :', path); - unlink(path); - } catch (e) {} - }); + arr.forEach(function(path) { + try + { + log('Removing :', path); + unlink(path); + } catch (e) {} + }); - log('Removing :', filename); - fs.unlinkSync(filename); - log(colors.bgGreen + colors.white + 'Status :', 'Success' + colors.reset); - log(''); + log('Removing :', filename); + Fs.unlinkSync(filename); + log(colors.bgGreen + colors.white + 'Status :', 'Success' + colors.reset); + log(''); - if (remove) { - filename = ph.join(process.cwd(), 'package.json'); + if (remove) { + filename = Path.join(process.cwd(), 'package.json'); - if (fs.existsSync(filename)) { - var obj = JSON.parse(fs.readFileSync(filename).toString('utf8')); - if (obj.tpm) - delete obj.tpm[pkgname]; - fs.writeFileSync(filename, JSON.stringify(obj, null, 4)); - } + if (Fs.existsSync(filename)) { + var obj = JSON.parse(Fs.readFileSync(filename).toString('utf8')); + if (obj.tpm) + obj.tpm[pkgname] = undefined; + Fs.writeFileSync(filename, JSON.stringify(obj, null, 4)); + } - } + } - if (cb) - cb(); + cb && cb(); - }, function(path) { - arr.push(path); - return false; - }); + }, function(path) { + arr.push(path); + return false; + }); - }); + }); - response.pipe(stream); - - }); + response.pipe(stream); + }); } function create() { - var name = current_package; + var target = process.cwd(); + var name = current_package; + + var length = process.argv.length; + + 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('.package') === -1) - name += '.package'; + if (name.toLowerCase().lastIndexOf('.package') === -1) + name += '.package'; - log(''); - log('--- CREATE PACKAGE --'); - log(''); - log('Package :', current_package); - log('Directory :', process.cwd()); + log(''); + log('--- CREATE PACKAGE --'); + log(''); + log('Package :', current_package); + log('Directory :', target); - var backup = new Backup(); - backup.backup(process.cwd(), ph.join(process.cwd(), name), function(err, path) { + if (!isWindows) { + if (name[0] !== '/') + name = Path.join(process.cwd(), name); + } - if (err) { - throw err; - return; - } + var backup = new Backup(); - log('Success :', path); - log(''); + backup.backup(target, name, function(err, path) { - }, function(path) { - return path.lastIndexOf('.package') === -1; - }); + if (err) + throw err; + + log('Success :', path); + log(''); + + }, () => 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) { - var p = ph.join(process.cwd(), path); + var p = Path.join(process.cwd(), path); - if (!fs.existsSync(p)) - return; + if (!Fs.existsSync(p)) + return; - var stats = fs.statSync(p); + var stats = Fs.statSync(p); - if (stats.isFile()) { - fs.unlinkSync(p); - unlink(ph.dirname(path)); - return; - } + if (stats.isFile()) { + Fs.unlinkSync(p); + unlink(Path.dirname(path)); + return; + } - if (fs.readdirSync(p).length === 0) - fs.rmdirSync(p); + !Fs.readdirSync(p).length && Fs.rmdirSync(p); } function main() { - var filename = ph.join(ph.dirname(process.argv[1]), 'tpm.json'); - var dir = process.cwd(); - settings = { 'default': 'http://packages.totaljs.com/' + VERSION.replace(/\.+/g, '').replace(/\s+/g, '') + '/' }; + var filename = Path.join(Path.dirname(process.argv[1]), 'tpm.json'); + settings = { 'default': 'https://modules.totaljs.com/packages/' }; + + if (Fs.existsSync(filename)) + settings = JSON.parse(Fs.readFileSync(filename).toString('utf8')); + + if (process.argv.length === 2) { + display_help(); + return; + } - if (fs.existsSync(filename)) - settings = JSON.parse(fs.readFileSync(filename).toString('utf8')); + for (var i = 2; i < process.argv.length; i++) { + var arg = process.argv[i]; + var cmd = arg.toLowerCase(); - if (process.argv.length === 2) { - display_help(); - return; - } + if (cmd === '-v' || cmd === '-version') { + log(VERSION); + return; + } - for (var i = 2; i < process.argv.length; i++) { - var arg = process.argv[i]; - var cmd = arg.toLowerCase(); + if (cmd === 'install') { + $type = 1; + continue; + } - if (cmd === '-v' || cmd === '-version') { - log(VERSION); - return; - } + if (cmd === 'uninstall') { + $type = 4; + continue; + } - if (cmd === 'install') { - $type = 1; - continue; - } + if (cmd === 'create') { + $type = 2; + continue; + } - if (cmd === 'uninstall') { - $type = 4; - continue; - } + if (cmd === 'bundle') { + $type = 7; + continue; + } - if (cmd === 'create') { - $type = 2; - continue; - } + if (cmd === 'repository') { + $type = 3; + continue; + } - if (cmd === 'repository') { - $type = 3; - continue; - } + if (cmd === 'repositories') { + display_repositories(); + return; + } - if (cmd === 'repositories') { - display_repositories(); - return; - } + if (cmd === 'unpack') { + current_repository = ''; + $type = 6; + continue; + } - if (cmd === 'packages' || cmd === 'list') { - $type = 5; - continue; - } + if (cmd === 'packages' || cmd === 'list') { + $type = 5; + continue; + } - if (cmd === '-h' || cmd === '-help' || cmd === '--help' || cmd === 'help') { - display_help(); - return; - } + if (cmd === '-h' || cmd === '-help' || cmd === '--help' || cmd === 'help') { + display_help(); + return; + } - if (current_package.length > 0) - current_repository = arg; - else - current_package = arg; + if (current_package.length > 0) + current_repository = arg; + else + current_package = arg; - continue; - } + continue; + } - switch ($type) { + switch ($type) { - case 1: + case 1: - if (current_package.length !== 0) { - install(settings[current_repository], current_package, null, true); - return; - } + if (current_package.length) { + install(settings[current_repository], current_package, null, true); + return; + } - var filename = ph.join(process.cwd(), 'package.json'); - var packagejson = {}; + var filename = Path.join(process.cwd(), 'package.json'); + var packagejson = {}; - if (!fs.existsSync(filename)) - return; + if (!Fs.existsSync(filename)) + return; - packagejson = JSON.parse(fs.readFileSync(filename).toString('utf8')); + packagejson = JSON.parse(Fs.readFileSync(filename).toString('utf8')); - if (!packagejson.tpm) - return; + if (!packagejson.tpm) + return; - var index = 0; - var fn = function() { - var key = Object.keys(packagejson.tpm)[index++]; + var index = 0; + var fn = function() { + var key = Object.keys(packagejson.tpm)[index++]; - if (typeof(key) === 'undefined') - return; + if (typeof(key) === 'undefined') + return; - var url = packagejson.tpm[key]; + var url = packagejson.tpm[key]; + install(url, key, () => fn()); + }; - install(url, key, function() { - fn(); - }); - }; + fn(); - fn(); + break; - break; + case 2: + create(); + break; - case 2: - create(); - break; + case 7: + createbundle(); + break; - case 3: - repository_add(); - break; + case 3: + repository_add(); + break; - case 4: + case 4: - if (current_package.length !== 0) { - uninstall(settings[current_repository], current_package, null, true); - return; - } + if (current_package.length) { + uninstall(settings[current_repository], current_package, null, true); + return; + } - var filename = ph.join(process.cwd(), 'package.json'); - var packagejson = {}; + var filename = Path.join(process.cwd(), 'package.json'); + var packagejson = {}; - if (!fs.existsSync(filename)) - return; + if (!Fs.existsSync(filename)) + return; - packagejson = JSON.parse(fs.readFileSync(filename).toString('utf8')); + packagejson = JSON.parse(Fs.readFileSync(filename).toString('utf8')); - if (!packagejson.tpm) - return; + if (!packagejson.tpm) + return; - var index = 0; - var fn = function() { - var key = Object.keys(packagejson.tpm)[index++]; + var index = 0; + var fn = function() { + var key = Object.keys(packagejson.tpm)[index++]; - if (typeof(key) === 'undefined') - return; + if (typeof(key) === 'undefined') + return; - var url = packagejson.tpm[key]; + var url = packagejson.tpm[key]; + uninstall(url, key, () => fn(), true); + }; - uninstall(url, key, function() { - fn(); - }, true); - }; + fn(); + break; - fn(); - break; + case 6: + unpack(current_package, current_repository); + break; - default: - display_help(); - break; - } + default: + display_help(); + break; + } } function log() { - console.log.apply(console, arguments); + console.log.apply(console, arguments); } -main(); \ No newline at end of file +main(); diff --git a/builders.js b/builders.js index 6e2a73782..ef1c17963 100755 --- a/builders.js +++ b/builders.js @@ -1,1078 +1,6729 @@ +// 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 FrameworkBuilders - * @author Peter Širka - * @copyright Peter Širka 2012-2014 - * @version 1.5.0 + * @version 3.4.4 */ 'use strict'; -var utils = require('./utils'); - -var schema = {}; -var schemaValidation = {}; -var schemaValidator = {}; -var schemaDefaults = {}; - -var UNDEFINED = 'undefined'; -var FUNCTION = 'function'; -var OBJECT = 'object'; -var STRING = 'string'; -var NUMBER = 'number'; -var BOOLEAN = 'boolean'; -var REQUIRED = 'The field "@" is required.'; - -/* - @onResource {Function} :: function(name, key) return {String} -*/ -/** - * ErrorBuilder - * @class - * @classdesc Object validation. - * @param {ErrorBuilderOnResource} onResource Resource handler. - * @property {Number} count Count of errors. - */ -function ErrorBuilder(onResource) { - - this.errors = []; - this.onResource = onResource; - this.resourceName = ''; - this.resourcePrefix = ''; - this.count = 0; - this.replacer = []; - this.isPrepared = false; - - if (typeof(onResource) === UNDEFINED) - this._resource(); +const REQUIRED = 'The field "@" is invalid.'; +const DEFAULT_SCHEMA = 'default'; +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-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 tasks = {}; +var transforms = { pagination: {}, error: {}, restbuilder: {} }; +var restbuilderupgrades = []; + +function SchemaBuilder(name) { + this.name = name; + this.collection = {}; } -/** - * @callback ErrorBuilderOnResource - * @param {String} name Filename of resource. - * @param {String} key Resource key. - * @return {String} - */ +const SchemaBuilderProto = SchemaBuilder.prototype; -/** - * UrlBuilder - * @class - * @classdesc CRUD parameters in URL. - */ -function UrlBuilder() { - this.builder = {}; +function SchemaOptions(error, model, options, callback, controller, name, schema) { + this.error = error; + this.value = this.model = model; + this.options = options || EMPTYOBJECT; + this.callback = this.next = callback; + this.controller = (controller instanceof SchemaOptions || controller instanceof OperationOptions) ? controller.controller : controller; + this.name = name; + this.schema = schema; } -/** - * Pagination - * @class - * @param {Number} items Count of items. - * @param {Number} page Current page. - * @param {Number} max Max items on page. - * @param {String} format URL format for links (next, back, go to). Example: ?page={0} --- {0} = page, {1} = items count, {2} = page count - * @property {Number} isNext Is next page? - * @property {Number} isPrev Is previous page? - * @property {Number} count Page count. - * @property {Boolean} visible Is more than one page? - * @property {String} format Format URL. Example: ?page={0} --- {0} = page, {1} = items count, {2} = page count - */ -function Pagination(items, page, max, format) { - this.isNext = false; - this.isPrev = false; - this.items = items; - this.count = 0; - this.skip = 0; - this.take = 0; - this.page = 0; - this.max = 0; - this.visible = false; - this.format = format || '?page={0}'; - this.refresh(items, page, max); +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 = { -/** - * Create schema - * @param {String} name chema name. - * @param {Object} obj Schema definition. - * @param {SchemaDefaults} defaults Schema defaults. - * @param {SchemaValidator} validator Schema validator. - * @return {Object} - */ -exports.schema = function(name, obj, defaults, validator) { - - if (typeof(obj) === UNDEFINED) - return schema[name] || null; + get user() { + return this.controller ? this.controller.user : null; + }, - if (typeof(defaults) === FUNCTION) - schemaDefaults[name] = defaults; + get session() { + return this.controller ? this.controller.session : null; + }, - if (typeof(validator) === FUNCTION) - schemaValidator[name] = validator; + get sessionid() { + return this.controller && this.controller ? this.controller.req.sessionid : null; + }, - schema[name] = obj; - return obj; -}; + get language() { + return (this.controller ? this.controller.language : '') || ''; + }, -/** - * Default handler - * @callback SchemaDefaults - * @param {String} name Property name. - * @param {Booelan} isDefault Is default (true) or prepare (false)? - * @return {Object} Property value. - */ + get ip() { + return this.controller ? this.controller.ip : null; + }, -/** - * Validator handler - * @callback SchemaValidator - * @param {String} name Property name. - * @param {Object} value Property value. - * @return {Boolean} Is valid? - */ + get id() { + return this.controller ? this.controller.id : null; + }, -/** - * Check if property value is joined to other class - * @private - * @param {String} value Property value from Schema definition. - * @return {Boolean} - */ -exports.isJoin = function(value) { - if (!value) - return false; - if (value[0] === '[') - return true; - return typeof(schema[value]) !== UNDEFINED; -}; + get req() { + return this.controller ? this.controller.req : null; + }, -/** - * Create validation - * @param {String} name Schema name. - * @param {Function or Array} fn Validator Handler or Property names as array for validating. - * @return {Function or Array} - */ -exports.validation = function(name, fn) { + get res() { + return this.controller ? this.controller.res : null; + }, - if (typeof(fn) === FUNCTION) { - schemaValidator[name] = fn; - return true; - } + get params() { + return this.controller ? this.controller.params : null; + }, - if (typeof(fn) === UNDEFINED) - return schemaValidation[name] || []; + get files() { + return this.controller ? this.controller.files : null; + }, - schemaValidation[name] = fn; - return fn; -}; + get body() { + return this.controller ? this.controller.body : null; + }, + get query() { + return this.controller ? this.controller.query : null; + }, -/** - * Validate model - * @param {String} name Schema name. - * @param {Object} model Object for validating. - * @return {ErrorBuilder} - */ -exports.validate = function(name, model) { + get model() { + return this.value; + }, - var fn = schemaValidator[name]; - var builder = new ErrorBuilder(); + set model(val) { + this.value = val; + }, - if (typeof(fn) === UNDEFINED) - return builder; + get headers() { + return this.controller && this.controller.req ? this.controller.req.headers : null; + }, - return utils.validate.call(this, model, Object.keys(schema[name]), fn, builder); -}; + get ua() { + return this.controller && this.controller.req ? this.controller.req.ua : null; + }, -/** - * Create default object according to schema - * @param {String} name Schema name. - * @return {Object} - */ -exports.create = function(name) { - return exports.defaults(name); + 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; + } }; -/** - * Create default object according to schema - * @param {String} name Schema name. - * @return {Object} - */ -exports.defaults = function(name) { +const TaskBuilderProto = TaskBuilder.prototype; - var obj = exports.schema(name); +SchemaOptions.prototype = { - if (obj === null) - return null; - - var defaults = schemaDefaults[name]; - var item = utils.extend({}, obj, true); - var properties = Object.keys(item); - var length = properties.length; - - for (var i = 0; i < length; i++) { - - var property = properties[i]; - var value = item[property]; - var type = typeof(value); - - if (defaults) { - var def = defaults(property, true); - if (typeof(def) !== UNDEFINED) { - item[property] = def; - continue; - } - } - - if (type === FUNCTION) { - - if (value === Number) { - item[property] = 0; - continue; - } - - if (value === Boolean) { - item[property] = false; - continue; - } + get user() { + return this.controller ? this.controller.user : null; + }, - if (value === String) { - item[property] = ''; - continue; - } + get session() { + return this.controller ? this.controller.session : null; + }, - if (value === Date) { - item[property] = new Date(); - continue; - } + get keys() { + return this.model.$$keys; + }, - if (value === Object) { - item[property] = {}; - continue; - } + get parent() { + return this.model.$$parent; + }, - if (value === Array) { - item[property] = []; - continue; - } + get repo() { + if (this.controller) + return this.controller.repository; + if (!this.model.$$repository) + this.model.$$repository = {}; + return this.model.$$repository; + }, - item[property] = value(); - continue; - } + get sessionid() { + return this.controller && this.controller ? this.controller.req.sessionid : null; + }, - if (type === NUMBER) { - item[property] = 0; - continue; - } + get language() { + return (this.controller ? this.controller.language : '') || ''; + }, - if (type === BOOLEAN) { - item[property] = false; - continue; - } + get ip() { + return this.controller ? this.controller.ip : null; + }, - if (type === OBJECT) { - item[property] = value instanceof Array ? [] : {}; - continue; - } + get id() { + return this.controller ? this.controller.id : null; + }, - if (type !== STRING) { - item[property] = null; - continue; - } + get req() { + return this.controller ? this.controller.req : null; + }, - var isArray = value[0] === '['; + get res() { + return this.controller ? this.controller.res : null; + }, - if (isArray) - value = value.substring(1, value.length - 1); + get params() { + return this.controller ? this.controller.params : null; + }, - if (isArray) { - item[property] = []; - continue; - } + get files() { + return this.controller ? this.controller.files : null; + }, - var lower = value.toLowerCase(); + get body() { + return this.controller ? this.controller.body : null; + }, - if (lower.contains([STRING, 'text', 'varchar', 'nvarchar', 'binary', 'data', 'base64'])) { - item[property] = ''; - continue; - } + get query() { + return this.controller ? this.controller.query : null; + }, - if (lower.contains(['int', NUMBER, 'decimal', 'byte', 'float', 'double'])) { - item[property] = 0; - continue; - } + get headers() { + return this.controller && this.controller.req ? this.controller.req.headers : null; + }, - if (lower.contains('bool')) { - item[property] = false; - continue; - } - - if (lower.contains(['date', 'time'])) { - item[property] = new Date(); - continue; - } + get ua() { + return this.controller && this.controller.req ? this.controller.req.ua : null; + }, - if (lower.contains(['object'])) { - item[property] = {}; - continue; - } - - if (lower.contains(['array'])) { - item[property] = []; - continue; - } + 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; + } +}; - if (lower.contains(['binary', 'data', 'base64'])) { - item[property] = null; - continue; - } - - item[property] = exports.defaults(value); - } +var SchemaOptionsProto = SchemaOptions.prototype; - return item; +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; }; -/** - * Prepare object according to schema - * @param {String} name Schema name. - * @param {Object} model Object to prepare. - * @return {Object} Prepared object. - */ -exports.prepare = function(name, model) { - - var obj = exports.schema(name); +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; + } +}; - if (obj === null) - return null; +SchemaOptionsProto.redirect = function(url) { + this.callback(new F.callback_redirect(url)); + return this; +}; - if (model === null) - return exports.defaults(name); +SchemaOptionsProto.clean = function() { + return this.model.$clean(); +}; - var tmp; - var item = utils.extend({}, obj, true); - var properties = Object.keys(item); - var defaults = schemaDefaults[name]; - var length = properties.length; +SchemaOptionsProto.$async = function(callback, index) { + return this.model.$async(callback, index); +}; - for (var i = 0; i < length; i++) { +SchemaOptionsProto.$workflow = function(name, helper, callback, async) { + return this.model.$workflow(name, helper, callback, async); +}; - var property = properties[i]; - var val = model[property]; +SchemaOptionsProto.$transform = function(name, helper, callback, async) { + return this.model.$transform(name, helper, callback, async); +}; - if (typeof(val) === UNDEFINED && defaults) - val = defaults(property, false); +SchemaOptionsProto.$operation = function(name, helper, callback, async) { + return this.model.$operation(name, helper, callback, async); +}; - if (typeof(val) === UNDEFINED) - val = ''; +SchemaOptionsProto.$hook = function(name, helper, callback, async) { + return this.model.$hook(name, helper, callback, async); +}; - var value = item[property]; - var type = typeof(value); - var typeval = typeof(val); +SchemaOptionsProto.$save = function(helper, callback, async) { + return this.model.$save(helper, callback, async); +}; - if (typeval === FUNCTION) - val = val(); +SchemaOptionsProto.$insert = function(helper, callback, async) { + return this.model.$insert(helper, callback, async); +}; - if (type === FUNCTION) { +SchemaOptionsProto.$update = function(helper, callback, async) { + return this.model.$update(helper, callback, async); +}; - if (value === Number) { - item[property] = utils.parseFloat(val); - continue; - } +SchemaOptionsProto.$patch = function(helper, callback, async) { + return this.model.$patch(helper, callback, async); +}; - if (value === Boolean) { - tmp = val.toString(); - item[property] = tmp === 'true' || tmp === '1'; - continue; - } - - if (value === String) { - item[property] = val.toString(); - continue; - } - - if (value === Date) { - - tmp = null; - - switch (typeval) { - case OBJECT: - if (utils.isDate(val)) - tmp = val; - else - tmp = null; - break; - - case NUMBER: - tmp = new Date(val); - break; - - case STRING: - if (val === '') - tmp = null; - else - tmp = val.parseDate(); - break; - } - - if (tmp !== null && typeof(tmp) === OBJECT && tmp.toString() === 'Invalid Date') - tmp = null; - - item[property] = tmp || (defaults ? isUndefined(defaults(property), null) : null); - continue; - } - - item[property] = isUndefined(defaults(property), null); - continue; - } - - if (type === OBJECT) { - item[property] = typeval === OBJECT ? val : null; - continue; - } - - if (type === NUMBER) { - item[property] = utils.parseFloat(val); - continue; - } - - if (val === null || typeval === UNDEFINED) - tmp = ''; - else - tmp = val.toString(); - - if (type === BOOLEAN) { - item[property] = tmp === 'true' || tmp === '1'; - continue; - } - - if (type !== STRING) { - item[property] = tmp; - continue; - } - - var isArray = value[0] === '[' || value === 'array'; - - if (isArray) { - - if (value[0] === '[') - value = value.substring(1, value.length - 1); - else - value = null; - - if (!(val instanceof Array)) { - item[property] = (defaults ? isUndefined(defaults(property, false), []) : []); - continue; - } - - item[property] = []; - var sublength = val.length; - for (var j = 0; j < sublength; j++) { - - if (value === null) { - item[property].push(model[property][j]); - continue; - } - - var tmp = model[property][j]; - - switch (value) { - case 'string': - case 'varchar': - case 'text': - item[property].push((tmp || '').toString()); - break; - case 'bool': - case 'boolean': - tmp = (tmp || '').toString().toLowerCase(); - item[property].push(tmp === 'true' || tmp === '1'); - break; - case 'int': - case 'integer': - item[property].push(utils.parseInt(tmp)); - break; - case 'number': - item[property].push(utils.parseFloat(tmp)); - break; - default: - item[property][j] = exports.prepare(value, model[property][j]); - break; - } - } - - continue; - } - - var lower = value.toLowerCase(); - - if (lower.contains([STRING, 'text', 'varchar', 'nvarchar'])) { - - var beg = lower.indexOf('('); - if (beg === -1) { - item[property] = tmp; - continue; - } - - var size = lower.substring(beg + 1, lower.length - 1).parseInt(); - item[property] = tmp.max(size, '...'); - continue; - } - - if (lower.contains(['int', 'byte'])) { - item[property] = utils.parseInt(val); - continue; - } - - if (lower.contains(['decimal', NUMBER, 'float', 'double'])) { - item[property] = utils.parseFloat(val); - continue; - } - - if (lower.contains('bool', BOOLEAN)) { - item[property] = tmp === 'true' || tmp === '1'; - continue; - } - - if (lower.contains(['date', 'time'])) { - - if (typeval === 'date') { - item[property] = val; - continue; - } - - if (typeval === STRING) { - item[property] = val.parseDate(); - continue; - } - - if (typeval === NUMBER) { - item[property] = new Date(val); - continue; - } - - item[property] = isUndefined(defaults(property)); - continue; - } - - item[property] = exports.prepare(value, model[property]); - } - - return item; +SchemaOptionsProto.$query = function(helper, callback, async) { + return this.model.$query(helper, callback, async); }; -function isUndefined(value, def) { - if (typeof(value) === UNDEFINED) - return def; - return value; -} +SchemaOptionsProto.$delete = SchemaOptionsProto.$remove = function(helper, callback, async) { + return this.model.$remove(helper, callback, async); +}; -// ====================================================== -// PROTOTYPES -// ====================================================== +SchemaOptionsProto.$get = SchemaOptionsProto.$read = function(helper, callback, async) { + return this.model.$get(helper, callback, async); +}; -/** - * Resource setting - * @param {String} name Resource name. - * @param {String} prefix Resource prefix. - * @return {ErrorBuilder} - */ -ErrorBuilder.prototype.resource = function(name, prefix) { - var self = this; - self.resourceName = name; - self.resourcePrefix = prefix || ''; - return self._resource(); +SchemaOptionsProto.push = function(type, name, helper, first) { + return this.model.$push(type, name, helper, first); }; -/** - * Internal: Resource wrapper - * @private - * @return {ErrorBuilder} - */ -ErrorBuilder.prototype._resource = function() { - var self = this; - self.onResource = function(name) { - var self = this; - return framework.resource(self.resourceName, self.resourcePrefix + name); - }; - return self; +SchemaOptionsProto.next = function(type, name, helper) { + return this.model.$next(type, name, helper); }; -/** - * Add an error - * @param {String} name Property name. - * @param {String} error Error message. - * @param {String} path Current path (in object). - * @return {ErrorBuilder} - */ -ErrorBuilder.prototype.add = function(name, error, path) { - var self = this; - self.isPrepared = false; +SchemaOptionsProto.output = function() { + return this.model.$output(); +}; - if (name instanceof ErrorBuilder) { +SchemaOptionsProto.stop = function() { + return this.model.$stop(); +}; - name.errors.forEach(function(o) { - self.errors.push(o); - }); +SchemaOptionsProto.response = function(index) { + return this.model.$response(index); +}; - self.count = self.errors.length; - return self; - } +SchemaOptionsProto.DB = function() { + return F.database(this.error); +}; - self.errors.push({ - name: name, - error: error || '@', - path: path - }); - self.count = self.errors.length; - return self; +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); + }; }; -/** - * Remove error - * @param {String} name Property name. - * @return {ErrorBuilder} - */ -ErrorBuilder.prototype.remove = function(name) { - var self = this; +SchemaOptionsProto.success = function(a, b) { - self.errors = self.errors.remove(function(o) { - return o.name === name; - }); + if (a && b === undefined && typeof(a) !== 'boolean') { + b = a; + a = true; + } - self.count = self.errors.length; - return self; + this.callback(SUCCESS(a === undefined ? true : a, b)); + return this; }; -/** - * Has error? - * @param {String} name Property name (optional). - * @return {Boolean} - */ -ErrorBuilder.prototype.hasError = function(name) { - var self = this; +SchemaOptionsProto.done = function(arg) { + var self = this; + return function(err, response) { + if (err) { - if (name) { - return self.errors.find(function(o) { - return o.name === name; - }) !== null; - } + if (self.error !== err) + self.error.push(err); - return self.errors.length > 0; + self.callback(); + } else if (arg) + self.callback(SUCCESS(err == null, arg === true ? response : arg)); + else + self.callback(SUCCESS(err == null)); + }; }; -/** - * Read an error - * @param {String} name Property name. - * @return {String} - */ -ErrorBuilder.prototype.read = function(name) { - - var self = this; - - if (!self.isPrepared) - self.prepare(); +SchemaOptionsProto.invalid = function(name, error, path, index) { + var self = this; - var error = self.errors.find(function(o) { - return o.name === name; - }); + if (arguments.length) { + self.error.push(name, error, path, index); + self.callback(); + return self; + } - if (error !== null) - return error.error; + return function(err) { + self.error.push(err); + self.callback(); + }; +}; - return null; +SchemaOptionsProto.repository = function(name, value) { + return this.model && this.model.$repository ? this.model.$repository(name, value) : value; }; -/** - * Clear error collection - * @return {ErrorBuilder} - */ -ErrorBuilder.prototype.clear = function() { - var self = this; - self.errors = []; - self.count = 0; - return self; +SchemaOptionsProto.noop = function() { + this.callback(NoOp); + return this; }; /** - * Replace text in message - * @param {String} search Text to search. - * @param {String} newvalue Text to replace. - * @return {ErrorBuilder} + * + * Get a schema + * @param {String} name + * @return {Object} */ -ErrorBuilder.prototype.replace = function(search, newvalue) { - var self = this; - self.isPrepared = false; - self.replacer[search] = newvalue; - return self; +SchemaBuilderProto.get = function(name) { + return this.collection[name]; }; -/* - Serialize ErrorBuilder to JSON format - return {String} -*/ /** - * Serialize ErrorBuilder to JSON - * @param {Boolean} beautify Beautify JSON. - * @return {String} + * Create a new schema + * @alias + * @return {SchemaBuilderEntity} */ -ErrorBuilder.prototype.json = function(beautify) { - if (beautify) - return JSON.stringify(this.prepare().errors, null, '\t'); - return JSON.stringify(this.prepare().errors); +SchemaBuilderProto.create = function(name) { + this.collection[name] = new SchemaBuilderEntity(this, name); + return this.collection[name]; }; /** - * Serialize ErrorBuilder to JSON - * @param {Boolean} beautify Beautify JSON. - * @return {String} + * Removes an existing schema or group of schemas + * @param {String} name Schema name, optional. + * @return {SchemaBuilder} */ -ErrorBuilder.prototype.JSON = function(beautify) { - if (beautify) - return JSON.stringify(this.prepare().errors, null, '\t'); - return JSON.stringify(this.prepare().errors); +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 { + exports.remove(this.name); + this.collection = null; + } }; -/** - * Internal: Prepare error messages with onResource() - * @private - * @return {ErrorBuidler} - */ -ErrorBuilder.prototype._prepare = function() { - var self = this; +SchemaBuilderProto.destroy = function(name) { + return this.remove(name); +}; - if (self.onResource === null) - return self; +function SchemaBuilderEntity(parent, name) { + this.parent = parent; + this.name = name; + this.primary; + this.trim = true; + this.schema = {}; + this.meta = {}; + this.properties = []; + this.inherits = []; + this.verifications = null; + this.resourcePrefix; + this.extensions = {}; + this.resourceName; + this.transforms; + this.workflows; + this.hooks; + this.operations; + this.constants; + this.onPrepare; + this.$onPrepare; // Array of functions for inherits + this.onDefault; + this.$onDefault; // Array of functions for inherits + this.onValidate = F.onValidate; + this.onSave; + this.onInsert; + this.onUpdate; + this.onGet; + this.onRemove; + this.onQuery; + this.onError; + this.gcache = {}; + this.dependencies; + this.fields; + this.fields_allow; + this.CurrentSchemaInstance = function(){}; + this.CurrentSchemaInstance.prototype = new SchemaInstance(); + this.CurrentSchemaInstance.prototype.$$schema = this; +} - var errors = self.errors; - var length = errors.length; +const SchemaBuilderEntityProto = SchemaBuilderEntity.prototype; - for (var i = 0; i < length; i++) { +SchemaBuilderEntityProto.allow = function() { + var self = this; - var o = errors[i]; + if (!self.fields_allow) + self.fields_allow = []; - if (o.error[0] !== '@') - continue; + var arr = arguments; - if (o.error.length === 1) - o.error = self.onResource(o.name); - else - o.error = self.onResource(o.error.substring(1)); + if (arr.length === 1) + arr = arr[0].split(',').trim(); - if (typeof(o.error) === UNDEFINED) - o.error = REQUIRED.replace('@', o.name); - } + 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(arr[i]); + } + return self; +}; - return self; +SchemaBuilderEntityProto.before = function(name, fn) { + var self = this; + if (!self.preparation) + self.preparation = {}; + self.preparation[name] = fn; + return self; }; -/** - * To string - * @return {String} - */ -ErrorBuilder.prototype.toString = function() { +SchemaBuilderEntityProto.required = function(name, fn) { + + var self = this; - var self = this; + if (!name) + return self; - if (!self.isPrepared) - self.prepare(); + if (name.indexOf(',') !== -1) { + var arr = name.split(','); + for (var i = 0; i < arr.length; i++) + self.required(arr[i].trim(), fn); + return self; + } - var errors = self.errors; - var length = errors.length; - var builder = []; + if (fn === false) { + self.properties && (self.properties = self.properties.remove(name)); + return self; + } - for (var i = 0; i < length; i++) - builder.push(errors[i].error || errors[i].name); + var prop = self.schema[name]; + if (!prop) + throw new Error('Property "{0}" doesn\'t exist in "{1}" schema.'.format(name, self.name)); - return builder.join('\n'); + prop.can = typeof(fn) === 'function' ? fn : null; + if (!prop.required) { + prop.required = true; + if (self.properties) { + self.properties.indexOf(name) === -1 && self.properties.push(name); + } else + self.properties = [name]; + } + + return self; }; -/** - * Internal: Prepare error messages with onResource() - * @private - * @return {ErrorBuidler} - */ -ErrorBuilder.prototype._prepareReplace = function() { +SchemaBuilderEntityProto.clear = function() { + var self = this; + + self.schema = {}; + self.properties = []; + self.fields = []; + self.verifications = null; - var self = this; - var errors = self.errors; - var lengthBuilder = errors.length; - var keys = Object.keys(self.replacer); - var lengthKeys = keys.length; + if (self.preparation) + self.preparation = null; - if (lengthBuilder === 0 || lengthKeys === 0) - return self; + if (self.dependencies) + self.dependencies = null; - for (var i = 0; i < lengthBuilder; i++) { - var o = errors[i]; - for (var j = 0; j < lengthKeys; j++) { - var key = keys[j]; - o.error = o.error.replace(key, self.replacer[key]); - } - } + if (self.fields_allow) + self.fields_allow = null; - return self; + return self; }; -/** - * Internal: Prepare error messages with onResource() - * @private - * @return {ErrorBuidler} - */ -ErrorBuilder.prototype.prepare = function() { - var self = this; +SchemaBuilderEntityProto.middleware = function(fn) { + var self = this; + if (!self.middlewares) + self.middlewares = []; + self.middlewares.push(fn); + return self; +}; - if (self.isPrepared) - return self; +function runmiddleware(opt, schema, callback, index, processor) { - self._prepare()._prepareReplace(); - self.isPrepared = true; + if (!index) + index = 0; - return self; -}; + 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); +} /** - * Refresh pagination - * @param {Number} items Count of items. - * @param {Number} page Current page. - * @param {Number} max Max items on page. - * @return {Pagination} + * Define type in schema + * @param {String|String[]} name + * @param {Object/String} type + * @param {Boolean} [required=false] Is required? Default: false. + * @param {Number|String} [custom] Custom tag for search. + * @return {SchemaBuilder} */ -Pagination.prototype.refresh = function(items, page, max) { - var self = this; +SchemaBuilderEntityProto.define = function(name, type, required, custom) { + + if (name instanceof Array) { + for (var i = 0, length = name.length; i < length; i++) + this.define(name[i], type, required, custom); + return this; + } + + var rt = typeof(required); + + if (required !== undefined && rt === 'string') { + custom = required; + 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; + + var a = this.schema[name] = this.$parse(name, type, required, custom); + switch (this.schema[name].type) { + case 7: + if (this.dependencies) + this.dependencies.push(name); + else + this.dependencies = [name]; + break; + } + + this.fields = Object.keys(this.schema); + + 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 function(val) { + a.def = val; + return this; + }; +}; + +SchemaBuilderEntityProto.verify = function(name, fn, cache) { + var self = this; - self.count = Math.floor(items / max) + (items % max > 0 ? 1 : 0); - self.page = page - 1; + if (!self.verifications) + self.verifications = []; - if (self.page < 0) - self.page = 0; + var cachekey; - self.items = items; - self.skip = self.page * max; - self.take = max; - self.max = max; - self.isPrev = self.page > 0; - self.isNext = self.page < self.count - 1; - self.visible = self.count > 1; - self.page++; + if (cache) + cachekey = self.name + '_verify_' + name + '_'; - return self; + self.verifications.push({ name: name, fn: fn, cache: cache, cachekey: cachekey }); + return self; }; -/** - * Get previous page - * @param {String} format Custom format (optional). - * @return {Object} Example: { url: String, page: Number, selected: Boolean } - */ -Pagination.prototype.prev = function(format) { - var self = this; - var page = 0; +SchemaBuilderEntityProto.inherit = function(group, name) { - format = format || self.format; + if (!name) { + name = group; + group = DEFAULT_SCHEMA; + } - if (self.isPrev) - page = self.page - 1; - else - page = self.count; + var self = this; - return { - url: format.format(page, self.items, self.count), - page: page, - selected: false - }; -}; + exports.getschema(group, name, function(err, schema) { -/** - * Get next page - * @param {String} format Custom format (optional). - * @return {Object} Example: { url: String, page: Number, selected: Boolean } - */ -Pagination.prototype.next = function(format) { - var self = this; - var page = 0; + if (err) + throw err; - format = format || self.format; + self.primary = schema.primary; + self.inherits.push(schema); - if (self.isNext) - page = self.page + 1; - else - page = 1; + if (!self.resourceName && schema.resourceName) + self.resourceName = schema.resourceName; - return { - url: format.format(page, self.items, self.count), - page: page, - selected: false - }; -}; + if (!self.resourcePrefix && schema.resourcePrefix) + self.resourcePrefix = schema.resourcePrefix; -/** - * Create pagination - * @param {Number} max Max pages in collection (optional). - * @param {String} format Custom format (optional). - * @return {Object Array} Example: [{ url: String, page: Number, selected: Boolean }] - */ -Pagination.prototype.render = function(max, format) { + copy_inherit(self, 'schema', schema.schema); + copy_inherit(self, 'meta', schema.meta); + copy_inherit(self, 'transforms', schema.transforms); + copy_inherit(self, 'workflows', schema.workflows); + copy_inherit(self, 'hooks', schema.hooks); + copy_inherit(self, 'operations', schema.operations); + copy_inherit(self, 'constants', schema.constants); - var self = this; - var builder = []; - format = format || self.format; + if (schema.middlewares) { + self.middlewares = []; + for (var i = 0; i < schema.middlewares.length; i++) + self.middlewares.push(schema.middlewares[i]); + } - if (typeof(max) === STRING) { - var tmp = format; - format = max; - max = format; - } + if (schema.verifications) { + self.verifications = []; + for (var i = 0; i < schema.verifications.length; i++) + self.verifications.push(schema.verifications[i]); + } - if (typeof(max) === UNDEFINED || max === null) { - for (var i = 1; i < self.count + 1; i++) - builder.push({ - url: format.format(i, self.items, self.count), - page: i, - selected: i === self.page - }); - return builder; - } + schema.properties.forEach(function(item) { + if (self.properties.indexOf(item) === -1) + self.properties.push(item); + }); - var half = Math.floor(max / 2); - var pages = self.count; + if (schema.preparation) { + self.preparation = {}; + Object.keys(schema.preparation).forEach(function(key) { + self.preparation[key] = schema.preparation[key]; + }); + } - var pageFrom = self.page - half; - var pageTo = self.page + half; - var plus = 0; + if (schema.onPrepare) { + if (!self.$onPrepare) + self.$onPrepare = []; + self.$onPrepare.push(schema.onPrepare); + } - if (pageFrom <= 0) { - plus = Math.abs(pageFrom); - pageFrom = 1; - pageTo += plus; - } + if (schema.onDefault) { + if (!self.$onDefault) + self.$onDefault = []; + self.$onDefault.push(schema.onDefault); + } - if (pageTo >= pages) { - pageTo = pages; - pageFrom = pages - max; - } + if (self.onValidate === F.onValidate && self.onValidate !== schema.onValidate) + self.onValidate = schema.onValidate; - if (pageFrom < 0) - pageFrom = 1; + if (!self.onSave && schema.onSave) + self.onSave = schema.onSave; - for (var i = pageFrom; i < pageTo + 1; i++) - builder.push({ - url: format.format(i, self.items, self.count), - page: i, - selected: i === self.page - }); + if (!self.onInsert && schema.onInsert) + self.onInsert = schema.onInsert; - return builder; -}; + if (!self.onUpdate && schema.onUpdate) + self.onUpdate = schema.onUpdate; -/** - * Add parameter - * @param {String} name - * @param {Object} value - * return {UrlBuilder} - */ -UrlBuilder.prototype.add = function(name, value) { - var self = this; + if (!self.onGet && schema.onGet) + self.onGet = schema.onGet; + + if (!self.onRemove && schema.onRemove) + self.onRemove = schema.onRemove; - if (typeof(name) === 'object') { - Object.keys(name).forEach(function(o) { - self.builder[o] = name[o]; - }); - return; - } + if (!self.onQuery && schema.onQuery) + self.onQuery = schema.onQuery; - self.builder[name] = value; - return self; + if (!self.onError && schema.onError) + self.onError = schema.onError; + + self.fields = Object.keys(self.schema); + }); + + return self; }; +function copy_inherit(schema, field, value) { + + if (!value) + return; + + if (value && !schema[field]) { + schema[field] = framework_utils.clone(value); + return; + } + + Object.keys(value).forEach(function(key) { + if (schema[field][key] === undefined) + schema[field][key] = framework_utils.clone(value[key]); + }); +} + /** - * Remove parameter - * @param {String} name - * @return {UrlBuilder} + * Set primary key + * @param {String} name */ -UrlBuilder.prototype.remove = function(name) { - var self = this; - delete self.builder[name]; - return self; +SchemaBuilderEntityProto.setPrimary = function(name) { + this.primary = name; + return this; }; /** - * Read value - * @param {String} name - * @return {Object} + * Filters current names of the schema via custom attribute + * @param {Number/String} custom + * @param {Object} model Optional + * @param {Boolean} reverse Reverse results. + * @return {Array|Object} Returns Array (with property names) if the model is undefined otherwise returns Object Name/Value. */ -UrlBuilder.prototype.read = function(name) { - return this.builder[name] || null; +SchemaBuilderEntityProto.filter = function(custom, model, reverse) { + + if (typeof(model) === 'boolean') { + var tmp = reverse; + reverse = model; + model = tmp; + } + + var output = model === undefined ? [] : {}; + var type = typeof(custom); + var isSearch = type === 'string' ? custom[0] === '*' || custom[0] === '%' : false; + var isReg = false; + + if (isSearch) + custom = custom.substring(1); + else if (type === 'object') + isReg = framework_utils.isRegExp(custom); + + for (var prop in this.schema) { + + var schema = this.schema[prop]; + if (!schema) + continue; + + var tv = typeof(schema.custom); + + if (isSearch) { + if (tv === 'string') { + if (schema.custom.indexOf(custom) === -1) { + if (!reverse) + continue; + } else if (reverse) + continue; + } else + continue; + } else if (isReg) { + if (tv === 'string') { + if (!custom.test(schema.current)) { + if (!reverse) + continue; + } else if (reverse) + continue; + } else + continue; + } else if (schema.custom !== custom) { + if (!reverse) + continue; + } else if (reverse) + continue; + + if (model === undefined) + output.push(prop); + else + output[prop] = model[prop]; + } + + return output; }; -/** - * Clear parameter collection - * @return {UrlBuilder} - */ -UrlBuilder.prototype.clear = function() { - var self = this; - self.builder = {}; - return self; +function parseLength(lower, result) { + result.raw = 'string'; + var beg = lower.indexOf('('); + if (beg !== -1) { + result.length = lower.substring(beg + 1, lower.length - 1).parseInt(); + result.raw = lower.substring(0, beg); + } + return result; +} + +SchemaBuilderEntityProto.$parse = function(name, value, required, custom) { + + var type = typeof(value); + var result = {}; + + result.raw = value; + result.type = 0; + result.length = 0; + result.required = required ? true : false; + result.validate = typeof(required) === 'function' ? required : null; + result.can = null; + result.isArray = false; + result.custom = custom || ''; + + // 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; + + if (value === '[]') { + result.isArray = true; + return result; + } + + 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; + } + + if (value === String) { + result.type = 3; + return result; + } + + if (value === Boolean) { + result.type = 4; + return result; + } + + if (value === Date) { + result.type = 5; + return result; + } + + if (value === Array) { + result.isArray = true; + return result; + } + + if (value === Object) { + result.type = 6; + return result; + } + + if (value instanceof SchemaBuilderEntity) + result.type = 7; + else { + result.type = 10; + if (!this.asyncfields) + this.asyncfields = []; + this.asyncfields.push(name); + } + + return result; + } + + if (type === 'object') { + if (value instanceof Array) { + result.type = 8; // enum + result.subtype = typeof(value[0]); + } else + result.type = 9; // keyvalue + return result; + } + + if (value[0] === '[') { + value = value.substring(1, value.length - 1); + result.isArray = true; + result.raw = value; + } + + var lower = value.toLowerCase(); + + if (lower === 'object') { + result.type = 6; + return result; + } + + if (lower === 'array') { + result.isArray = true; + 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'; + return parseLength(lower, result); + } + + if ((/^(lower|lowercase)+(\(\d+\))?$/).test(lower)) { + result.subtype = 'lowercase'; + result.type = 3; + 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; + return parseLength(lower, result); + } + + if (lower === 'uid') { + result.type = 3; + result.length = 20; + result.raw = 'string'; + result.subtype = 'uid'; + return result; + } + + if (lower === 'email') { + result.type = 3; + result.length = 120; + result.raw = 'string'; + result.subtype = 'email'; + return result; + } + + if (lower === 'json') { + result.type = 3; + result.raw = 'string'; + result.subtype = 'json'; + return result; + } + + if (lower === 'url') { + result.type = 3; + result.length = 500; + result.raw = 'string'; + result.subtype = 'url'; + return result; + } + + if (lower === 'zip') { + result.type = 3; + result.length = 10; + result.raw = 'string'; + result.subtype = 'zip'; + return result; + } + + if (lower === 'phone') { + result.type = 3; + result.length = 20; + result.raw = 'string'; + result.subtype = 'phone'; + return result; + } + + if (lower === 'number2') { + result.type = 11; + return result; + } + + if (['int', 'integer', 'byte'].indexOf(lower) !== -1) { + result.type = 1; + return result; + } + + if (['decimal', 'number', 'float', 'double'].indexOf(lower) !== -1) { + result.type = 2; + return result; + } + + if (['bool', 'boolean'].indexOf(lower) !== -1) { + result.type = 4; + return result; + } + + if (['date', 'time', 'datetime'].indexOf(lower) !== -1) { + result.type = 5; + return result; + } + + result.type = 7; + return result; }; -/** - * Create URL - * @return {String} - */ -UrlBuilder.prototype.toString = function() { +SchemaBuilderEntityProto.getDependencies = function() { + var dependencies = []; + + for (var name in this.schema) { - var self = this; - var builder = []; + var type = this.schema[name]; + if (typeof(type) !== 'string') + continue; - Object.keys(self.builder).forEach(function(o) { - builder.push(o + '=' + encodeURIComponent(self.builder[o] || '')); - }); + var isArray = type[0] === ']'; + if (isArray) + type = type.substring(1, type.length - 1); - return builder.join('&'); + var m = this.parent.get(type); + m && dependencies.push({ name: name, isArray: isArray, schema: m }); + } + + return dependencies; }; /** - * Has these parameters? - * @param {String Array} keys Keys. - * @return {Boolean} + * Set schema validation + * @param {String|Array} properties Properties to validate, optional. + * @param {Function(propertyName, value, path, entityName, model)} fn A validation function. + * @return {SchemaBuilderEntity} */ -UrlBuilder.prototype.hasValue = function(keys) { +SchemaBuilderEntityProto.setValidate = function(properties, fn) { - if (typeof(keys) === UNDEFINED) - return false; + if (fn === undefined && properties instanceof Array) { + this.properties = properties; + return this; + } - var self = this; + if (typeof(properties) !== 'function') { + this.properties = properties; + this.onValidate = fn; + } else + this.onValidate = properties; - if (typeof(keys) === 'string') - keys = [keys]; + return this; +}; - for (var i = 0; i < keys.length; i++) { - var val = self.builder[keys[i]]; - if (typeof(val) === UNDEFINED || val === null) - return false; - } +SchemaBuilderEntityProto.setPrefix = function(prefix) { + this.resourcePrefix = prefix; + return this; +}; - return true; +SchemaBuilderEntityProto.setResource = function(name) { + this.resourceName = name; + return this; }; /** - * Render paramerters - * @param {String Array} keys Keys. - * @param {String} delimiter Delimiter (default &). - * @return {String} + * Set the default values for schema + * @param {Function(propertyName, isntPreparing, entityName)} fn + * @return {SchemaBuilderEntity} */ -UrlBuilder.prototype.toOne = function(keys, delimiter) { - - var self = this; - var builder = []; +SchemaBuilderEntityProto.setDefault = function(fn) { + this.onDefault = fn; + return this; +}; - keys.forEach(function(o) { - builder.push(self.builder[o] || ''); - }); +/** + * Set the prepare + * @param {Function(name, value)} fn Must return a new value. + * @return {SchemaBuilderEntity} + */ +SchemaBuilderEntityProto.setPrepare = function(fn) { + this.onPrepare = fn; + return this; +}; + +/** + * Set save handler + * @param {Function(error, model, helper, next(value), controller)} fn + * @return {SchemaBuilderEntity} + */ +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 || null; + this.meta.savefilter = filter; + !fn.$newversion && OBSOLETE('Schema("{0}").setSave()'.format(this.name), MSG_OBSOLETE_NEW); + return this; +}; + +SchemaBuilderEntityProto.setSaveExtension = function(fn) { + var key = 'save'; + if (this.extensions[key]) + this.extensions[key].push(fn); + else + this.extensions[key] = [fn]; + return this; +}; + +/** + * Set insert handler + * @param {Function(error, model, helper, next(value), controller)} fn + * @return {SchemaBuilderEntity} + */ +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.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 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} + */ +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 || 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; +}; + +/** + * Set remove handler + * @param {Function(error, helper, next(value), controller)} fn + * @param {String} description Optional. + * @return {SchemaBuilderEntity} + */ +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 || 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; +}; + +/** + * Add a new constant for the schema + * @param {String} name Constant name, optional. + * @param {Object} value + * @param {String} description Optional. + * @return {SchemaBuilderEntity} + */ +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 || null; + return this; +}; + +/** + * Add a new transformation for the entity + * @param {String} name Transform name, optional. + * @param {Function(errorBuilder, model, helper, next([output]), controller)} fn + * @param {String} description Optional. + * @return {SchemaBuilderEntity} + */ +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 || 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; +}; + +/** + * Add a new operation for the entity + * @param {String} name Operation name, optional. + * @param {Function(errorBuilder, [model], helper, next([output]), controller)} fn + * @param {String} description Optional. + * @return {SchemaBuilderEntity} + */ +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 || 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; +}; + +/** + * Add a new workflow for the entity + * @param {String} name Workflow name, optional. + * @param {Function(errorBuilder, model, helper, next([output]), controller)} fn + * @param {String} description Optional. + * @return {SchemaBuilderEntity} + */ +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 || 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; +}; + +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 || 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; +}; + +/** + * Find an entity in current group + * @param {String} name + * @return {SchemaBuilderEntity} + */ +SchemaBuilderEntityProto.find = function(name) { + return this.parent.get(name); +}; + +/** + * Destroys current entity + */ +SchemaBuilderEntityProto.destroy = function() { + delete this.parent.collection[this.name]; + 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; +}; + +/** + * Execute onSave 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.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; + callback = options; + options = undefined; + } else if (typeof(options) === 'function') { + callback = options; + options = undefined; + } + + if (typeof(controller) === 'boolean') { + var tmp = skip; + skip = controller; + controller = tmp; + } + + if (typeof(callback) !== 'function') + callback = function(){}; + + var self = this; + 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) { + + if (err) { + callback(err, model); + 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[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; + } + + callback.success = false; + + var onError = function(err) { + if (!err || callback.success) + return; + + callback.success = true; + + 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; + + if (arguments.length === 2 || (res instanceof Error || res instanceof ErrorBuilder)) { + if ((res instanceof Error || res instanceof ErrorBuilder) && builder !== res) + builder.push(res); + res = arguments[1]; + } + + 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[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*'; +} + +/** + * Execute onGet delegate + * @param {Object} options Custom options object, optional + * @param {Function(err, result)} callback + * @return {SchemaBuilderEntity} + */ +SchemaBuilderEntityProto.get = SchemaBuilderEntityProto.read = function(options, callback, controller) { + + if (typeof(options) === 'function') { + callback = options; + options = undefined; + } + + if (typeof(callback) !== 'function') + callback = function(){}; + + 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) { + 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) { + CONF.logger && F.ilogger(self.getLoggerName($type), controller, $now); + self.$process(arguments, output, $type, undefined, builder, res, callback, controller); + }, controller); + return self; + } + + callback.success = false; + + var onError = function(err) { + if (!err || callback.success) + return; + callback.success = true; + + 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; + + if (arguments.length === 2 || (res instanceof Error || res instanceof ErrorBuilder)) { + if ((res instanceof Error || res instanceof ErrorBuilder) && builder !== res) + builder.push(res); + res = arguments[1]; + } + + callback.success = true; + var has = builder.is; + has && self.onError && self.onError(builder, output, $type); + callback(has ? builder : null, res === undefined ? output : res); + }; + + 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; +}; + +/** + * Execute onRemove delegate + * @param {Object} options Custom options object, optional + * @param {Function(err, result)} callback + * @return {SchemaBuilderEntity} + */ +SchemaBuilderEntityProto.remove = function(options, callback, controller) { + + if (typeof(options) === 'function') { + callback = options; + options = undefined; + } + + 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) { + + 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) { + CONF.logger && F.ilogger(self.getLoggerName($type), controller, $now); + self.$process(arguments, undefined, $type, undefined, builder, res, callback, controller); + }, controller); + return self; + } + + callback.success = false; + + var onError = function(err) { + if (!err || callback.success) + return; + callback.success = true; + + 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; + + if (arguments.length === 2 || (res instanceof Error || res instanceof ErrorBuilder)) { + if ((res instanceof Error || res instanceof ErrorBuilder) && builder !== res) + builder.push(res); + res = arguments[1]; + } + + 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) { + 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; +}; + +/** + * Execute onQuery delegate + * @param {Object} options Custom options object, optional + * @param {Function(err, result)} callback + * @return {SchemaBuilderEntity} + */ +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) { + 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) { + CONF.logger && F.ilogger(self.getLoggerName($type), controller, $now); + self.$process(arguments, undefined, $type, undefined, builder, res, callback, controller); + }, controller); + return self; + } + + callback.success = false; + + var onError = function(err) { + if (!err || callback.success) + return; + callback.success = true; + + 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; + + if (arguments.length === 2 || (res instanceof Error || res instanceof ErrorBuilder)) { + if ((res instanceof Error || res instanceof ErrorBuilder) && builder !== res) + builder.push(res); + res = arguments[1]; + } + + var has = builder.is; + has && self.onError && self.onError(builder, EMPTYOBJECT, $type); + callback.success = true; + callback(builder.is ? builder : null, res); + }; + + 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; +}; + +/** + * Validate a schema + * @param {Object} model Object to validate. + * @param {String} resourcePrefix Prefix for resource key. + * @param {String} resourceName Resource filename. + * @param {ErrorBuilder} builder ErrorBuilder, INTERNAL. + * @return {ErrorBuilder} + */ +SchemaBuilderEntityProto.validate = function(model, resourcePrefix, resourceName, builder, filter, path, index) { + + var self = this; + + if (builder === undefined) { + builder = new ErrorBuilder(); + self.resourceName && builder.setResource(self.resourceName); + self.resourcePrefix && builder.setPrefix(self.resourcePrefix); + } + + if (self.resourcePrefix) + builder.resourcePrefix = self.resourcePrefix; + + if (self.resourceName) + builder.resourceName = self.resourceName; + + if (resourceName) + builder.resourceName = resourceName; + + if (resourcePrefix) + builder.resourcePrefix = resourcePrefix; + + if (filter) + filter = self.filter(filter); + + if (path) + path += '.'; + else + path = ''; + + framework_utils.validate_builder.call(self, model, builder, self, '', index, filter, path); + return builder; +}; + +/** + * Create a default object according the schema + * @alias SchemaBuilderEntity.default() + * @return {Object} + */ +SchemaBuilderEntityProto.create = function() { + return this.default(); +}; + +SchemaBuilderEntityProto.Create = function() { + return this.default(); +}; + +/** + * Makes extensible object + * @param {Object} obj + * @return {Object} + */ +SchemaBuilderEntityProto.$make = function(obj) { + return obj; +}; + +SchemaBuilderEntityProto.$prepare = function(obj, callback) { + if (obj && typeof(obj.$save) === 'function') + callback(null, obj); + else + this.make(obj, (err, model) => callback(err, model)); + return this; +}; + +/** + * Create a default object according the schema + * @return {SchemaInstance} + */ +SchemaBuilderEntityProto.default = function() { + + var obj = this.schema; + if (obj === null) + return null; + + var item = new this.CurrentSchemaInstance(); + var defaults = this.onDefault || this.$onDefault ? true : false; + + for (var property in obj) { + + var type = obj[property]; + + if (defaults) { + var def = this.$ondefault(property, true, this.name); + if (def !== undefined) { + item[property] = def; + continue; + } + } + + 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 + case 1: + 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 ? [] : type.subtype === 'email' ? '@' : ''; + break; + // boolean + case 4: + item[property] = type.isArray ? [] : false; + break; + // date + case 5: + item[property] = type.isArray ? [] : NOW; + break; + // schema + case 7: + + if (type.isArray) { + item[property] = []; + } else { + var tmp = this.parent.collection[type.raw] || GETSCHEMA(type.raw); + if (tmp) { + item[property] = tmp.default(); + } else { + F.error(new Error('Schema: "' + property + '.' + type.raw + '" not found in "' + this.parent.name + '".')); + item[property] = null; + } + } + break; + + // enum + keyvalue + case 8: + case 9: + item[property] = undefined; + break; + } + } + + return item; +}; + +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(); + }; +} + +SchemaOptionsVerify.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; + } +}; + +/** + * 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; +} + +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, req); + if (tmp !== undefined) + val = tmp; + } + } + + if (this.onPrepare) + 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; +}; + +SchemaBuilderEntityProto.$ondefault = function(property, create, entity) { + + var val; + + if (this.onDefault) { + val = this.onDefault(property, create, entity); + if (val !== undefined) + return val; + } + + if (this.$onDefault) { + for (var i = 0, length = this.$onDefault.length; i < length; i++) { + val = this.$onDefault[i](property, create, entity); + if (val !== undefined) + return val; + } + } +}; + +/** + * Prepare model according to schema + * @param {Object} model + * @param {String|Array} [dependencies] INTERNAL. + * @return {SchemaInstance} + */ +SchemaBuilderEntityProto.prepare = function(model, dependencies, req, verifications) { + + var self = this; + var obj = self.schema; + + if (obj === null) + return null; + + if (model == null) + return self.default(); + + var tmp; + 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; + + 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 typeval = typeof(val); + + if (typeval === 'function') + val = val(); + + if (!type.isArray) { + + switch (type.type) { + // undefined + case 0: + break; + // number: integer + case 1: + 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, def ? type.def() : type.def), undefined, model, req); + break; + + // string + case 3: + + var tv = typeof(val); + + if (val == null || tv === 'object') + tmp = ''; + else if (tv === 'string') + tmp = autotrim(self, val); + else + tmp = autotrim(self, val.toString()); + + if (type.length && type.length < tmp.length) + tmp = tmp.substring(0, type.length); + + switch (type.subtype) { + case 'uid': + if (tmp && !type.required && !tmp.isUID()) + tmp = ''; + break; + case 'email': + tmp = tmp.toLowerCase().replace(REGEXP_CLEAN_EMAIL, ''); + if (tmp && !type.required && !tmp.isEmail()) + tmp = ''; + break; + case 'url': + if (tmp && !type.required && !tmp.isURL()) + tmp = ''; + break; + case 'zip': + tmp = tmp.replace(REGEXP_CLEAN_EMAIL, ''); + if (tmp && !type.required && !tmp.isZIP()) + tmp = ''; + break; + case 'phone': + tmp = tmp.replace(REGEXP_CLEAN_PHONE, ''); + if (tmp && !type.required && !tmp.isPhone()) + tmp = ''; + break; + case 'capitalize': + tmp = tmp.capitalize(); + break; + case 'capitalize2': + tmp = tmp.capitalize(true); + break; + case 'lowercase': + tmp = tmp.toLowerCase(); + break; + case 'uppercase': + tmp = tmp.toUpperCase(); + break; + case 'json': + if (tmp && !type.required && !tmp.isJSON()) + tmp = ''; + break; + case 'base64': + if (tmp && !type.required && !tmp.isBase64()) + tmp = ''; + break; + } + + 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; + 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 + case 5: + + tmp = null; + + if (typeval === 'string') { + if (val) + tmp = val.trim().parseDate(); + } else if (typeval === 'number') + tmp = new Date(val); + else + tmp = val; + + if (framework_utils.isDate(tmp)) + 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, 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, 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, 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 = (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; + } + } + + if (val && typeof(val.$schema) === 'function') { + tmp = val.$schema(); + if (tmp && tmp.name && tmp.name === type.raw) { + item[property] = val; + break; + } + } + + entity = GETSCHEMA(type.raw); + if (entity) { + + 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] = (type.def === undefined ? defaults ? isUndefined(self.$ondefault(property, false, self.name), EMPTYARRAY) : [] : (def ? type.def() : type.def)); + continue; + } + + item[property] = []; + for (var j = 0, sublength = val.length; j < sublength; j++) { + + // tmp = model[property][j]; + tmp = val[j]; + typeval = typeof(tmp); + + switch (type.type) { + case 0: + tmp = self.$onprepare(property, tmp, j, model, req); + break; + + case 1: + tmp = self.$onprepare(property, framework_utils.parseInt(tmp), j, model, req); + break; + + case 2: + tmp = self.$onprepare(property, framework_utils.parseFloat(tmp), j, model, req); + break; + + case 3: + + tmp = tmp == null ? '' : autotrim(self, tmp.toString()); + if (type.length && tmp.length < tmp.length) + tmp = tmp.substring(0, type.length); + + switch (type.subtype) { + case 'uid': + if (tmp && !type.required && !tmp.isUID()) + continue; + break; + case 'url': + if (tmp && !type.required && !tmp.isURL()) + continue; + break; + case 'email': + tmp = tmp.toLowerCase().replace(REGEXP_CLEAN_EMAIL, ''); + if (tmp && !type.required && !tmp.isEmail()) + continue; + break; + case 'phone': + tmp = tmp.replace(REGEXP_CLEAN_PHONE, ''); + if (tmp && !type.required && !tmp.isPhone()) + continue; + break; + case 'capitalize': + tmp = tmp.capitalize(); + break; + case 'capitalize2': + tmp = tmp.capitalize(true); + break; + case 'lowercase': + tmp = tmp.toLowerCase(); + break; + case 'uppercase': + tmp = tmp.toUpperCase(); + break; + case 'json': + 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, req); + break; + + case 4: + if (tmp) + tmp = tmp.toString().toLowerCase(); + tmp = self.$onprepare(property, BOOL[tmp], j, model, req); + break; + + case 5: + + if (typeval === 'string') { + if (tmp) + tmp = tmp.trim().parseDate(); + } else if (typeval === 'number') + tmp = new Date(tmp); + + if (framework_utils.isDate(tmp)) + tmp = self.$onprepare(property, tmp, j, model, req); + else + tmp = undefined; + + break; + + case 6: + tmp = self.$onprepare(property, tmp, j, model, req); + break; + + case 7: + + entity = self.parent.collection[type.raw] || GETSCHEMA(type.raw); + + if (entity) { + 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 + 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 }); + + break; + + 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; + } + + if (tmp !== undefined) + item[property].push(tmp); + } + } + + if (self.fields_allow) { + 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) { + 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 + * @param {Object} model + * @param {Object} options Custom options object, optional. + * @param {Function(errorBuilder, output, model)} callback + * @param {Boolean} skip Skips preparing and validation, optional. + * @param {Object} controller Optional + * @return {SchemaBuilderEntity} + */ +SchemaBuilderEntityProto.transform = function(name, model, options, callback, skip, controller) { + return this.$execute('transform', name, model, options, callback, skip, controller); +}; + +SchemaBuilderEntityProto.transform2 = function(name, options, callback, controller) { + + if (typeof(options) === 'function') { + controller = callback; + callback = options; + options = undefined; + } + + !callback && (callback = function(){}); + return this.transform(name, this.create(), options, callback, true, controller); +}; + +SchemaBuilderEntityProto.$process = function(arg, model, type, name, builder, response, callback, controller) { + + var self = this; + + if (arg.length > 1 || (response instanceof Error || response instanceof ErrorBuilder)) { + if ((response instanceof Error || response instanceof ErrorBuilder || typeof(response) === 'string') && builder !== response) + builder.push(response); + response = arg[1]; + } + + var has = builder.is; + has && self.onError && self.onError(builder, model, type, name); + + 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; +}; + +SchemaBuilderEntityProto.$process_hook = function(model, type, name, builder, result, callback) { + var self = this; + var has = builder.is; + has && self.onError && self.onError(builder, model, type, name); + callback(has ? builder : null, model, result); + return self; +}; + +/** + * Run a workflow + * @param {String} name + * @param {Object} model + * @param {Object} options Custom options object, optional. + * @param {Function(errorBuilder, output, model)} callback + * @param {Boolean} skip Skips preparing and validation, optional. + * @param {Object} controller Optional + * @return {SchemaBuilderEntity} + */ +SchemaBuilderEntityProto.workflow = function(name, model, options, callback, skip, controller) { + return this.$execute('workflow', name, model, options, callback, skip, controller); +}; + +SchemaBuilderEntityProto.workflow2 = function(name, options, callback, controller) { + + if (typeof(options) === 'function') { + controller = callback; + callback = options; + options = undefined; + } + + !callback && (callback = function(){}); + return this.workflow(name, this.create(), options, callback, true, controller); +}; + +/** + * Run hooks + * @param {String} name + * @param {Object} model + * @param {Object} helper A helper object, optional. + * @param {Function(errorBuilder, output, model)} callback + * @param {Boolean} skip Skips preparing and validation, optional. + * @return {SchemaBuilderEntity} + */ +SchemaBuilderEntityProto.hook = function(name, model, options, callback, skip, controller) { + + var self = this; + + if (typeof(name) !== 'string') { + callback = options; + options = model; + model = name; + name = 'default'; + } + + if (typeof(callback) === 'boolean') { + skip = callback; + callback = options; + options = undefined; + } else if (typeof(options) === 'function') { + callback = options; + options = undefined; + } + + if (typeof(callback) !== 'function') + callback = function(){}; + + var hook = self.hooks ? self.hooks[name] : undefined; + + if (!hook || !hook.length) { + 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 || 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) { + + var opt = new SchemaOptions(builder, model, options, function(result) { + output.push(result == undefined ? model : result); + next(); + }, 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); + + return self; + } + + self.$prepare(model, function(err, model) { + + if (err) { + callback(err, model); + return; + } + + 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) { + 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, '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) { + 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; + if (builder !== err) + builder.push(err); + next(); + }, opt); + + } else { + async.call(self, item.fn)(function(err) { + if (!err) + return; + 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; +}; + +SchemaBuilderEntityProto.hook2 = function(name, options, callback, controller) { + + if (typeof(options) === 'function') { + controller = callback; + callback = options; + options = undefined; + } + + if (!callback) + callback = function(){}; + + return this.hook(name, this.create(), options, callback, true, controller); +}; + +SchemaBuilderEntityProto.$execute = function(type, name, model, options, callback, skip, controller) { + var self = this; + + if (typeof(name) !== 'string') { + callback = options; + options = model; + model = name; + name = 'default'; + } + + if (typeof(callback) === 'boolean') { + skip = callback; + callback = options; + options = undefined; + } else if (typeof(options) === 'function') { + callback = options; + options = undefined; + } + + if (typeof(callback) !== 'function') + callback = function(){}; + + 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; + + 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) { + + 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) { + 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; + } + + self.$prepare(model, function(err, model) { + + if (err) { + callback(err, model); + 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); + + 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) { + CONF.logger && F.ilogger(self.getLoggerName(type, name), controller, $now); + self.$process(arguments, model, type, name, builder, res, callback, controller); + }, controller); + return; + } + + callback.success = false; + + var onError = function(err) { + if (!err || callback.success) + return; + callback.success = true; + 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; + + if (arguments.length === 2 || (res instanceof Error || res instanceof ErrorBuilder)) { + if ((res instanceof Error || res instanceof ErrorBuilder) && builder !== res) + builder.push(res); + res = arguments[1]; + } + + 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) { + 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 + * @param {Object} model + * @param {Object} options Custom options object, optional. + * @param {Function(errorBuilder, output, model)} callback + * @param {Boolean} skip Skips preparing and validation, optional. + * @param {Object} controller Optional + * @return {SchemaBuilderEntity} + */ +SchemaBuilderEntityProto.operation = function(name, model, options, callback, skip, controller) { + + var self = this; + + var th = typeof(options); + var tc = typeof(callback); + + if (tc === 'undefined') { + if (th === 'function') { + callback = options; + options = model; + model = undefined; + } else if (th === 'undefined') { + options = model; + model = undefined; + } + } else if (th === 'undefined') { + options = model; + model = undefined; + } else if (tc === 'boolean') { + skip = callback; + callback = options; + options = undefined; + } + + if (typeof(options) === 'function') { + callback = options; + options = undefined; + } + + if (typeof(callback) !== 'function') + callback = function(){}; + + var operation = self.operations ? self.operations[name] : undefined; + + if (!operation) { + callback(new ErrorBuilder().push('', 'Operation "{0}" not found.'.format(name))); + return self; + } + + 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 (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) { + 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) { + 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; + } + + callback.success = false; + + var onError = function(err) { + if (!err || callback.success) + return; + callback.success = true; + 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; + + if (arguments.length === 2 || (res instanceof Error || res instanceof ErrorBuilder)) { + if ((res instanceof Error || res instanceof ErrorBuilder) && builder !== res) + builder.push(res); + res = arguments[1]; + } + + 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, key, self)); + else + async.call(self, operation)(onError, builder, model, options, onCallback, controller, skip !== true); + + return self; +}; + +SchemaBuilderEntityProto.operation2 = function(name, options, callback, controller) { + + if (typeof(options) === 'function') { + controller = callback; + callback = options; + options = undefined; + } + + !callback && (callback = function(){}); + return this.operation(name, this.create(), options, callback, true, controller); +}; + +/** + * Clean model (remove state of all schemas in model). + * @param {Object} m Model. + * @param {Boolean} isCopied Internal argument. + * @return {Object} + */ +SchemaBuilderEntityProto.clean = function(m) { + return clone(m); +}; + +// For async operations, because SUCCESS() returns singleton instance everytime +function copy(obj) { + return F.isSuccess(obj) ? { success: obj.success, value: obj.value } : obj; +} + +function clone(obj) { + + if (!obj) + return obj; + + var type = typeof(obj); + if (type !== 'object' || obj instanceof Date) + return obj; + + var length; + var o; + + if (obj instanceof Array) { + + length = obj.length; + o = new Array(length); + + for (var i = 0; i < length; i++) { + type = typeof(obj[i]); + if (type !== 'object' || obj[i] instanceof Date) { + if (type !== 'function') + o[i] = obj[i]; + continue; + } + if (obj[i] instanceof SchemaInstance) + o[i] = obj[i].$clean(); + else + o[i] = clone(obj[i]); + } + + return o; + } + + o = {}; + + for (var m in obj) { + + if (SKIP[m]) + continue; + + var val = obj[m]; + + 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') + o[m] = val; + continue; + } + + // 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; +} + +/** + * Returns prototype of instances + * @returns {Object} + */ +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 + */ +function SchemaInstance() { +} + +/** + * @type {SchemaBuilderEntity} + */ +SchemaInstance.prototype.$$schema = null; + +SchemaInstance.prototype.$async = function(callback, index) { + var self = this; + !callback && (callback = function(){}); + + 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_wait(arr, onItem, onCallback, index) { + var item = arr[index]; + if (item) + onItem(item, () => async_wait(arr, onItem, onCallback, index + 1), index); + else + onCallback(); +} + +Object.defineProperty(SchemaInstance.prototype, '$parent', { + get: function() { + return this.$$parent; + }, + set: function(value) { + this.$$parent = value; + } +}); + +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 = {}; + } + + if (value !== undefined) { + this.$$repository[name] = value; + return value; + } + + return this.$$repository[name]; +}; + +SchemaInstance.prototype.$index = function(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) { + var a = this.$$async; + if (a) + a.callback = callback; + return this; +}; + +SchemaInstance.prototype.$output = function() { + var a = this.$$async; + if (a) + a.index = true; + return this; +}; + +SchemaInstance.prototype.$stop = function() { + this.async.length = 0; + return this; +}; + +const PUSHTYPE1 = { save: 1, insert: 1, update: 1, patch: 1 }; +const PUSHTYPE2 = { query: 1, get: 1, read: 1, remove: 1 }; + +SchemaInstance.prototype.$push = function(type, name, helper, first, async, callback) { + + var self = this; + var fn; + + if (PUSHTYPE1[type]) { + fn = function(next, indexer) { + self.$$schema[type](self, helper, function(err, 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; + a.callback(err, a.response); + }, self.$$controller); + }; + + } else if (PUSHTYPE2[type]) { + fn = function(next, indexer) { + self.$$schema[type](helper, function(err, 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; + a.callback(err, a.response); + }, self.$$controller); + }; + } else { + fn = function(next, indexer) { + self.$$schema[type](name, self, helper, function(err, 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; + a.callback(err, a.response); + }, self.$$controller); + }; + } + + 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, async) { + return this.$push(type, name, helper, true, async); +}; + +SchemaInstance.prototype.$exec = function(name, helper, callback) { + + if (typeof(helper) === 'function') { + callback = helper; + helper = undefined; + } + + var group = this.$$schema.parent.name; + var key = group !== 'default' ? group + '/' + this.$$schema.name : this.$$schema.name; + var workflow = F.workflows[key + '#' + name] || F.workflows[name]; + + if (workflow) + workflow(this, helper, callback || NOOP); + else + callback && callback(new ErrorBuilder().push('Workflow "' + name + '" not found in workflows.')); + + return this; +}; + +SchemaInstance.prototype.$controller = function(controller) { + this.$$controller = controller; + return this; +}; + +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.$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, 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('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, 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('remove', null, helper, null, async, callback); + + } else + this.$$schema.remove(helper, callback, this.$$controller); + + return this; +}; + +SchemaInstance.prototype.$default = function() { + return this.$$schema.default(); +}; + +SchemaInstance.prototype.$destroy = function() { + return this.$$schema.destroy(); +}; + +SchemaInstance.prototype.$transform = function(name, 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('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, 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('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, 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('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, 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('operation', name, helper, null, async, callback); + } else + this.$$schema.operation(name, this, helper, callback, undefined, this.$$controller); + + return this; +}; + +SchemaInstance.prototype.$clean = SchemaInstance.prototype.$plain = function() { + return this.$$schema.clean(this); +}; + +SchemaInstance.prototype.$clone = function() { + return framework_utils.extend(new this.$$schema.CurrentSchemaInstance(), this, true); +}; + +SchemaInstance.prototype.$prepare = function() { + return this.$$schema.prepare(this); +}; + +SchemaInstance.prototype.$schema = function() { + return this.$$schema; +}; + +SchemaInstance.prototype.$validate = function(resourcePrefix, resourceName, builder) { + return this.$$schema.validate(this, resourcePrefix, resourceName, builder); +}; + +SchemaInstance.prototype.$constant = function(name) { + return this.$$schema.constant(name); +}; + +/** + * ErrorBuilder + * @class + * @classdesc Object validation. + * @param {ErrorBuilderOnResource} onResource Resource handler. + * @property {Number} count Count of errors. + */ +function ErrorBuilder(onResource) { + + this.items = []; + this.transformName = transforms.error_default; + this.onResource = onResource; + 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 = 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(); +} + +/** + * @callback ErrorBuilderOnResource + * @param {String} name Filename of resource. + * @param {String} key Resource key. + * @return {String} + */ + +/** + * UrlBuilder + * @class + * @classdesc CRUD parameters in URL. + */ +function UrlBuilder() { + this.builder = {}; +} + +exports.isSchema = function(obj) { + return obj instanceof SchemaInstance; +}; + +global.EACHSCHEMA = exports.eachschema = function(group, fn) { + + if (fn === undefined) { + fn = group; + group = undefined; + } + + var groups = group ? [group] : Object.keys(schemas); + for (var i = 0, length = groups.length; i < length; i++) { + var schema = schemas[groups[i]]; + if (!schema) + continue; + var collection = Object.keys(schema.collection); + for (var j = 0, jl = collection.length; j < jl; j++) + fn(schema.name, schema.collection[collection[j]].name, schema.collection[collection[j]]); + } +}; + +global.$$$ = global.GETSCHEMA = exports.getschema = function(group, name, fn, timeout) { + + if (!name || typeof(name) === 'function') { + timeout = fn; + fn = name; + } 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(() => !!schemasall[group], err => fn(err, schemasall[group]), timeout || 20000); + else + return schemasall[group]; +}; + +exports.findschema = function(groupname) { + return schemasall[groupname.toLowerCase()]; +}; + +exports.newschema = function(group, name) { + + if (!group) + group = DEFAULT_SCHEMA; + + if (!schemas[group]) + 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; +}; + +/** + * Remove a schema + * @param {String} group Optional + * @param {String} name + */ +exports.remove = function(group, name) { + if (name) { + + var g = schemas[group || DEFAULT_SCHEMA]; + g && g.remove(name); + 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) { + var keys = Object.keys(operations); + for (var i = 0, length = keys.length; i < length; i++) + fn(keys[i]); +}; + +/** + * Check if property value is joined to other class + * @private + * @param {String} value Property value from Schema definition. + * @return {Boolean} + */ +exports.isJoin = function(collection, value) { + if (!value) + return false; + if (value[0] === '[') + return true; + if (collection === undefined) + return false; + return collection[value] !== undefined; +}; + +/** + * Create validation + * @param {String} name Schema name. + * @param {Function|Array} fn Validator Handler or Property names as array for validating. + * @param {String|Array} properties Valid only these properties, optional. + * @return {Function|Array} + */ +exports.validation = function(name, properties, fn) { + + if (schemas[DEFAULT_SCHEMA] === undefined) + return EMPTYARRAY; + + var schema = schemas[DEFAULT_SCHEMA].get(name); + if (schema === undefined) + return EMPTYARRAY; + + if (fn instanceof Array && typeof(properties) === 'function') { + var tmp = fn; + fn = properties; + properties = tmp; + } + + if (typeof(fn) === 'function') { + schema.onValidate = fn; + if (properties) + schema.properties = properties; + else + schema.properties = Object.keys(schema.schema); + return true; + } + + if (!fn) { + var validator = schema.properties; + if (validator === undefined) + return Object.keys(schema.schema); + return validator || []; + } + + schema.onValidate = fn; + return fn; +}; + +/** + * Validate model + * @param {String} name Schema name. + * @param {Object} model Object for validating. + * @return {ErrorBuilder} + */ +exports.validate = function(name, model, resourcePrefix, resourceName) { + var schema = schemas[DEFAULT_SCHEMA]; + if (schema === undefined) + return null; + schema = schema.get(name); + model = schema.prepare(model); + return schema === undefined ? null : schema.validate(model, resourcePrefix, resourceName); +}; + +/** + * Create default object according to schema + * @param {String} name Schema name. + * @return {Object} + */ +exports.create = function(name) { + return exports.defaults(name); +}; + +/** + * Create default object according to schema + * @param {String} name Schema name. + * @return {Object} + */ +exports.defaults = function(name) { + if (schemas[DEFAULT_SCHEMA] === undefined) + return null; + var schema = schemas[DEFAULT_SCHEMA].get(name); + return schema === undefined ? null : schema.default(); +}; + +/** + * Prepare object according to schema + * @param {String} name Schema name. + * @param {Object} model Object to prepare. + * @return {Object} Prepared object. + */ +exports.prepare = function(name, model) { + if (schemas[DEFAULT_SCHEMA] === undefined) + return null; + var schema = schemas[DEFAULT_SCHEMA].get(name); + return schema === undefined ? null : schema.prepare(model); +}; + +function isUndefined(value, def) { + return value === undefined ? (def === EMPTYARRAY ? [] : def) : value; +} + +// ====================================================== +// PROTOTYPES +// ====================================================== + +ErrorBuilder.prototype = { + + get errors() { + var self = this; + !self.isPrepared && self.prepare(); + return self._transform(); + }, + + get error() { + var self = this; + !self.isPrepared && self.prepare(); + return self._transform(); + }, + + get is() { + return this.items.length > 0; + }, + + get length() { + return this.items.length; + } +}; + +/** + * Resource setting + * @param {String} name Resource name. + * @param {String} prefix Resource prefix. + * @return {ErrorBuilder} + */ +ErrorBuilder.prototype.resource = function(name, prefix) { + var self = this; + self.isResourceCustom = true; + self.resourceName = name; + self.resourcePrefix = prefix || ''; + return self._resource(); +}; + +ErrorBuilder.prototype.setContentType = function(type) { + this.contentType = type; + return this; +}; + +ErrorBuilder.prototype.setResource = function(name) { + var self = this; + self.isResourceCustom = true; + self.resourceName = name; + return self._resource(); +}; + +ErrorBuilder.prototype.setPrefix = function(name) { + var self = this; + self.resourcePrefix = name || ''; + return self._resource(); +}; + +/** + * Internal: Resource wrapper + * @private + * @return {ErrorBuilder} + */ +ErrorBuilder.prototype._resource = function() { + var self = this; + self.onResource = self._resource_handler; + return self; +}; + +ErrorBuilder.prototype._resource_handler = function(name) { + var self = this; + return global.F ? F.resource(self.resourceName || 'default', name) : ''; +}; + +ErrorBuilder.prototype.exception = function(message) { + this.items.push({ name: '', error: message }); + return this; +}; + +/** + * Add an error + * @param {String} name Property name. + * @param {String|Error} error Error message. + * @param {String} path Current path (in object). + * @param {Number} index Array Index, optional. + * @return {ErrorBuilder} + */ +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. + * @param {String or Error} error Error message. + * @param {String} path Current path (in object). + * @param {Number} index Array Index, optional. + * @return {ErrorBuilder} + */ +ErrorBuilder.prototype.push = function(name, error, path, index, prefix) { + + this.isPrepared = false; + + if (name instanceof ErrorBuilder) { + 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; + } + return this; + } + + if (name instanceof Array) { + for (var i = 0, length = name.length; i < length; i++) + this.push(name[i], undefined, path, index, prefix); + return this; + } + + if (error instanceof Array) { + for (var i = 0, length = error.length; i < length; i++) + this.push(name, error[i], path, index, prefix); + return this; + } + + if (typeof(name) === 'object') { + path = error; + error = name; + name = ''; + } + + if (error === null || (!name && !error)) + return this; + + // Status code + if (error > 0) { + this.status = error; + error = '@'; + } else if (path > 0) { + this.status = path; + path = undefined; + } + + 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; + error = error.toString(); + } + + this.items.push({ name: name, error: typeof(error) === 'string' ? error : error.toString(), path: path, index: index, prefix: prefix }); + this.count = this.items.length; + 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. + * @return {ErrorBuilder} + */ +ErrorBuilder.prototype.remove = function(name) { + this.items = this.items.remove('name', name); + this.count = this.items.length; + return this; +}; + +/** + * Has error? + * @param {String} name Property name (optional). + * @return {Boolean} + */ +ErrorBuilder.prototype.hasError = function(name) { + return name ? this.items.findIndex('name', name) !== -1 : this.items.length > 0; +}; + +/** + * Read an error + * @param {String} name Property name. + * @return {String} + */ +ErrorBuilder.prototype.read = function(name) { + !this.isPrepared && this.prepare(); + var error = this.items.findItem('name', name); + return error ? error.error : null; +}; + +/** + * Clear error collection + * @return {ErrorBuilder} + */ +ErrorBuilder.prototype.clear = function() { + this.items = []; + this.count = 0; + return this; +}; + +/** + * Replace text in message + * @param {String} search Text to search. + * @param {String} newvalue Text to replace. + * @return {ErrorBuilder} + */ +ErrorBuilder.prototype.replace = function(search, newvalue) { + this.isPrepared = false; + this.replacer[search] = newvalue; + return this; +}; + +/** + * Serialize ErrorBuilder to JSON + * @param {Boolean} beautify Beautify JSON. + * @param {Function(key, value)} replacer JSON replacer. + * @return {String} + */ +ErrorBuilder.prototype.json = function(beautify, replacer) { + var items = this.prepare().items; + return beautify ? JSON.stringify(items, replacer, '\t') : JSON.stringify(items, replacer); +}; + +ErrorBuilder.prototype.plain = function() { + var items = this.prepare().items; + var output = ''; + for (var i = 0, length = items.length; i < length; i++) + output += (output ? ', ' : '') + items[i].error; + return output; +}; + +/** + * Serialize ErrorBuilder to JSON + * @param {Boolean} beautify Beautify JSON. + * @return {String} + */ +ErrorBuilder.prototype.JSON = function(beautify, replacer) { + return this.json(beautify, replacer); +}; + +/** + * Internal: Prepare error messages with onResource() + * @private + * @return {ErrorBuilder} + */ +ErrorBuilder.prototype._prepare = function() { + + if (!this.onResource) + return this; + + var arr = this.items; + for (var i = 0, length = arr.length; i < length; i++) { + + var o = arr[i]; + + if (o.error[0] !== '@') + continue; + + if (o.error.length === 1) + o.error = this.onResource(o.prefix ? o.prefix : (this.resourcePrefix + o.name)); + else + o.error = this.onResource(o.error.substring(1)); + + if (!o.error) + o.error = REQUIRED.replace('@', o.name); + } + + return this; +}; + +/** + * Execute a transform + * @private + * @return {Object} + */ +ErrorBuilder.prototype._transform = function(name) { + var transformName = name || this.transformName; + if (transformName) { + var current = transforms['error'][transformName]; + return current ? current.call(this) : this.items; + } + return this.items; +}; + +ErrorBuilder.prototype.output = function(isResponse) { + + if (!this.transformName) + return isResponse ? this.json() : this.items; + + var current = transforms['error'][this.transformName]; + if (current) { + this.prepare(); + return current.call(this, isResponse); + } + + return isResponse ? this.json() : this.items; +}; + +/** + * To string + * @return {String} + */ +ErrorBuilder.prototype.toString = function() { + + !this.isPrepared && this.prepare(); + + var errors = this.items; + var length = errors.length; + var builder = []; + + for (var i = 0; i < length; i++) + builder.push(errors[i].error || errors[i].name); + + return builder.join('\n'); + +}; + +/** + * Set transformation for current ErrorBuilder + * @param {String} name + * @return {ErrorBuilder} + */ +ErrorBuilder.prototype.setTransform = function(name) { + this.transformName = name; + return this; +}; + +/** + * Transform + * @param {String} name + * @return {Object} + */ +ErrorBuilder.prototype.transform = function(name) { + return this.prepare()._transform(name); +}; + +/** + * Internal: Prepare error messages with onResource() + * @private + * @return {ErrorBuidler} + */ +ErrorBuilder.prototype._prepareReplace = function() { + + var self = this; + var errors = self.items; + var lengthBuilder = errors.length; + var keys = Object.keys(self.replacer); + var lengthKeys = keys.length; + + if (!lengthBuilder || !lengthKeys) + return self; + + for (var i = 0; i < lengthBuilder; i++) { + var o = errors[i]; + for (var j = 0; j < lengthKeys; j++) { + var key = keys[j]; + o.error = o.error.replace(key, self.replacer[key]); + } + } + + return self; +}; + +/** + * Internal: Prepare error messages with onResource() + * @private + * @return {ErrorBuilder} + */ +ErrorBuilder.prototype.prepare = function() { + if (this.isPrepared) + return this; + this._prepare()._prepareReplace(); + this.isPrepared = true; + return this; +}; + +/** + * STATIC: Create transformation + * @param {String} name + * @param {Function(ErrorBuilder)} fn + * @param {Boolean} isDefault Default transformation for all error builders. + */ +ErrorBuilder.addTransform = function(name, fn, isDefault) { + transforms['error'][name] = fn; + isDefault && ErrorBuilder.setDefaultTransform(name); +}; + +/** + * STATIC: Remove transformation + * @param {String} name + */ +ErrorBuilder.removeTransform = function(name) { + delete transforms['error'][name]; +}; + +/** + * STATIC: Create transformation + * @param {String} name + * @param {Function(errorBuilder)} fn + */ +ErrorBuilder.setDefaultTransform = function(name) { + if (name) + transforms['error_default'] = name; + else + delete transforms['error_default']; +}; + +/** + * Pagination + * @class + * @param {Number} items Count of items. + * @param {Number} page Current page. + * @param {Number} max Max items on page. + * @param {String} format URL format for links (next, back, go to). Example: ?page={0} --- {0} = page, {1} = items count, {2} = page count + * @property {Number} isNext Is next page? + * @property {Number} isPrev Is previous page? + * @property {Number} count Page count. + * @property {Boolean} visible Is more than one page? + * @property {String} format Format URL. Example: ?page={0} --- {0} = page, {1} = items count, {2} = page count + */ +function Pagination(items, page, max, format) { + this.isNext = false; + this.isPrev = false; + this.isFirst = false; + this.isLast = false; + this.nextPage = 0; + this.prevPage = 0; + this.lastPage = 0; + this.firstPage = 0; + this.items = Math.max(0, +items); + this.count = 0; + this.skip = 0; + this.take = 0; + this.page = 0; + this.max = 0; + this.visible = false; + this.format = format || '?page={0}'; + this.refresh(items, page, max); + this.transformName = transforms['pagination_default']; +} + +function Page(url, page, selected, enabled) { + this.url = url; + this.page = page; + this.selected = selected; + this.enabled = enabled; +} + +Page.prototype.html = function(body, cls) { + var classname = cls ? cls : ''; + if (this.selected) + classname += (classname ? ' ' : '') + 'selected'; + return '' + (body || this.page) + ''; +}; + +/** + * STATIC: Create transformation + * @param {String} name + * @param {Function(pagination)} fn + * @param {Boolean} isDefault Default transformation for all paginations. + */ +Pagination.addTransform = function(name, fn, isDefault) { + transforms['pagination'][name] = fn; + isDefault && Pagination.setDefaultTransform(name); +}; + +/** + * STATIC: Create transformation + * @param {String} name + * @param {Function(pagination)} fn + */ +Pagination.setDefaultTransform = function(name) { + if (name) + transforms['pagination_default'] = name; + else + delete transforms['pagination_default']; +}; + +/** + * STATIC: Remove transformation + * @param {String} name + */ +Pagination.removeTransform = function(name) { + delete transforms['pagination'][name]; +}; + +/** + * Refresh pagination + * @param {Number} items Count of items. + * @param {Number} page Current page. + * @param {Number} max Max items on page. + * @return {Pagination} + */ +Pagination.prototype.refresh = function(items, page, max) { + + this.page = Math.max(1, +page) - 1; + + if (this.page <= 0) + this.page = 0; + + this.items = Math.max(0, +items); + this.max = Math.max(1, +max); + this.skip = this.page * this.max; + this.count = Math.ceil(this.items / this.max); + this.take = Math.min(this.max, (this.items - this.skip)); + + this.lastPage = this.count; + this.firstPage = 1; + this.prevPage = this.page ? this.page : 1; + this.nextPage = this.page + 2 < this.count - 1 ? this.page + 2 : this.count; + + this.isPrev = this.page > 0; + this.isNext = this.page < this.count - 1; + + this.isFirst = this.page === 0; + this.isLast = this.page === this.count - 1; + + this.visible = this.count > 1; + this.page++; + + return this; +}; + +/** + * Set transformation for current Pagination + * @param {String} name + * @return {Pagination} + */ +Pagination.prototype.setTransform = function(name) { + this._transform = name; + return this; +}; + +/** + * Execute a transform + * @private + * @param {String} name A transformation name. + * @param {Object} argument1 Optional. + * @param {Object} argument2 Optional. + * @param {Object} argument3 Optional. + * @param {Object} argument4 Optional. + * @param {Object} argument..n Optional. + * @return {Object} + */ +Pagination.prototype.transform = function(name) { + + var transformName = name || this.transformName; + if (!transformName) + throw new Error('A transformation of Pagination not found.'); + + var current = transforms['pagination'][transformName]; + if (!current) + return this.render(); + + var param = []; + for (var i = 1; i < arguments.length; i++) + param.push(arguments[i]); + + return current.apply(this, param); +}; + +/** + * Get a previous page + * @param {String} format Custom format (optional). + * @return {Object} Example: { url: String, page: Number, selected: Boolean } + */ +Pagination.prototype.prev = function(format) { + var page = 0; + + format = format || this.format; + + if (this.isPrev) + page = this.page - 1; + else + page = this.count; + + return new Page(format.format(page, this.items, this.count), page, false, this.isPrev); +}; + +/** + * Get a next page + * @param {String} format Custom format (optional). + * @return {Object} Example: { url: String, page: Number, selected: Boolean } + */ +Pagination.prototype.next = function(format) { + var page = 0; + + format = format || this.format; + + if (this.isNext) + page = this.page + 1; + else + page = 1; + + return new Page(format.format(page, this.items, this.count), page, false, this.isNext); +}; + +/** + * Get a last page + * @param {String} format Custom format (optional). + * @return {Object} Example: { url: String, page: Number, selected: Boolean } + */ +Pagination.prototype.last = function(format) { + var page = this.count; + format = format || this.format; + return new Page(format.format(page, this.items, this.count), page, false, this.count > 0); +}; + +/** + * Get a first page + * @param {String} format Custom format (optional). + * @return {Object} Example: { url: String, page: Number, selected: Boolean } + */ +Pagination.prototype.first = function(format) { + var page = 1; + format = format || this.format; + return new Page(format.format(page, this.items, this.count), page, false, this.count > 0); +}; + +/** + * Create a pagination object + * @param {Number} max Max pages in collection (optional). + * @param {String} format Custom format (optional). + * @return {Object Array} Example: [{ url: String, page: Number, selected: Boolean }] + */ +Pagination.prototype.prepare = function(max, format, type) { + + var self = this; + + if (self.transformName) + return transforms['pagination'][self.transformName].apply(self, arguments); + + var builder = []; + format = format || self.format; + + if (typeof(max) === 'string') { + var tmp = format; + format = max; + max = tmp; + } + + var isHTML = type === 'html'; + + if (max == null) { + for (var i = 1; i < self.count + 1; i++) { + var page = new Page(format.format(i, self.items, self.count), i, i === self.page, true); + builder.push(isHTML ? page.html() : page); + } + return builder; + } + + var half = Math.floor(max / 2); + var pages = self.count; + + var pageFrom = self.page - half; + var pageTo = self.page + half; + var plus = 0; + + if (pageFrom <= 0) { + plus = Math.abs(pageFrom); + pageFrom = 1; + pageTo += plus; + } + + if (pageTo >= pages) { + pageTo = pages; + pageFrom = pages - max; + if (pageFrom <= 0) + pageFrom = 1; + } + + for (var i = pageFrom; i < pageTo + 1; i++) { + var page = new Page(format.format(i, self.items, self.count), i, i === self.page, true); + builder.push(isHTML ? page.html() : page); + } + + return builder; +}; + +Pagination.prototype.render = function(max, format) { + return this.prepare(max, format); +}; + +Pagination.prototype.html = function(max, format) { + return this.prepare(max, format, 'html').join(''); +}; + +Pagination.prototype.json = function(max, format) { + return JSON.stringify(this.prepare(max, format)); +}; + +UrlBuilder.make = function(fn) { + var b = new UrlBuilder(); + fn.call(b, b); + return b; +}; + +/** + * Add parameter + * @param {String} name + * @param {Object} value + * return {UrlBuilder} + */ +UrlBuilder.prototype.add = function(name, value) { + + if (typeof(name) !== 'object') { + this.builder[name] = value; + return this; + } + + var arr = Object.keys(name); + + for (var i = 0, length = arr.length; i < length; i++) + this.builder[arr[i]] = name[arr[i]]; + + return this; +}; + +/** + * Remove parameter + * @param {String} name + * @return {UrlBuilder} + */ +UrlBuilder.prototype.remove = function(name) { + delete this.builder[name]; + return this; +}; + +/** + * Read value + * @param {String} name + * @return {Object} + */ +UrlBuilder.prototype.read = function(name) { + return this.builder[name] || null; +}; + +/** + * Clear parameter collection + * @return {UrlBuilder} + */ +UrlBuilder.prototype.clear = function() { + this.builder = {}; + return this; +}; + +/** + * Create URL + * @return {String} + */ +UrlBuilder.prototype.toString = function(url, skipEmpty) { + + if (typeof(url) === 'boolean') { + var tmp = skipEmpty; + skipEmpty = url; + url = tmp; + } + + var self = this; + var builder = []; + + Object.keys(self.builder).forEach(function(o) { + + var value = self.builder[o]; + if (value == null) + value = ''; + else + value = value.toString(); + + if (skipEmpty && value === '') + return; + + builder.push(o + '=' + encodeURIComponent(value)); + }); + + if (typeof(url) === 'string') { + if (url[url.length - 1] !== '?') + url += '?'; + } else + url = ''; + + return url + builder.join('&'); +}; + +/** + * Has these parameters? + * @param {String Array} keys Keys. + * @return {Boolean} + */ +UrlBuilder.prototype.hasValue = function(keys) { + + if (keys === undefined) + return false; + + if (typeof(keys) === 'string') + keys = [keys]; + + for (var i = 0; i < keys.length; i++) { + var val = this.builder[keys[i]]; + if (val == null) + return false; + } + + return true; +}; + +/** + * Render parameters + * @param {String Array} keys Keys. + * @param {String} delimiter Delimiter (default &). + * @return {String} + */ +UrlBuilder.prototype.toOne = function(keys, delimiter) { + var self = this; + var builder = []; + keys.forEach(key => builder.push(self.builder[key] || '')); + return builder.join(delimiter || '&'); +}; + +function RESTBuilder(url) { + + this.$url = url; + this.$headers = { 'user-agent': 'Total.js/v' + F.version_header, accept: 'application/json, text/plain, text/plain, text/xml' }; + this.$method = 'get'; + this.$timeout = 10000; + this.$type = 0; // 0 = query, 1 = json, 2 = urlencode, 3 = raw + this.$schema; + this.$length = 0; + this.$transform = transforms['restbuilder_default']; + this.$files = null; + this.$persistentcookies = false; + + // this.$flags; + // this.$data = {}; + // this.$nodnscache = true; + // 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 && 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 + * @param {Function} fn + * @param {Boolean} isDefault Default transformation for all RESTBuilders. + */ +RESTBuilder.addTransform = function(name, fn, isDefault) { + transforms['restbuilder'][name] = fn; + isDefault && RESTBuilder.setDefaultTransform(name); +}; + +RESTBuilder.setDefaultTransform = function(name) { + if (name) + transforms['restbuilder_default'] = name; + else + delete transforms['restbuilder_default']; +}; + +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; +}; - return builder.join(delimiter || '&'); +RESTP.url = function(url) { + if (url === undefined) + return this.$url; + this.$url = url; + return this; }; +RESTP.file = function(name, filename, buffer) { + var obj = { name: name, filename: filename, buffer: buffer }; + if (this.$files) + this.$files.push(obj); + else + this.$files = [obj]; + return this; +}; + +RESTP.maketransform = function(obj, data) { + if (this.$transform) { + var fn = transforms['restbuilder'][this.$transform]; + return fn ? fn(obj, data) : obj; + } + return obj; +}; + +RESTP.timeout = function(number) { + this.$timeout = number; + return this; +}; + +RESTP.maxlength = function(number) { + this.$length = number; + this.$flags = null; + return this; +}; + +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; +}; + +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; +}; + +RESTP.noDnsCache = function() { + this.$nodnscache = true; + this.$flags = null; + return this; +}; + +RESTP.noCache = function() { + this.$nocache = true; + return this; +}; + +RESTP.make = function(fn) { + fn.call(this, this); + return this; +}; + +RESTP.xhr = function() { + this.$headers['X-Requested-With'] = 'XMLHttpRequest'; + return this; +}; + +RESTP.method = function(method, data) { + this.$method = method.charCodeAt(0) < 97 ? method.toLowerCase() : method; + this.$flags = null; + data && this.raw(data); + return this; +}; + +RESTP.referer = RESTP.referrer = function(value) { + this.$headers['Referer'] = value; + return this; +}; + +RESTP.origin = function(value) { + this.$headers['Origin'] = value; + return this; +}; + +RESTP.robot = function() { + if (this.$headers['User-Agent']) + this.$headers['User-Agent'] += ' Bot'; + else + this.$headers['User-Agent'] = 'Bot'; + return this; +}; + +RESTP.mobile = function() { + if (this.$headers['User-Agent']) + this.$headers['User-Agent'] += ' iPhone'; + else + this.$headers['User-Agent'] = 'iPhone'; + return this; +}; + +RESTP.put = RESTP.PUT = function(data) { + if (this.$method !== 'put') { + this.$flags = null; + this.$method = 'put'; + this.$type = 1; + } + data && this.raw(data); + return this; +}; + +RESTP.delete = RESTP.DELETE = function(data) { + if (this.$method !== 'delete') { + this.$flags = null; + this.$method = 'delete'; + this.$type = 1; + } + data && this.raw(data); + return this; +}; + +RESTP.get = RESTP.GET = function(data) { + if (this.$method !== 'get') { + this.$flags = null; + this.$method = 'get'; + } + data && this.raw(data); + return this; +}; + +RESTP.post = RESTP.POST = function(data) { + if (this.$method !== 'post') { + this.$flags = null; + this.$method = 'post'; + this.$type = 1; + } + data && this.raw(data); + return this; +}; + +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; + + data && this.raw(data); + this.$type = 1; + + if (this.$method === 'get') + this.$method = 'post'; + + return this; +}; + +RESTP.urlencoded = function(data) { + + if (this.$type !== 2) + this.$flags = null; + + if (this.$method === 'get') + this.$method = 'post'; + + this.$type = 2; + data && this.raw(data); + return this; +}; + +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.$flags = null; + this.$headers.Accept = type; + + return this; +}; + +RESTP.xml = function(data, replace) { + + if (this.$type !== 3) + this.$flags = null; + + if (this.$method === 'get') + this.$method = 'post'; + + this.$type = 3; + + if (replace) + this.$replace = true; + + data && this.raw(data); + return this; +}; + +RESTP.redirect = function(value) { + this.$redirect = value; + return this; +}; + +RESTP.raw = function(value) { + this.$data = value && value.$clean ? value.$clean() : value; + return this; +}; + +RESTP.plain = function() { + this.$plain = true; + return this; +}; + +RESTP.cook = function(value) { + this.$flags = null; + this.$persistentcookies = value !== false; + return this; +}; + +RESTP.cookies = function(obj) { + this.$cookies = obj; + return this; +}; + +RESTP.cookie = function(name, value) { + !this.$cookies && (this.$cookies = {}); + this.$cookies[name] = value; + return this; +}; + +RESTP.header = function(name, value) { + this.$headers[name] = value; + return this; +}; + +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; +}; + +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; + } else { + var arr = Object.keys(name); + for (var i = 0, length = arr.length; i < length; i++) + this.$data[arr[i]] = name[arr[i]]; + } + return this; +}; + +RESTP.rem = function(name) { + if (this.$data && this.$data[name]) + this.$data[name] = undefined; + return this; +}; + +RESTP.stream = function(callback) { + var self = this; + var flags = self.$flags ? self.$flags : [self.$method]; + + if (!self.$flags) { + !self.$nodnscache && flags.push('dnscache'); + self.$persistentcookies && flags.push('cookies'); + switch (self.$type) { + case 1: + flags.push('json'); + break; + case 3: + flags.push('xml'); + break; + } + self.$flags = flags; + } + + return U.download(self.$url, flags, self.$data, callback, self.$cookies, self.$headers, undefined, self.$timeout); +}; + +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; + + var self = this; + + 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; + + if (!self.$flags) { + + !self.$nodnscache && flags.push('dnscache'); + 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'); + } else { + switch (self.$type) { + case 1: + flags.push('json'); + break; + case 3: + flags.push('xml'); + break; + } + } + + self.$flags = flags; + } + + if (self.$cache_expire && !self.$nocache) { + key = '$rest_' + (self.$url + flags.join(',') + (self.$data ? Qs.stringify(self.$data) : '')).hash(); + var data = F.cache.read2(key); + if (data) { + var evt = new framework_utils.EventEmitter2(); + setImmediate(exec_removelisteners, evt); + callback(null, self.maketransform(this.$schema ? this.$schema.make(data.value) : data.value, data), data); + return evt; + } + } + + self.$callback_key = key; + return U.request(self.$url, flags, self.$data, exec_callback, self.$cookies, self.$headers, undefined, self.$timeout, self.$files, self); +}; + +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': + case 'application/xml': + output.value = response ? response.parseXML(self.$replace ? true : false) : {}; + break; + case 'application/x-www-form-urlencoded': + output.value = response ? F.onParseQuery(response) : {}; + break; + case 'application/json': + case 'text/json': + output.value = response ? response.parseJSON(true) : null; + break; + default: + output.value = response && response.isJSON() ? response.parseJSON(true) : null; + break; + } + } + + if (output.value == null) + output.value = EMPTYOBJECT; + + output.response = response; + output.status = status; + output.headers = headers; + output.hostname = hostname; + output.cache = false; + output.datetime = NOW; + + var val; + + if (self.$schema) { + + 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; + } + } + } + + 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; + } + } + + if (self.$convert && val && val !== EMPTYOBJECT) + val = CONVERT(val, self.$convert); + + callback(err, val, output); + output.cache = true; + } +} + +function exec_removelisteners(evt) { + evt.removeAllListeners(); +} + +function RESTBuilderResponse() {} + +RESTBuilderResponse.prototype.cookie = function(name) { + var self = this; + + if (self.cookies) + return $decodeURIComponent(self.cookies[name] || ''); + + var cookie = self.headers['cookie']; + if (!cookie) + return ''; + + self.cookies = {}; + + var arr = cookie.split(';'); + + for (var i = 0, length = arr.length; i < length; i++) { + var line = arr[i].trim(); + var index = line.indexOf('='); + if (index !== -1) + self.cookies[line.substring(0, index)] = line.substring(index + 1); + } + + return $decodeURIComponent(self.cookies[name] || ''); +}; + +// Handle errors of decodeURIComponent +function $decodeURIComponent(value) { + try + { + return decodeURIComponent(value); + } catch (e) { + return value; + } +} + +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]; + } 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); + } +}; + +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, 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; + } + + 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; + } + if (value !== NoOp) + callback(error.is ? error : null, value, param); + } + }); + } else { + error.push('Operation "{0}" not found.'.format(name)); + callback && callback(error, EMPTYOBJECT, param); + } +}; + +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 = { + + 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); +}; + +OperationOptionsProto.done = function(arg) { + var self = this; + return function(err, response) { + 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)); + } + }; +}; + +OperationOptionsProto.success = function(a, b) { + + if (a && b === undefined && typeof(a) !== 'boolean') { + b = a; + a = true; + } + + this.callback(SUCCESS(a === undefined ? true : a, b)); + return this; +}; + +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 // ====================================================== +exports.SchemaBuilder = SchemaBuilder; +exports.RESTBuilder = RESTBuilder; exports.ErrorBuilder = ErrorBuilder; exports.Pagination = Pagination; -exports.UrlBuilder = UrlBuilder; \ No newline at end of file +exports.Page = Page; +exports.UrlBuilder = UrlBuilder; +exports.SchemaOptions = SchemaOptions; +exports.OperationOptions = OperationOptions; +exports.RESTBuilderResponse = RESTBuilderResponse; +exports.AuthOptions = AuthOptions; +global.RESTBuilder = RESTBuilder; +global.RESTBuilderResponse = RESTBuilderResponse; +global.ErrorBuilder = ErrorBuilder; +global.Pagination = Pagination; +global.Page = Page; +global.UrlBuilder = global.URLBuilder = UrlBuilder; +global.SchemaBuilder = SchemaBuilder; +global.TaskBuilder = TaskBuilder; + +// Uninstall owners +exports.uninstall = function(owner) { + + if (!owner) + return; + + Object.keys(operations).forEach(function(key) { + if (operations[key].$owner === owner) + delete operations[key]; + }); + + exports.eachschema(function(group, name, schema) { + schema.owner === owner && schema.destroy(); + }); +}; + +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 ff98388a4..68a16e3a6 100755 --- a/changes.txt +++ b/changes.txt @@ -1,9 +1,2324 @@ -IN DEVELOPMENT ======= 1.5.2 (HOTFIX) +======= 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 ` - @{js('default.js')} - @{favicon('favicon.ico')} - - -
- @{body} -
- - - \ No newline at end of file diff --git a/empty-project/views/homepage.html b/empty-project/views/homepage.html deleted file mode 100644 index 925ef7c0e..000000000 --- a/empty-project/views/homepage.html +++ /dev/null @@ -1,5 +0,0 @@ -@{meta('Title', 'Description (optional)', 'Keywords (optional)')} - -
Hello World!
- -@{content('adv')} \ No newline at end of file diff --git a/empty-project/views/products/index.html b/empty-project/views/products/index.html deleted file mode 100644 index 134af0eab..000000000 --- a/empty-project/views/products/index.html +++ /dev/null @@ -1,6 +0,0 @@ -@{meta('Products', 'Description (optional)', 'Keywords (optional)')} - -
Welcome in products!
- -@{content('adv')} -@{template('products', [{ name: 'A', price: 23.32 }, { name: 'B', price: 32.10 }])} \ No newline at end of file diff --git a/error.html b/error.html new file mode 100644 index 000000000..71b8f4a95 --- /dev/null +++ b/error.html @@ -0,0 +1,48 @@ + + + + @{model.code}: @{model.status} + + + + + + + +
+
+
+ @{model.code} +
@{model.status}
+
+ @{if model.error}
@{model.error.replace(/\n/g, '
')}
@{fi} +
+
+
+ + + + + \ No newline at end of file diff --git a/examples/readme.md b/examples/readme.md deleted file mode 100644 index cfc1d968f..000000000 --- a/examples/readme.md +++ /dev/null @@ -1,5 +0,0 @@ -# total.js examples - -> Examples have a new repository: . - -__Please support total.js on GitHub with the star button.__ 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 new file mode 100644 index 000000000..1161565cc --- /dev/null +++ b/helpers/debug.js @@ -0,0 +1,17 @@ +// =================================================== +// FOR DEVELOPMENT +// Total.js - framework for Node.js platform +// https://www.totaljs.com +// =================================================== + +const options = {}; + +// options.ip = '127.0.0.1'; +// options.port = parseInt(process.argv[2]); +// options.config = { name: 'Total.js' }; +// 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/helpers/release.js b/helpers/release.js new file mode 100644 index 000000000..fe0397b64 --- /dev/null +++ b/helpers/release.js @@ -0,0 +1,15 @@ +// =================================================== +// FOR PRODUCTION +// Total.js - framework for Node.js platform +// https://www.totaljs.com +// =================================================== + +const options = {}; + +// options.ip = '127.0.0.1'; +// options.port = parseInt(process.argv[2]); +// options.config = { name: 'Total.js' }; +// options.sleep = 3000; + +require('total.js').http('release', options); +// require('total.js').cluster.http(5, 'release', options); \ No newline at end of file diff --git a/helpers/test.js b/helpers/test.js new file mode 100644 index 000000000..96fedb5f4 --- /dev/null +++ b/helpers/test.js @@ -0,0 +1,14 @@ +// =================================================== +// FOR UNIT-TESTING +// Total.js - framework for Node.js platform +// https://www.totaljs.com +// =================================================== + +const options = {}; + +// options.ip = '127.0.0.1'; +// options.port = parseInt(process.argv[2]); +// options.config = { name: 'Total.js' }; +// options.sleep = 3000; + +require('total.js').http('test', options); \ No newline at end of file diff --git a/image.js b/image.js index 18f605701..c77af5e65 100755 --- a/image.js +++ b/image.js @@ -1,48 +1,61 @@ +// 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 FrameworkImage - * @author Peter Širka - * @copyright Peter Širka 2012-2014 - * @version 1.5.0 + * @version 3.3.0 */ 'use strict'; -var child = require('child_process'); -var exec = child.exec; -var spawn = child.spawn; -var path = require('path'); -var utils = require('./utils'); - -// INTERNAL -var sof = { - 0xc0: true, - 0xc1: true, - 0xc2: true, - 0xc3: true, - 0xc5: true, - 0xc6: true, - 0xc7: true, - 0xc9: true, - 0xca: true, - 0xcb: true, - 0xcd: true, - 0xce: true, - 0xcf: true -}; +const sof = { 0xc0: true, 0xc1: true, 0xc2: true, 0xc3: true, 0xc5: true, 0xc6: true, 0xc7: true, 0xc9: true, 0xca: true, 0xcb: true, 0xcd: true, 0xce: true, 0xcf: true }; +const child = require('child_process'); +const exec = child.exec; +const spawn = child.spawn; +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 = {}; + +if (!global.framework_utils) + global.framework_utils = require('./utils'); function u16(buf, o) { - return buf[o] << 8 | buf[o + 1]; + return buf[o] << 8 | buf[o + 1]; } function u32(buf, o) { - return buf[o] << 24 | buf[o + 1] << 16 | buf[o + 2] << 8 | buf[o + 3]; + return buf[o] << 24 | buf[o + 1] << 16 | buf[o + 2] << 8 | buf[o + 3]; } exports.measureGIF = function(buffer) { - return { - width: buffer[6], - height: buffer[8] - }; + return { width: buffer.readInt16LE(6), height: buffer.readInt16LE(8) }; }; // MIT @@ -50,555 +63,701 @@ exports.measureGIF = function(buffer) { // visionmedia exports.measureJPG = function(buffer) { - var len = buffer.length; - var o = 0; - - var jpeg = 0xff == buffer[0] && 0xd8 == buffer[1]; - if (!jpeg) - return; - - o += 2; + var len = buffer.length; + var o = 0; - while (o < len) { - while (0xff != buffer[o]) o++; - while (0xff == buffer[o]) o++; + var jpeg = 0xff == buffer[0] && 0xd8 == buffer[1]; + 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); - 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; + return null; }; // MIT // Written by TJ Holowaychuk // visionmedia exports.measurePNG = function(buffer) { - return { - width: u32(buffer, 16), - height: u32(buffer, 16 + 4) - }; + return { width: u32(buffer, 16), height: u32(buffer, 16 + 4) }; +}; + +exports.measureSVG = function(buffer) { + + var match = buffer.toString('utf8').match(REGEXP_SVG); + if (!match) + return; + + var width = 0; + var height = 0; + + for (var i = 0, length = match.length; i < length; i++) { + var value = match[i]; + + if (width > 0 && height > 0) + break; + + if (!width && value.startsWith('width="')) + width = value.parseInt2(); + + if (!height && value.startsWith('height="')) + height = value.parseInt2(); + } + + return { width: width, height: height }; +}; + +exports.measure = function(type, buffer) { + switch (type) { + case '.jpg': + case '.jpeg': + case 'jpg': + case 'jpeg': + case 'image/jpeg': + return exports.measureJPG(buffer); + case '.gif': + case 'gif': + case 'image/gif': + return exports.measureGIF(buffer); + case '.png': + case 'png': + case 'image/png': + return exports.measurePNG(buffer); + case '.svg': + case 'svg': + case 'image/svg+xml': + return exports.measureSVG(buffer); + } +}; + +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.outputType = type === 'string' ? framework_utils.getExtension(filename) : 'jpg'; + this.islimit = false; + this.cmdarg = cmd || CONF.default_image_converter; +} + +var ImageProto = Image.prototype; + +ImageProto.clear = function() { + var self = this; + self.builder = []; + return self; }; -/* - Image class - @filename {String} - @useImageMagick {Boolean} :: default false -*/ -function Image(filename, useImageMagick) { +ImageProto.measure = function(callback) { - var type = typeof(filename); + var self = this; + var index = self.filename.lastIndexOf('.'); - this.builder = []; - this.filename = type === 'string' ? filename : null; - this.currentStream = type === 'object' ? filename : null; - this.isIM = useImageMagick || false; - this.outputType = type === 'string' ? path.extname(filename).substring(1) : 'jpg'; + if (!self.filename) { + callback(new Error('Measure does not support stream.')); + return; + } - if (!filename) - throw new Error('Image filename is undefined.'); -} + if (index === -1) { + callback(new Error('This type of file is not supported.')); + 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 }); + + stream.on('data', function(buffer) { + + switch (extension) { + case '.jpg': + callback(null, exports.measureJPG(buffer)); + return; + case '.gif': + callback(null, exports.measureGIF(buffer)); + return; + case '.png': + callback(null, exports.measurePNG(buffer)); + return; + } + + callback(new Error('This type of file is not supported.')); + }); -/* - Clear all filter - return {Image} -*/ -Image.prototype.clear = function() { - var self = this; - self.builder = []; - return self; + stream.on('error', callback); + return self; }; -Image.prototype.measure = function(callback) { +ImageProto.$$measure = function() { + var self = this; + return function(callback) { + self.measure(callback); + }; +}; + +/** + * Execute commands + * @param {String} filename + * @param {Function(err, filename)} callback Optional. + * @param {Function(stream)} writer A custom stream writer, optional. + * @return {Image} + */ +ImageProto.save = function(filename, callback, writer) { + + var self = this; + + if (typeof(filename) === 'function') { + callback = filename; + filename = null; + } + + !self.builder.length && self.minify(); + filename = filename || self.filename || ''; - var self = this; - var index = self.filename.lastIndexOf('.'); + var command = self.cmd(self.filename ? self.filename : '-', filename); - if (!self.filename) { - callback(new Error('Measure does not support stream.')); - return; - } + if (F.isWindows) + command = command.replace(REGEXP_PATH, '\\'); - if (index === -1) { - callback(new Error('This type of file is not supported.')); - return; - } + var cmd = exec(command, function(err) { - var extension = self.filename.substring(index).toLowerCase(); - var stream = require('fs').createReadStream(self.filename, { - start: 0, - end: extension === '.jpg' ? 40000 : 24 - }); + // clean up + cmd.kill(); + cmd = null; - stream.on('data', function(buffer) { + self.clear(); - switch (extension) { - case '.jpg': - callback(null, exports.measureJPG(buffer)); - return; - case '.gif': - callback(null, exports.measureGIF(buffer)); - return; - case '.png': - callback(null, exports.measurePNG(buffer)); - return; - } + if (!callback) + return; - callback(new Error('This type of file is not supported.')); - }); + if (err) { + callback(err, false); + return; + } - stream.on('error', callback); - return self; + var middleware = middlewares[self.outputType]; + if (!middleware) + return callback(null, true); + + F.stats.performance.open++; + var reader = Fs.createReadStream(filename); + var writer = Fs.createWriteStream(filename + '_'); + + reader.pipe(middleware()).pipe(writer); + writer.on('finish', () => Fs.rename(filename + '_', filename, () => callback(null, true))); + }); + + if (self.currentStream) { + if (self.currentStream instanceof Buffer) + cmd.stdin.end(self.currentStream); + else + self.currentStream.pipe(cmd.stdin); + } + + CLEANUP(cmd.stdin); + writer && writer(cmd.stdin); + return self; }; -/* - Execute all filters and save image - @filename {String} - @callback {Function} :: optional - return {Image} -*/ -Image.prototype.save = function(filename, callback) { +ImageProto.$$save = function(filename, writer) { + var self = this; + return function(callback) { + self.save(filename, callback, writer); + }; +}; - var self = this; +ImageProto.pipe = function(stream, type, options) { - if (typeof(filename) === 'function') { - callback = filename; - filename = null; - } + var self = this; - filename = filename || self.filename || ''; + if (typeof(type) === 'object') { + options = type; + type = null; + } - var command = self.cmd(self.filename === null ? '-' : self.filename, filename); + !self.builder.length && self.minify(); - if (self.builder.length === 0) { - if (callback) - callback(null, filename); - return; - } + if (!type || !SUPPORTEDIMAGES[type]) + type = self.outputType; - var cmd = exec(command, function(error, stdout, stderr) { + 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')); + cmd.on('error', stream.emit.bind(stream, 'error')); - self.clear(); - if (!callback) - return; + var middleware = middlewares[type]; + if (middleware) + cmd.stdout.pipe(middleware()).pipe(stream, options); + else + cmd.stdout.pipe(stream, options); - if (error) - callback(error, ''); - else - callback(null, filename); - }); + if (self.currentStream) { + if (self.currentStream instanceof Buffer) + cmd.stdin.end(self.currentStream); + else + self.currentStream.pipe(cmd.stdin); + } - if (self.currentStream) - self.currentStream.pipe(cmd.stdin); + return self; +}; - return self; +/** + * Create a stream + * @param {String} type File type (png, jpg, gif) + * @param {Function(stream)} writer A custom stream writer. + * @return {ReadStream} + */ +ImageProto.stream = function(type, writer) { + + var self = this; + + !self.builder.length && self.minify(); + + 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); + if (self.currentStream) { + if (self.currentStream instanceof Buffer) + cmd.stdin.end(self.currentStream); + else + self.currentStream.pipe(cmd.stdin); + } + + writer && writer(cmd.stdin); + var middleware = middlewares[type]; + return middleware ? cmd.stdout.pipe(middleware()) : cmd.stdout; +}; + +ImageProto.cmd = function(filenameFrom, filenameTo) { + + var self = this; + var cmd = ''; + + if (!self.islimit) { + var tmp = CONF.default_image_consumption; + if (tmp) { + self.limit('memory', (1500 / 100) * tmp); + self.limit('map', (3000 / 100) * tmp); + } + } + + self.builder.sort(sort); + + var length = self.builder.length; + for (var i = 0; i < length; i++) + cmd += (cmd ? ' ' : '') + self.builder[i].cmd; + + return CMD_CONVERT2[self.cmdarg] + wrap(filenameFrom, true) + ' ' + cmd + wrap(filenameTo, true); }; -/* - Pipe stream - @stream {Stream} - @type {String} :: optional, image type (png, jpg, gif) - @options {Object} :: Stream object - return {Image} -*/ -Image.prototype.pipe = function(stream, type, options) { +function sort(a, b) { + return a.priority > b.priority ? 1 : -1; +} - var self = this; +ImageProto.arg = function(first, last) { - if (typeof(type) === 'object') { - options = type; - type = null; - } + var self = this; + var arr = []; - if (self.builder.length === 0) - return; + if (self.cmdarg === 'gm') + arr.push('convert'); - if (typeof(type) === 'undefined' || type === null) - type = self.outputType; + first && arr.push(first); - var cmd = spawn(self.isIM ? 'convert' : 'gm', self.arg(self.filename === null ? '-' : self.filename, (type ? type + ':' : '') + '-')); + if (!self.islimit) { + var tmp = CONF.default_image_consumption; + if (tmp) { + self.limit('memory', (1500 / 100) * tmp); + self.limit('map', (3000 / 100) * tmp); + } + } - 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')); - cmd.on('error', stream.emit.bind(stream, 'error')); - cmd.stdout.pipe(stream, options); + self.builder.sort(sort); - if (self.currentStream) - self.currentStream.pipe(cmd.stdin); + var length = self.builder.length; - return self; + for (var i = 0; i < length; i++) { + var o = self.builder[i]; + var index = o.cmd.indexOf(' '); + if (index === -1) + arr.push(o.cmd); + else { + arr.push(o.cmd.substring(0, index)); + arr.push(o.cmd.substring(index + 1).replace(/"/g, '')); + } + } + + last && arr.push(last); + return arr; }; -/* - Create a stream - @stream {Stream} - @type {String} :: optional, image type (png, jpg, gif) - return {Image} -*/ -Image.prototype.stream = function(type) { +ImageProto.identify = function(callback) { + var self = this; + F.stats.performance.open++; + exec((self.cmdarg === 'gm' ? 'gm ' : '') + 'identify' + wrap(self.filename, true), function(err, stdout) { + + if (err) { + callback(err, null); + return; + } - var self = this; + var arr = stdout.split(' '); + var size = arr[2].split('x'); + var obj = { type: arr[1], width: framework_utils.parseInt(size[0]), height: framework_utils.parseInt(size[1]) }; + callback(null, obj); + }); - if (self.builder.length === 0) - return; + return self; +}; - if (typeof(type) === 'undefined' || type === null) - type = self.outputType; +ImageProto.$$identify = function() { + var self = this; + return function(callback) { + self.identify(callback); + }; +}; - var cmd = spawn(self.isIM ? 'convert' : 'gm', self.arg(self.filename === null ? '-' : self.filename, (type ? type + ':' : '') + '-')); +ImageProto.push = function(key, value, priority, encode) { + var self = this; + var cmd = key; - if (self.currentStream) - self.currentStream.pipe(cmd.stdin); + if (value != null) { + if (encode && typeof(value) === 'string') + cmd += ' ' + D + value.replace(REGEXP_ESCAPE, '') + D; + else + cmd += ' ' + value; + } - return cmd.stdout; + var obj = CACHE[cmd]; + if (obj) { + obj.priority = priority; + self.builder.push(obj); + } else { + CACHE[cmd] = { cmd: cmd, priority: priority }; + self.builder.push(CACHE[cmd]); + } + + return self; }; -/* - Internal function - @filenameFrom {String} - @filenameTo {String} - return {String} -*/ -Image.prototype.cmd = function(filenameFrom, filenameTo) { +ImageProto.output = function(type) { + var self = this; + if (type[0] === '.') + type = type.substring(1); + self.outputType = type; + return self; +}; - var self = this; - var cmd = ''; +ImageProto.resize = function(w, h, options) { + options = options || ''; - self.builder.sort(function(a, b) { - if (a.priority > b.priority) - return 1; - else - return -1; - }); + var self = this; + var size = ''; - var length = self.builder.length; + if (w && h) + size = w + 'x' + h; + else if (w && !h) + size = w + 'x'; + else if (!w && h) + size = 'x' + h; - for (var i = 0; i < length; i++) - cmd += (cmd.length > 0 ? ' ' : '') + self.builder[i].cmd; + return self.push('-resize', size + options, 1, true); +}; - return (self.isIM ? 'convert' : 'gm -convert') + ' "' + filenameFrom + '"' + ' ' + cmd + ' "' + filenameTo + '"'; +ImageProto.thumbnail = function(w, h, options) { + options = options || ''; + + var self = this; + var size = ''; + + if (w && h) + size = w + 'x' + h; + else if (w && !h) + size = w; + else if (!w && h) + size = 'x' + h; + + return self.push('-thumbnail', size + options, 1, true); }; -/* - Internal function - @filenameFrom {String} - @filenameTo {String} - return {String} -*/ -Image.prototype.arg = function(first, last) { +ImageProto.geometry = function(w, h, options) { + options = options || ''; - var self = this; - var arr = []; + var self = this; + var size = ''; - if (!self.isIM) - arr.push('-convert'); + if (w && h) + size = w + 'x' + h; + else if (w && !h) + size = w; + else if (!w && h) + size = 'x' + h; - if (first) - arr.push(first); + return self.push('-geometry', size + options, 1, true); +}; - self.builder.sort(function(a, b) { - if (a.priority > b.priority) - return 1; - else - return -1; - }); - var length = self.builder.length; +ImageProto.filter = function(type) { + return this.push('-filter', type, 1, true); +}; - for (var i = 0; i < length; i++) { - var o = self.builder[i]; - var index = o.cmd.indexOf(' '); - if (index === -1) - arr.push(o.cmd); - else { - arr.push(o.cmd.substring(0, index)); - arr.push(o.cmd.substring(index + 1).replace(/\"/g, '')); - } - } +ImageProto.trim = function() { + return this.push('-trim +repage', 1); +}; - if (last) - arr.push(last); +ImageProto.limit = function(type, value) { + this.islimit = true; + return this.push('-limit', type + ' ' + value, 1); +}; + +ImageProto.extent = function(w, h, x, y) { + + var self = this; + var size = ''; + + if (w && h) + size = w + 'x' + h; + else if (w && !h) + size = w; + 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); +}; + +/** + * Resize picture to miniature (full picture) + * @param {Number} w + * @param {Number} h + * @param {String} color Optional, background color. + * @param {String} filter Optional, resize filter (default: Box) + * @return {Image} + */ +ImageProto.miniature = function(w, h, color, filter) { + return this.filter(filter || 'Hamming').thumbnail(w, h).background(color ? color : 'white').align('center').extent(w, h); +}; + +/** + * Resize picture to center + * @param {Number} w + * @param {Number} h + * @param {String} color Optional, background color. + * @return {Image} + */ +ImageProto.resizeCenter = ImageProto.resize_center = function(w, h, color) { + return this.resize(w, h, '^').background(color ? color : 'white').align('center').crop(w, h); +}; + +/** + * Resize picture to align + * @param {Number} w + * @param {Number} h + * @param {String} align (top, center, bottom) + * @param {String} color Optional, background color. + * @return {Image} + */ +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); +}; + +ImageProto.scale = function(w, h, options) { + options = options || ''; + + var self = this; + var size = ''; + + if (w && h) + size = w + 'x' + h; + else if (w && !h) + size = w; + else if (!w && h) + size = 'x' + h; + + return self.push('-scale', size + options, 1, true); +}; + +ImageProto.crop = function(w, h, x, y) { + return this.push('-crop', w + 'x' + h + '+' + (x || 0) + '+' + (y || 0), 4, true); +}; + +ImageProto.quality = function(percentage) { + return this.push('-quality', percentage || 80, 5, true); +}; + +ImageProto.align = function(type) { + + var output; + + switch (type) { + case 'left top': + case 'top left': + output = 'NorthWest'; + break; + case 'left bottom': + case 'bottom left': + output = 'SouthWest'; + break; + case 'right top': + case 'top right': + output = 'NorthEast'; + break; + case 'right bottom': + case 'bottom right': + output = 'SouthEast'; + break; + case 'left center': + case 'center left': + case 'left': + output = 'West'; + break; + case 'right center': + case 'center right': + case 'right': + output = 'East'; + break; + case 'bottom center': + case 'center bottom': + case 'bottom': + output = 'South'; + break; + case 'top center': + case 'center top': + case 'top': + output = 'North'; + break; + case 'center center': + case 'center': + case 'middle': + output = 'Center'; + break; + default: + output = type; + break; + } + + output && this.push('-gravity', output, 3, true); + return this; +}; + +ImageProto.gravity = function(type) { + return this.align(type); +}; - return arr; +ImageProto.blur = function(radius) { + return this.push('-blur', radius, 10, true); }; -/* - Identify image - cb {Function} :: function(err, info) {} :: info.type {String} == 'JPEG' | 'PNG', info.width {Number}, info.height {Number} - return {Image} -*/ -Image.prototype.identify = function(cb) { - var self = this; +ImageProto.normalize = function() { + return this.push('-normalize', null, 10); +}; - exec((self.isIM ? 'identify' : 'gm identify') + ' "' + self.fileName + '"', function(error, stdout, stderr) { +ImageProto.rotate = function(deg) { + return this.push('-rotate', deg || 0, 8, true); +}; - if (error) { - cb(error, null); - return; - } +ImageProto.flip = function() { + return this.push('-flip', null, 10); +}; - var arr = stdout.split(' '); - var size = arr[2].split('x'); - var obj = { - type: arr[1], - width: utils.parseInt(size[0]), - height: utils.parseInt(size[1]) - }; - - cb(null, obj); - }); - - return self; -}; - -/* - Append filter to filter list - @key {String} - @value {String} - @priority {Number} - return {Image} -*/ -Image.prototype.push = function(key, value, priority) { - var self = this; - self.builder.push({ - cmd: key + (value ? ' "' + value + '"' : ''), - priority: priority - }); - return self; -}; - -Image.prototype.output = function(type) { - var self = this; - - if (type[0] === '.') - type = type.substring(1); - - self.outputType = type; - return self; -}; - -/* - @w {Number} - @h {Number} - @options {String} - http://www.graphicsmagick.org/GraphicsMagick.html#details-resize -*/ -Image.prototype.resize = function(w, h, options) { - options = options || ''; - - var self = this; - var size = ''; - - if (w && h) - size = w + 'x' + h; - else if (w && !h) - size = w; - else if (!w && h) - size = 'x' + h; - - return self.push('-resize', size + options, 1); -}; - -/* - @w {Number} - @h {Number} -*/ -Image.prototype.resizeCenter = function(w, h) { - return this.resize(w, h, '^').align('center').crop(w, h); -}; - -/* - @w {Number} - @h {Number} - @options {String} - http://www.graphicsmagick.org/GraphicsMagick.html#details-scale -*/ -Image.prototype.scale = function(w, h, options) { - options = options || ''; - - var self = this; - var size = ''; - - if (w && h) - size = w + 'x' + h; - else if (w && !h) - size = w; - else if (!w && h) - size = 'x' + h; - - return self.push('-scale', size + options, 1); -}; - -/* - @w {Number} - @h {Number} - @x {Number} - @y {Number} - http://www.graphicsmagick.org/GraphicsMagick.html#details-crop -*/ -Image.prototype.crop = function(w, h, x, y) { - return this.push('-crop', w + 'x' + h + '+' + (x || 0) + '+' + (y || 0), 4); -}; - -/* - @percentage {Number} - http://www.graphicsmagick.org/GraphicsMagick.html#details-quality -*/ -Image.prototype.quality = function(percentage) { - return this.push('-quality', percentage || 80, 5); -}; - -/* - @type {String} -*/ -Image.prototype.align = function(type) { - - var output = ''; - - switch (type.toLowerCase().replace('-', '')) { - case 'left top': - case 'top left': - output = 'NorthWest'; - break; - case 'left bottom': - case 'bottom left': - output = 'SouthWest'; - break; - case 'right top': - case 'top right': - output = 'NorthEast'; - break; - case 'right bottom': - case 'bottom right': - output = 'SouthEast'; - break; - case 'left center': - case 'center left': - case 'left': - output = 'West'; - break; - case 'right center': - case 'center right': - case 'right': - output = 'East'; - break; - case 'bottom center': - case 'center bottom': - case 'bottom': - output = 'South'; - break; - case 'top center': - case 'center top': - case 'top': - output = 'North'; - break; - case 'center center': - case 'center': - output = 'Center'; - break; - default: - output = type; - break; - } - - return this.push('-gravity', output, 3); -}; - -/* - @type {String} -*/ -Image.prototype.gravity = function(type) { - return this.align(type); -}; - -/* - @radius {Number} - http://www.graphicsmagick.org/GraphicsMagick.html#details-blur -*/ -Image.prototype.blur = function(radius) { - return this.push('-blur', radius, 10); +ImageProto.flop = function() { + return this.push('-flop', null, 10); }; -Image.prototype.normalize = function() { - return this.push('-normalize', null, 10); +ImageProto.define = function(value) { + return this.push('-define', value, 10, true); }; -/* - @deg {Number} - http://www.graphicsmagick.org/GraphicsMagick.html#details-rotate -*/ -Image.prototype.rotate = function(deg) { - return this.push('-rotate', deg || 0, 8); -}; - -// http://www.graphicsmagick.org/GraphicsMagick.html#details-flip -Image.prototype.flip = function() { - return this.push('-flip', null, 10); +ImageProto.minify = function() { + return this.push('+profile', '*', null, 10, true); }; -// http://www.graphicsmagick.org/GraphicsMagick.html#details-flop -Image.prototype.flop = function() { - return this.push('-flop', null, 10); +ImageProto.grayscale = function() { + return this.push('-colorspace', 'Gray', 10, true); }; - -Image.prototype.minify = function() { - return this.push('+profile', '*'); + +ImageProto.bitdepth = function(value) { + return this.push('-depth', value, 10, true); }; -Image.prototype.grayscale = function() { - return this.push('-colorspace', 'Gray', 10); +ImageProto.colors = function(value) { + return this.push('-colors', value, 10, true); }; - -Image.prototype.bitdepth = function(value) { - return this.push('-depth', value, 10); + +ImageProto.background = function(color) { + return this.push('-background', color, 2, true).push('-extent 0x0', null, 2); }; -Image.prototype.colors = function(value) { - return this.push('-colors', value, 10); -}; - -/* - @color {String} -*/ -Image.prototype.background = function(color) { - return this.push('-background', color, 2); +ImageProto.fill = function(color) { + return this.push('-fill', color, 2, true); }; -Image.prototype.sepia = function(percentage) { - return this.push('-modulate', '115,0,100', 4).push('-colorize', '7,21,50', 5) +ImageProto.sepia = function() { + return this.push('-modulate', '115,0,100', 4).push('-colorize', '7,21,50', 5); }; -/* - @cmd {String} - @priority {Number} -*/ -Image.prototype.command = function(key, value, priority) { - return this.push(cmd, null, priority || 10); +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); }; +ImageProto.make = function(fn) { + fn.call(this, this); + return this; +}; + +ImageProto.command = function(key, value, priority, esc) { + + if (priority === true) { + priority = 0; + esc = true; + } + + return this.push(key, value, priority || 10, esc); +}; + +function wrap(command, empty) { + return (empty ? ' ' : '') + (command === '-' ? command : (D + command.replace(REGEXP_ESCAPE, '') + D)); +} + exports.Image = Image; exports.Picture = Image; -/* - Init image class - @filename {String} - @imageMagick {Boolean} :: default false -*/ -exports.init = function(filename, imageMagick) { - return new Image(filename, imageMagick); +exports.init = function(filename, cmd, width, height) { + return new Image(filename, cmd, width, height); +}; + +exports.load = function(filename, cmd, width, height) { + return new Image(filename, cmd, width, height); +}; + +exports.middleware = function(type, fn) { + if (type[0] === '.') + type = type.substring(1); + middlewares[type] = fn; +}; + +// Clears cache with commands +exports.clear = function() { + CACHE = {}; }; -exports.load = function(filename, imageMagick) { - return new Image(filename, imageMagick); -}; \ No newline at end of file +global.Image = exports; diff --git a/index.js b/index.js index 0b1c00e96..46586e267 100755 --- a/index.js +++ b/index.js @@ -1,10791 +1,19815 @@ +// 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 +// "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 Framework - * @author Peter Širka - * @copyright Peter Širka 2012-2014 - * @version 1.5.0 + * @version 3.4.13 */ 'use strict'; -var qs = require('querystring'); -var os = require('os'); - -var fs = require('fs'); -var zlib = require('zlib'); -var path = require('path'); -var crypto = require('crypto'); -var parser = require('url'); -var events = require('events'); -var sys = require('sys'); -var internal = require('./internal'); -var http = require('http'); -var directory = process.cwd(); -var child = require('child_process'); - -var ENCODING = 'utf8'; -var UNDEFINED = 'undefined'; -var STRING = 'string'; -var FUNCTION = 'function'; -var NUMBER = 'number'; -var OBJECT = 'object'; -var BOOLEAN = 'boolean'; - -var REQUEST_COMPRESS_EXTENSION = ['js', 'css', 'txt']; -var EXTENSION_JS = '.js'; -var EXTENSION_COFFEE = '.coffee'; -var RESPONSE_HEADER_CACHECONTROL = 'Cache-Control'; -var RESPONSE_HEADER_CONTENTTYPE = 'Content-Type'; -var CONTENTTYPE_TEXTPLAIN = 'text/plain'; -var CONTENTTYPE_TEXTHTML = 'text/html'; -var REQUEST_COMPRESS_CONTENTTYPE = [CONTENTTYPE_TEXTPLAIN, 'text/javascript', 'text/css', 'application/x-javascript', CONTENTTYPE_TEXTHTML]; -var _controller = ''; -var _test = ''; +const Qs = require('querystring'); +const Os = require('os'); +const Fs = require('fs'); +const Zlib = require('zlib'); +const Path = require('path'); +const Crypto = require('crypto'); +const Parser = require('url'); +const Child = require('child_process'); +const Util = require('util'); +const http = require('http'); + +const ENCODING = 'utf8'; +const HEADER_CACHE = 'Cache-Control'; +const HEADER_TYPE = 'Content-Type'; +const HEADER_LENGTH = 'Content-Length'; +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|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; +const REG_SANITIZE_BACKSLASH = /\/\//g; +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(@[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 REG_ENCODEDSPACE = /\+/g; +const FLAGS_INSTALL = ['get']; +const FLAGS_DOWNLOAD = ['get', 'dnscache']; +const QUERYPARSEROPTIONS = { maxKeys: 33 }; +const EMPTYARRAY = []; +const EMPTYOBJECT = {}; +const EMPTYREQUEST = { uri: {} }; +const SINGLETONS = {}; +const REPOSITORY_HEAD = '$head'; +const REPOSITORY_META_TITLE = '$title'; +const REPOSITORY_META_DESCRIPTION = '$description'; +const REPOSITORY_META_KEYWORDS = '$keywords'; +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', 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); +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.Builders = global.builders = require('./builders'); -var utils = global.Utils = global.utils = require('./utils'); -global.Mail = global.MAIL = require('./mail'); +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 }; +var HEADERS = {}; +var SUCCESSHELPER = { success: true }; + +// Cached headers for repeated usage +HEADERS.responseCode = {}; +HEADERS.responseCode[HEADER_TYPE] = CT_TEXT; +HEADERS.redirect = {}; +HEADERS.redirect[HEADER_TYPE] = CT_HTML + '; charset=utf-8'; +HEADERS.redirect[HEADER_LENGTH] = '0'; +HEADERS.sse = {}; +HEADERS.sse[HEADER_CACHE] = 'private, no-cache, no-store, max-age=0'; +HEADERS.sse['Pragma'] = 'no-cache'; +HEADERS.sse['Expires'] = '-1'; +HEADERS.sse[HEADER_TYPE] = 'text/event-stream'; +HEADERS.sse['X-Powered-By'] = 'Total.js'; +HEADERS.file_lastmodified = {}; +HEADERS.file_lastmodified['Access-Control-Allow-Origin'] = '*'; +HEADERS.file_lastmodified[HEADER_CACHE] = 'public, max-age=11111111'; +HEADERS.file_lastmodified['X-Powered-By'] = 'Total.js'; +HEADERS.file_release_compress = {}; +HEADERS.file_release_compress[HEADER_CACHE] = 'public, max-age=11111111'; +HEADERS.file_release_compress['Vary'] = 'Accept-Encoding'; +HEADERS.file_release_compress['Access-Control-Allow-Origin'] = '*'; +HEADERS.file_release_compress['Last-Modified'] = 'Mon, 01 Jan 2001 08:00:00 GMT'; +HEADERS.file_release_compress['Content-Encoding'] = 'gzip'; +HEADERS.file_release_compress['X-Powered-By'] = 'Total.js'; +HEADERS.file_release_compress_range = {}; +HEADERS.file_release_compress_range['Accept-Ranges'] = 'bytes'; +HEADERS.file_release_compress_range[HEADER_CACHE] = 'public, max-age=11111111'; +HEADERS.file_release_compress_range['Vary'] = 'Accept-Encoding'; +HEADERS.file_release_compress_range['Access-Control-Allow-Origin'] = '*'; +HEADERS.file_release_compress_range['Last-Modified'] = 'Mon, 01 Jan 2001 08:00:00 GMT'; +HEADERS.file_release_compress_range['Content-Encoding'] = 'gzip'; +HEADERS.file_release_compress_range[HEADER_LENGTH] = '0'; +HEADERS.file_release_compress_range['Content-Range'] = ''; +HEADERS.file_release_compress_range['X-Powered-By'] = 'Total.js'; +HEADERS.file_release = {}; +HEADERS.file_release[HEADER_CACHE] = 'public, max-age=11111111'; +HEADERS.file_release['Vary'] = 'Accept-Encoding'; +HEADERS.file_release['Access-Control-Allow-Origin'] = '*'; +HEADERS.file_release['Last-Modified'] = 'Mon, 01 Jan 2001 08:00:00 GMT'; +HEADERS.file_release['X-Powered-By'] = 'Total.js'; +HEADERS.file_release_range = {}; +HEADERS.file_release_range['Accept-Ranges'] = 'bytes'; +HEADERS.file_release_range[HEADER_CACHE] = 'public, max-age=11111111'; +HEADERS.file_release_range['Vary'] = 'Accept-Encoding'; +HEADERS.file_release_range['Access-Control-Allow-Origin'] = '*'; +HEADERS.file_release_range['Last-Modified'] = 'Mon, 01 Jan 2001 08:00:00 GMT'; +HEADERS.file_release_range[HEADER_LENGTH] = '0'; +HEADERS.file_release_range['Content-Range'] = ''; +HEADERS.file_release_range['X-Powered-By'] = 'Total.js'; +HEADERS.file_debug_compress = {}; +HEADERS.file_debug_compress[HEADER_CACHE] = 'private, no-cache, no-store, max-age=0'; +HEADERS.file_debug_compress['Vary'] = 'Accept-Encoding'; +HEADERS.file_debug_compress['Access-Control-Allow-Origin'] = '*'; +HEADERS.file_debug_compress['Pragma'] = 'no-cache'; +HEADERS.file_debug_compress['Expires'] = '-1'; +HEADERS.file_debug_compress['Content-Encoding'] = 'gzip'; +HEADERS.file_debug_compress['X-Powered-By'] = 'Total.js'; +HEADERS.file_debug_compress_range = {}; +HEADERS.file_debug_compress_range['Accept-Ranges'] = 'bytes'; +HEADERS.file_debug_compress_range[HEADER_CACHE] = 'private, no-cache, no-store, max-age=0'; +HEADERS.file_debug_compress_range['Vary'] = 'Accept-Encoding'; +HEADERS.file_debug_compress_range['Access-Control-Allow-Origin'] = '*'; +HEADERS.file_debug_compress_range['Content-Encoding'] = 'gzip'; +HEADERS.file_debug_compress_range['Pragma'] = 'no-cache'; +HEADERS.file_debug_compress_range['Expires'] = '-1'; +HEADERS.file_debug_compress_range[HEADER_LENGTH] = '0'; +HEADERS.file_debug_compress_range['Content-Range'] = ''; +HEADERS.file_debug_compress_range['X-Powered-By'] = 'Total.js'; +HEADERS.file_debug = {}; +HEADERS.file_debug[HEADER_CACHE] = 'private, no-cache, no-store, max-age=0'; +HEADERS.file_debug['Vary'] = 'Accept-Encoding'; +HEADERS.file_debug['Pragma'] = 'no-cache'; +HEADERS.file_debug['Expires'] = '-1'; +HEADERS.file_debug['Access-Control-Allow-Origin'] = '*'; +HEADERS.file_debug['X-Powered-By'] = 'Total.js'; +HEADERS.file_debug_range = {}; +HEADERS.file_debug_range['Accept-Ranges'] = 'bytes'; +HEADERS.file_debug_range[HEADER_CACHE] = 'private, no-cache, no-store, max-age=0'; +HEADERS.file_debug_range['Vary'] = 'Accept-Encoding'; +HEADERS.file_debug_range['Access-Control-Allow-Origin'] = '*'; +HEADERS.file_debug_range['Pragma'] = 'no-cache'; +HEADERS.file_debug_range['Expires'] = '-1'; +HEADERS.file_debug_range[HEADER_LENGTH] = '0'; +HEADERS.file_debug_range['Content-Range'] = ''; +HEADERS.file_debug_range['X-Powered-By'] = 'Total.js'; +HEADERS.content_mobile_release = {}; +HEADERS.content_mobile_release[HEADER_CACHE] = 'private, no-cache, no-store, max-age=0'; +HEADERS.content_mobile_release['Vary'] = 'Accept-Encoding, User-Agent'; +HEADERS.content_mobile_release['Content-Encoding'] = 'gzip'; +HEADERS.content_mobile_release['Expires'] = '-1'; +HEADERS.content_mobile_release['X-Powered-By'] = 'Total.js'; +HEADERS.content_mobile = {}; +HEADERS.content_mobile[HEADER_CACHE] = 'private, no-cache, no-store, max-age=0'; +HEADERS.content_mobile['Vary'] = 'Accept-Encoding, User-Agent'; +HEADERS.content_mobile['Expires'] = '-1'; +HEADERS.content_mobile['X-Powered-By'] = 'Total.js'; +HEADERS.content_compress = {}; +HEADERS.content_compress[HEADER_CACHE] = 'private, no-cache, no-store, max-age=0'; +HEADERS.content_compress['Vary'] = 'Accept-Encoding'; +HEADERS.content_compress['Content-Encoding'] = 'gzip'; +HEADERS.content_compress['Expires'] = '-1'; +HEADERS.content_compress['X-Powered-By'] = 'Total.js'; +HEADERS.content = {}; +HEADERS.content[HEADER_CACHE] = 'private, no-cache, no-store, max-age=0'; +HEADERS.content['Vary'] = 'Accept-Encoding'; +HEADERS.content['Expires'] = '-1'; +HEADERS.content['X-Powered-By'] = 'Total.js'; +HEADERS.stream_release_compress = {}; +HEADERS.stream_release_compress[HEADER_CACHE] = 'public, max-age=11111111'; +HEADERS.stream_release_compress['Access-Control-Allow-Origin'] = '*'; +HEADERS.stream_release_compress['Content-Encoding'] = 'gzip'; +HEADERS.stream_release_compress['X-Powered-By'] = 'Total.js'; +HEADERS.stream_release = {}; +HEADERS.stream_release[HEADER_CACHE] = 'public, max-age=11111111'; +HEADERS.stream_release['Access-Control-Allow-Origin'] = '*'; +HEADERS.stream_release['X-Powered-By'] = 'Total.js'; +HEADERS.stream_debug_compress = {}; +HEADERS.stream_debug_compress[HEADER_CACHE] = 'private, no-cache, no-store, max-age=0'; +HEADERS.stream_debug_compress['Pragma'] = 'no-cache'; +HEADERS.stream_debug_compress['Expires'] = '-1'; +HEADERS.stream_debug_compress['Access-Control-Allow-Origin'] = '*'; +HEADERS.stream_debug_compress['Content-Encoding'] = 'gzip'; +HEADERS.stream_debug_compress['X-Powered-By'] = 'Total.js'; +HEADERS.stream_debug = {}; +HEADERS.stream_debug[HEADER_CACHE] = 'private, no-cache, no-store, max-age=0'; +HEADERS.stream_debug['Pragma'] = 'no-cache'; +HEADERS.stream_debug['Expires'] = '-1'; +HEADERS.stream_debug['Access-Control-Allow-Origin'] = '*'; +HEADERS.stream_debug['X-Powered-By'] = 'Total.js'; +HEADERS.binary_compress = {}; +HEADERS.binary_compress[HEADER_CACHE] = 'private, no-cache, no-store, max-age=0'; +HEADERS.binary_compress['Content-Encoding'] = 'gzip'; +HEADERS.binary_compress['X-Powered-By'] = 'Total.js'; +HEADERS.binary = {}; +HEADERS.binary[HEADER_CACHE] = 'public'; +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.responseLocalize = {}; +HEADERS.responseLocalize['Access-Control-Allow-Origin'] = '*'; +HEADERS.responseNotModified = {}; +HEADERS.responseNotModified[HEADER_CACHE] = 'public, max-age=11111111'; +HEADERS.responseNotModified['X-Powered-By'] = 'Total.js'; +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.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); -global.include = global.INCLUDE = global.source = global.SOURCE = function(name) { - return framework.source(name); -}; +var _controller = ''; +var _owner = ''; +var _flags; +var _prefix; + +// GO ONLINE MODE +!global.framework_internal && (global.framework_internal = require('./internal')); +!global.framework_builders && (global.framework_builders = require('./builders')); +!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_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.MODULE = function(name) { - return framework.module(name); +global.SESSION = function(name) { + global.framework_session = require('./session'); + global.SESSION = sessionwrapper; + return sessionwrapper(name); }; -global.DATABASE = function() { - return framework.database.apply(framework, arguments); -}; +var TMPENV = framework_utils.copy(process.env); +TMPENV.istotaljsworker = true; -global.MODEL = function(name) { - return framework.model(name); -}; +HEADERS.workers = { cwd: '', silent: false, env: TMPENV }; +HEADERS.workers2 = { cwd: '', silent: true, env: TMPENV }; -if (typeof(setImmediate) === UNDEFINED) { - global.setImmediate = function(cb) { - process.nextTick(cb); - }; -} +global.Builders = framework_builders; +var U = global.Utils = global.utils = global.U = global.framework_utils; +global.Mail = framework_mail; -function Framework() { +global.WTF = (message, name, uri) => F.problem(message, name, uri); +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; - this.id = null; - this.version = 1502; - this.version_header = '1.5.2'; - - this.versionNode = parseInt(process.version.replace('v', '').replace(/\./g, ''), 10); - - this.handlers = { - onrequest: this._request.bind(this), - onxss: this.onXSS.bind(this), - onupgrade: this._upgrade.bind(this), - onservice: this._service.bind(this) - }; - - this.config = { - - debug: false, - - name: 'total.js', - version: '1.01', - author: '', - secret: os.hostname() + '-' + os.platform() + '-' + os.arch(), - - 'etag-version': '', - - 'directory-contents': '/contents/', - 'directory-controllers': '/controllers/', - 'directory-views': '/views/', - 'directory-definitions': '/definitions/', - 'directory-temp': '/tmp/', - 'directory-templates': '/templates/', - 'directory-models': '/models/', - 'directory-resources': '/resources/', - 'directory-public': '/public/', - 'directory-angular': '/app/', - 'directory-modules': '/modules/', - 'directory-source': '/source/', - 'directory-components': '/components/', - 'directory-logs': '/logs/', - 'directory-tests': '/tests/', - 'directory-databases': '/databases/', - 'directory-workers': '/workers/', - - // all HTTP static request are routed to directory-public - 'static-url': '', - 'static-url-js': '/js/', - 'static-url-css': '/css/', - 'static-url-image': '/img/', - 'static-url-video': '/video/', - 'static-url-font': '/font/', - 'static-url-download': '/download/', - 'static-accepts': ['.jpg', '.png', '.gif', '.ico', EXTENSION_JS, EXTENSION_COFFEE, '.css', '.txt', '.xml', '.woff', '.otf', '.ttf', '.eot', '.svg', '.zip', '.rar', '.pdf', '.docx', '.xlsx', '.doc', '.xls', '.html', '.htm', '.appcache', '.map', '.ogg', '.mp4', '.mp3', '.webp', '.webm', '.swf', '.package'], - - // 'static-accepts-custom': [], - - 'default-layout': '_layout', - - 'angular-version': '1.2.16', - 'angular-i18n-version': '1.2.15', - - // default maximum request size / length - // default 5 kB - 'default-request-length': 1024 * 5, - 'default-websocket-request-length': 1024 * 5, - 'default-websocket-encodedecode': true, - - // in milliseconds - 'default-request-timeout': 3000, - - // otherwise is used ImageMagick (Heroku supports ImageMagick) - // gm = graphicsmagick or im = imagemagick - 'default-image-converter': 'gm', - 'default-image-quality': 93, - - 'allow-gzip': true, - 'allow-websocket': true, - 'allow-compile-js': true, - 'allow-compile-css': true, - 'allow-compress-html': true, - 'allow-performance': false, - }; - - this.global = {}; - this.resources = {}; - this.connections = {}; - this.functions = {}; - this.versions = null; - - this.isDebug = true; - this.isTest = false; - this.isLoaded = false; - - this.routes = { - web: [], - files: [], - websockets: [], - partial: {}, - partialGlobal: [], - redirects: {}, - resize: {} - }; - - this.helpers = {}; - this.modules = {}; - this.models = {}; - this.sources = {}; - this.components = {}; - this.controllers = {}; - this.tests = {}; - this.errors = []; - this.problems = []; - this.changes = []; - this.server = null; - this.port = 0; - this.ip = ''; - - this.workers = {}; - this.databases = {}; - this.directory = directory; - this.isLE = os.endianness ? os.endianness() === 'LE' : true; - this.isHTTPS = false; - - this.temporary = { - path: {}, - processing: {}, - range: {}, - views: {} - }; - - this.stats = { - - request: { - pending: 0, - web: 0, - xhr: 0, - file: 0, - websocket: 0, - get: 0, - post: 0, - put: 0, - upload: 0, - xss: 0, - blocked: 0, - 'delete': 0 - }, - - response: { - view: 0, - json: 0, - websocket: 0, - timeout: 0, - custom: 0, - binary: 0, - pipe: 0, - file: 0, - destroy: 0, - stream: 0, - streaming: 0, - plain: 0, - empty: 0, - redirect: 0, - forwarding: 0, - restriction: 0, - notModified: 0, - mmr: 0, - sse: 0, - error400: 0, - error401: 0, - error403: 0, - error404: 0, - error408: 0, - error431: 0, - error500: 0, - error501: 0 - } - }; - - // intialize cache - this.cache = new FrameworkCache(this); - this.fs = new FrameworkFileSystem(this); - this.path = new FrameworkPath(this); - this.restrictions = new FrameworkRestrictions(this); - - this._request_check_redirect = false; - this._request_check_referer = false; - this._request_check_POST = false; - this._length_partial_private = 0; - this._length_partial_global = 0; - this._length_files = 0; - - this.isCoffee = false; - this.isWindows = os.platform().substring(0, 3).toLowerCase() === 'win'; - - var self = this; +function nomemwrapper(name) { + return global.framework_nosql.inmemory(name); } -// ====================================================== -// PROTOTYPES -// ====================================================== - -Framework.prototype = { +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); +}; - get async() { +global.CONFIG = function(name, val) { + return arguments.length === 1 ? CONF[name] : (CONF[name] = val); +}; - var self = this; +var prefid; - if (typeof(self._async) === UNDEFINED) - self._async = new utils.Async(self); +global.PREF = {}; +global.PREF.set = function(name, value) { - return self._async; - } -} + if (value === undefined) + return F.pref[name]; -Framework.prototype.__proto__ = new events.EventEmitter(); + if (value === null) { + delete F.pref[name]; + } else + F.pref[name] = global.PREF[name] = value; -/* - Refresh framework internal information - @clear {Boolean} || optional, default true - clear TMP directory - return {Framework} -*/ -Framework.prototype.refresh = function(clear) { - var self = this; + prefid && clearTimeout(prefid); + prefid = setTimeout(F.onPrefSave, 1000, F.pref); +}; - self.emit('clear', 'refresh'); +global.CACHE = function(name, value, expire, persistent) { + return arguments.length === 1 ? F.cache.get2(name) : F.cache.set(name, value, expire, persistent); +}; - self.resources = {}; - self.databases = {}; - self.configure(); - self.configureMapping(); - self.temporary.path = {}; - self.temporary.range = {}; - self.temporary.views = {}; - self.emit('reconfigure'); +global.CREATE = (group, name) => framework_builders.getschema(group, name).default(); +global.SINGLETON = (name, def) => SINGLETONS[name] || (SINGLETONS[name] = (new Function('return ' + (def || '{}')))()); +global.FUNCTION = (name) => F.functions[name] || NOOP; +global.FINISHED = framework_internal.onFinished; +global.DESTROY = framework_internal.destroyStream; - if (clear || true) - self.clear(); +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')); +} - return self; +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 }; -/* - Add/Register a new controller - @name {String} - @definition {Object} :: optional, controller definition - return {Framework} -*/ -Framework.prototype.controller = function(name, definition) { +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 self = this; + var index; - // is controller initialized? - if (self.controllers[name]) - return self.controllers[name]; + 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)); +}; - // get controller name to internal property - _controller = name; +global.ERROR = function(name) { + return name == null ? F.errorcallback : function(err) { + err && F.error(err, name); + }; +}; - var obj = null; +global.AUTH = function(fn) { + F.onAuthorize = framework_builders.AuthOptions.wrap(fn); +}; - if (!definition) { +global.WEBSOCKETCLIENT = function(callback) { + var ws = require('./websocketclient').create(); + callback && callback.call(ws, ws); + return ws; +}; - var filename = path.join(directory, self.config['directory-controllers'], name); - if (self.isCoffee) { - if (fs.existsSync(filename + EXTENSION_COFFEE)) - filename += EXTENSION_COFFEE; - else - filename += EXTENSION_JS; - } else - filename += EXTENSION_JS; +global.$CREATE = function(schema) { + var o = framework_builders.getschema(schema); + return o ? o.default() : null; +}; - obj = require(filename); +global.$MAKE = function(schema, model, filter, callback, novalidate, argument) { - } else - obj = definition(); + var o = framework_builders.getschema(schema); + var w = null; - self.controllers[name] = obj; + if (typeof(filter) === 'function') { + var tmp = callback; + callback = filter; + filter = tmp; + } - if (obj.install) { - obj.install.call(self, self, name); - return self; - } + 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; + } + } - if (obj.init) { - obj.init.call(self, self, name); - return self; - } + return o ? o.make(model, filter, callback, argument, novalidate, w) : undefined; +}; - return self; +global.$QUERY = function(schema, 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; }; -Framework.prototype._routeSort = function() { +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; +}; - var self = this; +global.$WORKFLOW = function(schema, 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; +}; - self.routes.web.sort(function(a, b) { - if (a.priority > b.priority) - return -1; +global.$TRANSFORM = function(schema, 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; +}; - if (a.priority < b.priority) - return 1; +global.$REMOVE = function(schema, options, callback, controller) { + var o = framework_builders.getschema(schema); - return 0; - }); + if (typeof(options) === 'function') { + controller = callback; + callback = options; + options = EMPTYOBJECT; + } - self.routes.websockets.sort(function(a, b) { - if (a.priority > b.priority) - return -1; + if (o) + o.remove(options, callback, controller); + else + callback && callback(new Error('Schema "{0}" not found.'.format(getSchemaName(schema)))); + return !!o; +}; - if (a.priority < b.priority) - return 1; +global.$SAVE = function(schema, model, options, callback, controller, novalidate) { + return performschema('$save', schema, model, options, callback, controller, novalidate); +}; - return 0; - }); +global.$INSERT = function(schema, model, options, callback, controller, novalidate) { + return performschema('$insert', schema, model, options, callback, controller, novalidate); +}; - return self; +global.$UPDATE = function(schema, model, options, callback, controller, novalidate) { + return performschema('$update', schema, model, options, callback, controller, novalidate); }; -/* - @name {String} :: file name of database - return {nosql} -*/ -Framework.prototype.database = function(name) { +global.$PATCH = function(schema, model, options, callback, controller, novalidate) { + return performschema('$patch', schema, model, options, callback, controller, novalidate); +}; - var self = this; +// GET Users/Neviem --> @query @workflow +global.$ACTION = function(schema, model, callback, controller) { - var db = self.databases[name]; + if (typeof(model) === 'function') { + controller = callback; + callback = model; + model = null; + } - if (typeof(db) !== UNDEFINED) - return db; + var meta = F.temporary.other[schema]; + var tmp, index; - self._verify_directory('databases'); + if (!meta) { - db = require('./nosql').load(path.join(directory, this.config['directory-databases'], name), path.join(directory, this.config['directory-databases'], name + '-binary'), true); - self.databases[name] = db; + index = schema.indexOf('-->'); - return db; -}; + var op = (schema.substring(index + 3).trim().trim() + ' ').split(/\s@/).trim(); + tmp = schema.substring(0, index).split(/\s|\t/).trim(); -/* - Stop the server and exit - @code {Number} :: optional, exit code - default 0 - return {Framework} -*/ -Framework.prototype.stop = function(code) { - var self = this; + if (tmp.length !== 2) { + callback('Invalid "{0}" type.'.format(schema)); + return; + } - if (typeof(process.send) === FUNCTION) - process.send('stop'); + meta = {}; + meta.method = tmp[0].toUpperCase(); + meta.schema = tmp[1]; - self.cache.stop(); - self.server.close(); + if (meta.schema[0] === '*') + meta.schema = meta.schema.substring(1); - process.exit(code || 0); - return self; -}; + meta.op = []; + meta.opcallbackindex = -1; -/** - * Add a redirect route - * @param {String} host Domain with protocol. - * @param {String} newHost Domain with protocol. - * @param {Boolean} withPath Copy path (default: true). - * @param {Boolean} permanent Is permanent redirect (302)? (default: false) - * @return {Framework} - */ -Framework.prototype.redirect = function(host, newHost, withPath, permanent) { - var self = this; + 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; + } - if (host[host.length - 1] === '/') - host = host.substring(0, host.length - 1); + for (var i = 0; i < op.length; i++) { - if (newHost[newHost.length - 1] === '/') - newHost = newHost.substring(0, newHost.length - 1); + tmp = {}; - self.routes.redirects[host] = { - url: newHost, - path: withPath, - permanent: permanent - }; - self._request_check_redirect = true; + var item = op[i]; + if (item[0] === '@') + item = item.substring(1); - return self; -}; + index = item.indexOf('('); -/** - * Auto resize picture according the path - * @param {String} url Relative path. - * @param {String} width New width (optional). - * @param {String} height New height (optional). - * @param {Object} options Additional options. - * @param {String Array} ext Allowed file extension (optional). - * @param {String} path Source directory (optional). - * @return {Framework} - */ -Framework.prototype.resize = function(url, width, height, options, path, extensions) { - var self = this; - var extension = null; - var index = url.lastIndexOf('.'); + if (index !== -1) { + meta.opcallbackindex = i; + tmp.response = true; + item = item.substring(0, index).trim(); + } - if (index !== -1) - extension = [url.substring(index)]; - else - extension = extensions || ['.jpg', '.png', '.gif']; + tmp.name = item; + tmp.name2 = '$' + tmp.name; - var length = extension.length; - for (var i = 0; i < length; i++) - extension[i] = (extension[i][0] !== '.' ? '.' : '') + extension[i].toLowerCase(); + 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; + } + } - index = url.lastIndexOf('/'); - if (index !== -1) - url = url.substring(0, index); + if (tmp.type) + tmp.type2 = tmp.type.substring(1); - if (url[0] !== '/') - url = '/' + url; + meta.op.push(tmp); + } - if (url[url.length - 1] !== '/') - url += '/'; + meta.multiple = meta.op.length > 1; + meta.schema = o; + meta.validate = meta.method !== 'GET'; + F.temporary.other[schema] = meta; + } - path = path || url; + if (meta.validate) { - if (!options) - options = {}; + var req = controller ? controller.req : null; + if (meta.method === 'PATCH' || meta.method === 'DELETE') { + if (!req) + req = {}; + req.$patch = true; + } - self.routes.resize[url] = { - width: width, - height: height, - extension: extension, - path: path || url, - grayscale: options.grayscale, - blur: options.blur, - rotate: options.rotate, - flip: options.flip, - flop: options.flop, - sepia: options.sepia, - quality: options.quality - }; + var data = {}; + data.meta = meta; + data.callback = callback; + data.controller = controller; + meta.schema.make(model, null, performsschemaaction_async, data, null, null, req); + } else + 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); +} - return self; -}; +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); + } + } +} -/** - * Add a route - * @param {String} url - * @param {Function} funcExecute Action. - * @param {String Array} flags - * @param {Number} maximumSize Maximum length of request data. - * @param {String Array} partial Loads partial content. - * @param {Number timeout Response timeout. - * @return {Framework} - */ -Framework.prototype.route = function(url, funcExecute, flags, maximumSize, partial, timeout) { - - if (url === '') - url = '/'; - - if (utils.isArray(maximumSize)) { - var tmp = partial; - partial = maximumSize; - maximumSize = tmp; - } - - if (typeof(funcExecute) === OBJECT || funcExecute instanceof Array) { - var tmp = funcExecute; - funcExecute = flags; - flags = tmp; - } - - if (!utils.isArray(flags) && typeof(flags) === 'object') { - maximumSize = flags['max'] || flags['length'] || flags['maximum'] || flags['maximumSize'] || flags['size']; - partial = flags['partials'] || flags['partial']; - timeout = flags['timeout']; - flags = flags['flags'] || flags['flag']; - } - - var self = this; - var priority = 0; - var index = url.indexOf(']'); - var subdomain = null; - var isASTERIX = url.indexOf('*') !== -1; - - priority = url.count('/'); - - if (isASTERIX) { - url = url.replace('*', '').replace('//', '/'); - priority = (-10) - priority; - } - - if (index > 0) { - subdomain = url.substring(1, index).trim().toLowerCase().split(','); - url = url.substring(index + 1); - priority += 2; - } - - var isRaw = false; - - if (flags) { - var tmp = []; - for (var i = 0; i < flags.length; i++) { - var flag = flags[i].toString().toLowerCase(); - switch (flag) { - case 'raw': - isRaw = true; - break; - case 'authorize': - priority += 2; - tmp.push('authorize'); - break; - case 'unauthorize': - priority += 2; - tmp.push('unauthorize'); - break; - case 'logged': - priority += 2; - tmp.push('authorize'); - console.log('OBSOLETE: flag "logged" - use "authorize".'); - break; - case 'unlogged': - tmp.push('unauthorize'); - console.log('OBSOLETE: flag "unlogged" - use "unauthorize".'); - break; - case 'referer': - case 'referrer': - tmp.push('referer'); - break; - default: - tmp.push(flag); - break; - } - } - flags = tmp; - priority += (flags.length * 2); - } else - flags = ['get']; - - var isMixed = flags.indexOf('mmr') !== -1; - - if (isMixed && url.indexOf('{') !== -1) - throw new Error('Mixed route cannot contain dynamic path.'); - - if (isMixed && flags.indexOf('upload') !== -1) - throw new Error('Multipart mishmash: mmr vs. upload.'); - - var isMember = false; - - if (flags.indexOf('logged') === -1 && flags.indexOf('authorize') === -1 && flags.indexOf('unauthorize') === -1) - isMember = true; - - var routeURL = internal.routeSplit(url.trim()); - var arr = []; - - if (url.indexOf('{') !== -1) { - routeURL.forEach(function(o, i) { - if (o.substring(0, 1) === '{') - arr.push(i); - }); - priority -= arr.length; - } - - if (url.indexOf('#') !== -1) - priority -= 100; - - if (flags.indexOf('proxy') !== -1 && flags.indexOf('json') === -1) { - flags.push('json'); - priority++; - } - - if ((flags.indexOf('json') !== -1 || isRaw) && (flags.indexOf('post') === -1 && flags.indexOf('put') === -1) && flags.indexOf('patch') === -1) { - flags.push('post'); - priority++; - } - - if (isMixed) { - if (flags.indexOf('post') === -1 && flags.indexOf('put') === -1 && flags.indexOf('upload') === -1) { - flags.push('upload'); - priority++ - } - } - - if (flags.indexOf('get') === -1 && - flags.indexOf('post') === -1 && - flags.indexOf('delete') === -1 && - flags.indexOf('put') === -1 && - flags.indexOf('upload') === -1 && - flags.indexOf('head') === -1 && - flags.indexOf('trace') === -1 && - flags.indexOf('patch') === -1 && - flags.indexOf('propfind') === -1) - flags.push('get'); - - if (flags.indexOf('referer') !== -1) - self._request_check_referer = true; - - if (!self._request_check_POST && (flags.indexOf('post') !== -1 || flags.indexOf('put') !== -1 || flags.indexOf('upload') !== -1 || flags.indexOf('mmr') !== -1 || flags.indexOf('json') !== -1 || flags.indexOf('patch') !== -1)) - self._request_check_POST = true; - - if (!(partial instanceof Array)) - partial = null; - - self.routes.web.push({ - priority: priority, - subdomain: subdomain, - name: (_controller || '').length === 0 ? 'unknown' : _controller, - url: routeURL, - param: arr, - flags: flags || [], - onExecute: funcExecute, - maximumSize: (maximumSize || self.config['default-request-length']) * 1024, - partial: partial, - timeout: timeout || self.config['default-request-timeout'], - isJSON: flags.indexOf('json') !== -1, - isRAW: isRaw, - isMEMBER: isMember, - isXSS: flags.indexOf('xss') !== -1, - isASTERIX: isASTERIX - }); - - if (_controller.length === 0) - self._routeSort(); - - return self; -}; +// 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; +} -/* - Add a new partial route - @name {String or Function} :: if @name is function, route will be a global partial content - @funcExecute {Function} :: optional - return {Framework} -*/ -Framework.prototype.partial = function(name, funcExecute) { - var self = this; +global.$ASYNC = function(schema, callback, index, controller) { - if (typeof(name) === FUNCTION) { - self.routes.partialGlobal.push(name); - self._length_partial_global = Object.keys(self.routes.partialGlobal).length; - return self; - } + if (index && typeof(index) === 'object') { + controller = index; + index = undefined; + } - self.routes.partial[name] = funcExecute; - self._length_partial_private = Object.keys(self.routes.partial).length; + var o = framework_builders.getschema(schema).default(); - return self; + if (!o) { + callback && callback(new Error('Schema "{0}" not found.'.format(getSchemaName(schema)))); + return EMPTYOBJECT; + } + + controller && (o.$$controller = controller); + return o.$async(callback, index); }; -/* - Add a new websocket route - @url {String} - @funcInitialize {Function} - @flags {String Array or Object} :: optional - @protocols {String Array} :: optional, websocket-allow-protocols - @allow {String Array} :: optional, allow origin - @maximumSize {Number} :: optional, default by the config - return {Framework} -*/ -Framework.prototype.websocket = function(url, funcInitialize, flags, protocols, allow, maximumSize) { - - if (url === '') - url = '/'; - - if (typeof(funcExecute) === OBJECT) { - var tmp = flags; - funcExecute = flags; - flags = tmp; - } - - if (!utils.isArray(flags) && typeof(flags) === 'object') { - protocols = flags['protocols'] || flags['protocol']; - allow = flags['allow'] || flags['origin']; - maximumSize = flags['max'] || flags['length'] || flags['maximum'] || flags['maximumSize']; - flags = flags['flags']; - } +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; +}; - var self = this; - var priority = 0; - var index = url.indexOf(']'); - var subdomain = null; - var isASTERIX = url.indexOf('*') !== -1; +global.DB = global.DATABASE = function(a, b, c, d) { + return typeof(F.database) === 'object' ? F.database : F.database(a, b, c, d); +}; - priority = url.count('/'); +global.OFF = function() { + return arguments.length > 1 ? F.removeListener.apply(F, arguments) : F.removeAllListeners.apply(F, arguments); +}; - if (index > 0) { - subdomain = url.substring(1, index).trim().toLowerCase().split(','); - url = url.substring(index + 1); - priority += 2; - } +global.NEWSCHEMA = function(group, name, make) { - if (isASTERIX) { - url = url.replace('*', '').replace('//', '/'); - priority = (-10) - priority; - } + if (typeof(name) === 'function') { + make = name; + name = undefined; + } - var arr = []; - var routeURL = internal.routeSplit(url.trim()); + if (!name) { + var arr = group.split('/'); + if (arr.length === 2) { + name = arr[1]; + group = arr[0]; + } else { + name = group; + group = 'default'; + } + } - if (url.indexOf('{') !== -1) { - routeURL.forEach(function(o, i) { - if (o.substring(0, 1) === '{') - arr.push(i); - }); - priority -= arr.length; - } + var schema = framework_builders.newschema(group, name); + make && make.call(schema, schema); + return schema; +}; - if (typeof(allow) === STRING) - allow = allow[allow]; +global.CLEANUP = function(stream, callback) { + FINISHED(stream, function() { + DESTROY(stream); + if (callback) { + callback(); + callback = null; + } + }); +}; - if (typeof(protocols) === STRING) - protocols = protocols[protocols]; +global.SUCCESS = function(success, value) { - if (typeof(flags) === STRING) - flags = flags[flags]; + if (typeof(success) === 'function') { + return function(err, value) { + success(err, SUCCESS(err, value)); + }; + } - var isJSON = false; - var isBINARY = false; - var tmp = []; + var err; - if (typeof(flags) === UNDEFINED) - flags = []; + if (success instanceof Error) { + err = success.toString(); + success = false; + } else if (success instanceof framework_builders.ErrorBuilder) { + if (success.hasError()) { + err = success.output(); + success = false; + } else + success = true; + } else if (success == null) + success = true; - for (var i = 0; i < flags.length; i++) { - flags[i] = flags[i].toString().toLowerCase(); + SUCCESSHELPER.success = !!success; + SUCCESSHELPER.value = value === SUCCESSHELPER ? value.value : value == null ? undefined : (value && value.$$schema ? value.$clean() : value); + SUCCESSHELPER.error = err ? err : undefined; + return SUCCESSHELPER; +}; - if (flags[i] === 'json') - isJSON = true; +global.TRY = function(fn, err) { + try { + fn(); + return true; + } catch (e) { + err && err(e); + return false; + } +}; - if (flags[i] === 'binary') - isBINARY = true; +global.OBSOLETE = function(name, message) { - if (flags[i] === 'raw') { - isBINARY = false; - isJSON = false; - } + if (F.config.nowarnings) + return; - if (flags[i] !== 'json' && flags[i] !== 'binary' && flags[i] !== 'raw') - tmp.push(flags[i]); - } + console.log(NOW.format('yyyy-MM-dd HH:mm:ss') + ' :: OBSOLETE / IMPORTANT ---> "' + name + '"', message); + if (global.F) + F.stats.other.obsolete++; +}; - flags = tmp; +global.DEBUG = false; +global.TEST = false; +global.RELEASE = false; +global.is_client = false; +global.is_server = true; - priority += (flags.length * 2); +var directory = U.$normalize(require.main ? Path.dirname(require.main.filename) : process.cwd()); - var isMember = false; +// F.service() changes the values below: +var DATE_EXPIRES = new Date().add('y', 1).toUTCString(); - if (!flags || (flags.indexOf('logged') === -1 && flags.indexOf('authorize') === -1)) - isMember = true; +const _randomstring = 'abcdefghijklmnoprstuwxy'.split(''); - self.routes.websockets.push({ - name: (_controller || '').length === 0 ? 'unknown' : _controller, - url: routeURL, - param: arr, - subdomain: subdomain, - priority: priority, - flags: flags || [], - onInitialize: funcInitialize, - protocols: protocols || [], - allow: allow || [], - length: (maximumSize || self.config['default-websocket-request-length']) * 1024, - isMEMBER: isMember, - isJSON: isJSON, - isBINARY: isBINARY, - isASTERIX: isASTERIX - }); +function random2string() { + return _randomstring[(Math.random() * _randomstring.length) >> 0] + _randomstring[(Math.random() * _randomstring.length) >> 0]; +} - if (_controller.length === 0) - self._routeSort(); +const WEBSOCKET_COMPRESS = Buffer.from([0x00, 0x00, 0xFF, 0xFF]); +const WEBSOCKET_COMPRESS_OPTIONS = { windowBits: Zlib.Z_DEFAULT_WINDOWBITS }; +const UIDGENERATOR = { types: {}, typesnumber: {} }; - return self; -}; +function UIDGENERATOR_REFRESH() { -/* - Alias for routeFile -*/ -Framework.prototype.file = function(name, funcValidation, funcExecute) { - var self = this; - self.routes.files.push({ - controller: (_controller || '').length === 0 ? 'unknown' : _controller, - name: name, - onValidation: funcValidation, - onExecute: funcExecute || funcValidation - }); - self._length_files++; - return self; -}; + var ticks = NOW.getTime(); + var dt = Math.round(((ticks - 1580511600000) / 1000 / 60)); -/* - Error caller - @err {Error} - @name {String} :: controller name - @uri {URI} :: optional - return {Framework} -*/ -Framework.prototype.error = function(err, name, uri) { - var self = this; + UIDGENERATOR.date = dt + ''; + UIDGENERATOR.date16 = dt.toString(16); - if (self.errors !== null) { - self.errors.push({ - error: err, - name: name, - uri: uri, - date: new Date() - }); + 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(); - if (self.errors.length > 50) - self.errors.shift(); - } + var keys; - self.onError(err, name, uri); - return self; -}; + if (UIDGENERATOR.multiple) { + keys = Object.keys(UIDGENERATOR.types); + for (var i = 0; i < keys.length; i++) + UIDGENERATOR.types[keys[i]] = 0; + } -/* - Problem caller - @message {String} - @name {String} :: controller name - @uri {URI} :: optional - @ip {String} :: optional - return {Framework} -*/ -Framework.prototype.problem = function(message, name, uri, ip) { - var self = this; + if (UIDGENERATOR.multiplenumber) { + keys = Object.keys(UIDGENERATOR.typesnumber); + for (var i = 0; i < keys.length; i++) + UIDGENERATOR.typesnumber[keys[i]] = 0; + } +} - if (self.problems !== null) { - self.problems.push({ - message: message, - name: name, - uri: uri, - ip: ip - }); +UIDGENERATOR_REFRESH(); - if (self.problems.length > 50) - self.problems.shift(); - } +const EMPTYBUFFER = Buffer.alloc(0); +global.EMPTYBUFFER = EMPTYBUFFER; - self.emit('problem', message, name, uri, ip); - return self; -}; +const controller_error_status = function(controller, status, problem) { -/* - Change caller - @message {String} - @name {String} :: controller name - @uri {URI} :: optional - @ip {String} :: optional - return {Framework} -*/ -Framework.prototype.change = function(message, name, uri, ip) { - var self = this; + if (status !== 500 && problem) + controller.problem(problem); - if (self.changes !== null) { - self.changes.push({ - message: message, - name: name, - uri: uri, - ip: ip - }); + if (controller.res.success || controller.res.headersSent || !controller.isConnected) + return controller; - if (self.changes.length > 50) - self.changes.shift(); - } + controller.precache && controller.precache(null, null, null); + controller.req.path = EMPTYARRAY; + controller.req.$total_success(); + controller.req.$total_route = F.lookup(controller.req, '#' + status, EMPTYARRAY, 0); + controller.req.$total_exception = problem; + controller.req.$total_execute(status, true); - self.emit('change', message, name, uri, ip); - return self; + return controller; }; -/* - Module caller - @name {String} - return {Object} :: framework return require(); -*/ -Framework.prototype.module = function(name) { - - var self = this; - var module = self.modules[name]; +var PERF = {}; - if (typeof(module) !== UNDEFINED) - return module; +function Framework() { - if (self.isLoaded) - return null; + var self = this; + + 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, + + //nowarnings: process.argv.indexOf('restart') !== -1, + nowarnings: true, + name: 'Total.js', + version: '1.0.0', + author: '', + 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, 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_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_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, + + // in milliseconds + default_request_timeout: 3000, + default_dependency_timeout: 1500, + default_restbuilder_timeout: 10000, + + // otherwise is used ImageMagick (Heroku supports ImageMagick) + // 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_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; + } + }; + + 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: {}, + resize: {}, + request: [], + views: {}, + merge: {}, + mapping: {}, + packages: {}, + blocks: {}, + proxies: [], + resources: {} + }; + + 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: /^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$/ + }; + + 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 + // self.waits = {}; + + self.temporary = { + path: {}, + shortcache: {}, + notfound: {}, + processing: {}, + range: {}, + views: {}, + versions: {}, + dependencies: {}, // temporary for module dependencies + other: {}, + keys: {}, // for crypto keys + internal: {}, // controllers/modules names for the routing + owners: {}, + ready: {}, + ddos: {}, + service: { redirect: 0, request: 0, file: 0, usage: 0 } + }; + + 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, + mail: 0 + }, + + request: { + request: 0, + pending: 0, + web: 0, + xhr: 0, + file: 0, + websocket: 0, + get: 0, + options: 0, + head: 0, + post: 0, + put: 0, + patch: 0, + upload: 0, + schema: 0, + operation: 0, + blocked: 0, + 'delete': 0, + mobile: 0, + desktop: 0 + }, + response: { + ddos: 0, + view: 0, + json: 0, + websocket: 0, + timeout: 0, + custom: 0, + binary: 0, + pipe: 0, + file: 0, + image: 0, + destroy: 0, + stream: 0, + streaming: 0, + plain: 0, + empty: 0, + redirect: 0, + forward: 0, + proxy: 0, + notModified: 0, + sse: 0, + errorBuilder: 0, + error400: 0, + error401: 0, + error403: 0, + error404: 0, + error408: 0, + error409: 0, + error431: 0, + error500: 0, + error501: 0, + error503: 0 + } + }; + + // intialize cache + 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] }; +} - var configDirectory = self.config['directory-modules']; - var filename = path.join(directory, configDirectory, name); - var isDirectory = false; +// ====================================================== +// PROTOTYPES +// ====================================================== - if (self.isCoffee) { - if (fs.existsSync(filename)) - filename += EXTENSION_COFFEE; - else - filename += EXTENSION_JS; - } else - filename += EXTENSION_JS; +Framework.prototype = { + get datetime() { + return global.NOW; + }, + set datetime(val) { + global.NOW = val; + }, + get cluster() { + return require('./cluster'); + }, + get id() { + 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; + F.$id = value; + return F.$id; + } +}; + +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; + proto.Database = framework_nosql.Database.prototype; + proto.DatabaseBinary = framework_nosql.DatabaseBinary.prototype; + 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; + proto.RESTBuilder = framework_builders.RESTBuilder.prototype; + proto.RESTBuilderResponse = framework_builders.RESTBuilderResponse.prototype; + proto.SchemaBuilder = framework_builders.SchemaBuilder.prototype; + proto.SchemaOptions = framework_builders.SchemaOptions.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; +}; + +global.ON = F.on = function(name, fn) { + + if (name === 'init' || name === 'ready' || name === 'load') { + 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(F); + break; + case 'component': + F.temporary.ready[name] && fn.call(F); + break; + case 'model': + F.temporary.ready[name] && fn.call(F, F.models[arr[1]]); + break; + case 'source': + F.temporary.ready[name] && fn.call(F, F.sources[arr[1]]); + break; + case 'package': + case 'module': + F.temporary.ready[name] && fn.call(F, F.modules[arr[1]]); + break; + case 'controller': + 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 F; +}; + +global.EMIT = F.emit = function(name, a, b, c, d, e, f, g) { + + var evt = F.$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(F, a, b, c, d, e, f, g); + } + if (clean) { + evt = evt.remove(n => n.$once); + if (evt.length) + F.$events[name] = evt; + else + F.$events[name] = undefined; + } + } + return F; +}; + +global.ONCE = F.once = function(name, fn) { + fn.$once = true; + return F.on(name, fn); +}; + +F.removeListener = function(name, fn) { + var evt = F.$events[name]; + if (evt) { + evt = evt.remove(n => n === fn); + if (evt.length) + F.$events[name] = evt; + else + F.$events[name] = undefined; + } + return F; +}; + +F.removeAllListeners = function(name) { + if (name) + F.$events[name] = undefined; + else + F.$events = {}; + return F; +}; - if (!fs.existsSync(filename)) { +/** + * Internal function + * @return {String} Returns current (dependency type and name) owner. + */ +F.$owner = function() { + return _owner; +}; + +F.isSuccess = function(obj) { + return obj === SUCCESSHELPER; +}; + +F.convert = function(value, convertor) { + + if (convertor) { + + 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; + else if (convertor === Boolean) + convertor = U.parseBoolean; + else if (typeof(convertor) === 'string') { + switch (convertor.toLowerCase()) { + case 'json': + convertor = U.parseJSON; + break; + case 'float': + case 'number': + case 'double': + convertor = U.parseFloat; + break; + case 'int': + case 'integer': + convertor = U.parseInt2; + break; + default: + return console.log('Unknown convertor type:', convertor); + } + } + + F.convertors.push({ name: value, convertor: convertor }); + F._length_convertors = F.convertors.length; + return true; + } + + 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; +}; - filename = path.join(directory, configDirectory, name, name); +/** + * Get a controller + * @param {String} name + * @return {Object} + */ +F.controller = function(name) { + return F.controllers[name] || null; +}; - if (self.isCoffee) { - if (fs.existsSync(filename + EXTENSION_COFFEE)) - filename += EXTENSION_COFFEE; - else - filename += EXTENSION_JS; - } else - filename += EXTENSION_JS; +/** + * Use configuration + * @param {String} filename + * @return {Framework} + */ +F.useConfig = function(name) { + OBSOLETE('F.useConfig', 'F.useConfig will be moreved in Total.js v4'); + return F.$configure_configs(name, true); +}; - if (!fs.existsSync(filename)) { +Mail.use = function(smtp, options, callback) { - filename = path.join(directory, configDirectory, name, 'index'); - if (self.isCoffee) { - if (fs.existsSync(filename + EXTENSION_COFFEE)) - filename += EXTENSION_COFFEE; - else - filename += EXTENSION_JS; - } else - filename += EXTENSION_JS; + if (typeof(options) === 'function') { + callback = options; + options = undefined; + } - } else - module = require(filename); + Mail.try(smtp, options, function(err) { - if (fs.existsSync(filename)) - module = require(filename); + if (!err) { + delete F.temporary.mail_settings; + CONF.mail_smtp = smtp; + CONF.mail_smtp_options = options; + } - isDirectory = true; + if (callback) + callback(err); + else if (err) + F.error(err, 'F.useSMTP()', null); + }); +}; - } else - module = require(filename); +F.useSMTP = function(smtp, options, callback) { + OBSOLETE('F.useSMTP', 'Use `Mail.use() instead of F.useSMTP()'); + Mail.use(smtp, options, callback); + return F; +}; - if (typeof(module) === UNDEFINED) - return null; +/** + * Sort all routes + * @return {Framework} + */ +F.$routesSort = function(type) { + + F.routes.web.sort((a, b) => a.priority > b.priority ? -1 : a.priority < b.priority ? 1 : 0); + F.routes.websockets.sort((a, b) => a.priority > b.priority ? -1 : a.priority < b.priority ? 1 : 0); + + var cache = {}; + var length = F.routes.web.length; + var url; + + for (var i = 0; i < length; i++) { + var route = F.routes.web[i]; + var name = F.temporary.internal[route.controller]; + if (name) + route.controller = name; + if (!route.isMOBILE || route.isUPLOAD || route.isXHR || route.isJSON || route.isSYSTEM || route.isXML || route.flags.indexOf('get') === -1) + continue; + url = route.url.join('/'); + cache[url] = true; + } + + for (var i = 0; i < length; i++) { + var route = F.routes.web[i]; + if (route.isMOBILE || route.isUPLOAD || route.isXHR || route.isJSON || route.isSYSTEM || route.isXML || route.flags.indexOf('get') === -1) + continue; + url = route.url.join('/'); + route.isMOBILE_VARY = cache[url] === true; + } + + (!type || type === 1) && F.routes.web.forEach(function(route) { + var tmp = F.routes.web.findItem(item => item.hash === route.hash && item !== route); + route.isUNIQUE = tmp == null; + }); + + // Clears cache + Object.keys(F.temporary.other).forEach(function(key) { + if (key[0] === '1') + F.temporary.other[key] = undefined; + }); + + return F; +}; + +F.parseComponent = parseComponent; + +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,FUNC,REPO,FILESTORAGE;try{' + body + ';\n}catch(e){next(e)}'); + } catch(e) { + err = e; + } + + if (err) { + callback && callback(err); + return compilation ? err : F; + } + + if (compilation) { + return (function() { + return function(model, callback, param) { + return fn.call(EMPTYOBJECT, function(value) { + if (value instanceof Error) + callback(value, undefined, param); + else + callback(null, value, param); + }, model, scriptNow); + }; + })(); + } + + fn.call(EMPTYOBJECT, function(value) { + + if (!callback) + return; + + if (value instanceof Error) + callback(value); + else + callback(null, value); + + }, value, scriptNow, param); + + return F; +}; + +function scriptNow() { + return new Date(); +} - _controller = '#module-' + 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; +} - if (module !== null && typeof(module.directory) === UNDEFINED) - module.directory = isDirectory ? path.join(directory, configDirectory) : path.join(directory, configDirectory, name); +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; - self.modules[name] = module; - return module; + return nosqlwrapper(name); }; -/* - Component caller - @name {String} - return {Object} :: framework return require(); -*/ -Framework.prototype.component = function(name) { - var self = this; - var component = self.components[name]; +function tablewrapper(name) { + var db = F.databases['$' + name]; + if (db) + return db; - if (typeof(component) !== UNDEFINED) - return component; + if (name[0] === '~') { + db = framework_nosql.load(U.getName(name), name.substring(1), true); + } else { + F.path.verify('databases'); + db = framework_nosql.table(name, F.path.databases(name)); + } - if (self.isLoaded) - return null; - - var configDirectory = self.config['directory-components']; - var filename = path.join(directory, configDirectory, name); - var isDirectory = false; + F.databases['$' + name] = db; + return db; +} - if (self.isCoffee) { - if (fs.existsSync(filename + EXTENSION_COFFEE)) - filename += EXTENSION_COFFEE; - else - filename += EXTENSION_JS; - } else - filename += EXTENSION_JS; +global.TABLE = function(name) { + if (!global.framework_nosql) + global.framework_nosql = require('./nosql'); + global.TABLE = tablewrapper; + return tablewrapper(name); +}; - if (!fs.existsSync(filename)) { +F.stop = F.kill = function(signal) { - filename = path.join(directory, configDirectory, name, name); + if (F.isKilled) + return F; - if (self.isCoffee) { - if (fs.existsSync(filename + EXTENSION_COFFEE)) - filename += EXTENSION_COFFEE; - else - filename += EXTENSION_JS; - } else - filename += EXTENSION_JS; + F.isKilled = true; - if (!fs.existsSync(filename)) { + if (!signal) + signal = 'SIGTERM'; - filename = path.join(directory, configDirectory, name, 'index'); + for (var m in F.workers) { + var worker = F.workers[m]; + TRY(() => worker && worker.kill && worker.kill(signal)); + } - if (self.isCoffee) { - if (fs.existsSync(filename + EXTENSION_COFFEE)) - filename += EXTENSION_COFFEE; - else - filename += EXTENSION_JS; - } else - filename += EXTENSION_JS; + global.framework_nosql && global.framework_nosql.kill(signal); - if (fs.existsSync(filename)) - component = require(filename); + EMIT('exit', signal); - } else - component = require(filename); + if (!F.isWorker && process.send && process.connected) + TRY(() => process.send('total:stop')); - isDirectory = true; - } else - component = require(filename); + F.cache.stop(); - if (typeof(component) === UNDEFINED) - return null; + if (F.server) { + F.server.setTimeout(1); + F.server.close(); + } - if (component !== null && typeof(component.directory) === UNDEFINED) - component.directory = isDirectory ? path.join(directory, configDirectory) : path.join(directory, configDirectory, name); + // 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; +}; - _controller = ''; - self.components[name] = component; - if (component.install) - component.install.call(self, self, name, component.directory); +global.PROXY = F.proxy = function(url, target, copypath, before, after) { - if (typeof(component.render) === UNDEFINED) - throw new Error('Component must contain "export.render" function.'); + if (typeof(copypath) == 'function') { + after = before; + before = copypath; + copypath = false; + } - return component; + 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; }; -/* - Install/Init modules - return {Framework} -*/ -Framework.prototype.install = function() { +global.REDIRECT = F.redirect = function(host, newHost, withPath, permanent) { - var self = this; - var dir = path.join(directory, self.config['directory-controllers']); - var framework = self; + var external = host.startsWith('http://') || host.startsWith('https'); + if (external) { - function install_controller(directory, level) { - fs.readdirSync(directory).forEach(function(o) { + if (host[host.length - 1] === '/') + host = host.substring(0, host.length - 1); - var isDirectory = fs.statSync(path.join(directory, o)).isDirectory(); - if (isDirectory) { - level++; - install_controller(path.join(directory, o), level); - return; - } + if (newHost[newHost.length - 1] === '/') + newHost = newHost.substring(0, newHost.length - 1); - var ext = path.extname(o).toLowerCase(); - if (ext !== EXTENSION_JS && ext !== EXTENSION_COFFEE) - return; + F.routes.redirects[host] = { url: newHost, path: withPath, permanent: permanent }; + F._request_check_redirect = true; + F.owners.push({ type: 'redirects', owner: _owner, id: host }); + return F; + } - self.controller((level > 0 ? directory.replace(dir, '') + '/' : '') + o.substring(0, o.length - ext.length)); - }); - } + if (host[0] !== '/') + host = '/' + host; - if (fs.existsSync(dir)) - install_controller(dir, 0); + var flags; - dir = path.join(directory, self.config['directory-modules']); + if (withPath instanceof Array) { + flags = withPath; + withPath = permanent === true; + } else if (permanent instanceof Array) { + flags = permanent; + withPath = withPath === true; + } else + withPath = withPath === true; - if (fs.existsSync(dir)) { - fs.readdirSync(dir).forEach(function(o) { + permanent = withPath; - var ext = path.extname(o); - var isDirectory = fs.statSync(path.join(dir + o)).isDirectory(); - var extLower = ext.toLowerCase(); + if (U.isStaticFile(host)) { + F.file(host, function(req, res) { + if (newHost.startsWith('http://') || newHost.startsWith('https://')) + res.redirect(newHost, permanent); + else + res.redirect(newHost[0] !== '/' ? '/' + newHost : newHost, permanent); + }); + return F; + } - if (!isDirectory && extLower !== EXTENSION_JS && extLower !== EXTENSION_COFFEE) - return; + F.route(host, function() { - var name = o.replace(ext, ''); + if (newHost.startsWith('http://') || newHost.startsWith('https://')) { + this.redirect(newHost + this.href(), permanent); + return; + } - if (name === '#') - return; + if (newHost[0] !== '/') + newHost = '/' + newHost; - var module = self.module(name); + this.redirect(newHost + this.href(), permanent); + }, flags); - if (module === null || typeof(module.install) === UNDEFINED) - return; + return F; +}; - try { - module.install(self, self, name); - } catch (err) { - self.error(err, name); - } - }); - } +/** + * Schedule job + * @param {Date or String} date + * @param {Boolean} repeat Repeat schedule + * @param {Function} fn + * @return {Framework} + */ +global.SCHEDULE = F.schedule = function(date, repeat, fn) { - self._routeSort(); + if (fn === undefined) { + fn = repeat; + repeat = false; + } - dir = path.join(directory, self.config['directory-components']); - if (fs.existsSync(dir)) { - fs.readdirSync(dir).forEach(function(o) { + var type = typeof(date); - var ext = path.extname(o); - var isDirectory = fs.statSync(path.join(dir + o)).isDirectory(); - var extLower = ext.toLowerCase(); + if (type === 'string') { + date = date.parseDate().toUTC(); + repeat && date < NOW && (date = date.add(repeat)); + } else if (type === 'number') + date = new Date(date); - if (!isDirectory && extLower !== EXTENSION_JS && extLower !== EXTENSION_COFFEE) - return; + var sum = date.getTime(); + repeat && (repeat = repeat.replace('each', '1')); + var id = U.GUID(5); + F.schedules[id] = { expire: sum, fn: fn, repeat: repeat, owner: _owner }; + return id; +}; - var name = o.replace(ext, ''); +F.clearSchedule = function(id) { + delete F.schedules[id]; + return F; +}; - if (name === '#') - return; +/** + * Auto resize picture according the path + * @param {String} url Relative path. + * @param {Function(image)} fn Processing. + * @param {String Array} flags Optional, can contains extensions `.jpg`, `.gif' or watching path `/img/gallery/` + * @return {Framework} + */ +global.RESIZE = F.resize = function(url, fn, flags) { + + var extensions = {}; + var cache = true; + + if (typeof(flags) === 'function') { + var tmp = flags; + flags = fn; + fn = tmp; + } + + var ext = url.match(/\*.\*$|\*?\.(jpg|png|gif|jpeg|heif|heic|apng)$/gi); + if (ext) { + url = url.replace(ext, ''); + switch (ext.toString().toLowerCase()) { + case '*.*': + extensions['*'] = true; + break; + case '*.jpg': + case '*.gif': + case '*.png': + case '*.heif': + case '*.heic': + case '*.apng': + case '*.jpeg': + extensions[ext.toString().toLowerCase().replace(/\*/g, '').substring(1)] = true; + break; + } + } + + var path = url; + + if (flags && flags.length) { + for (var i = 0, length = flags.length; i < length; i++) { + var flag = flags[i]; + if (flag[0] === '.') + extensions[flag.substring(1)] = true; + else if (flag[0] === '~' || flag[0] === '/' || flag.match(/^http:|https:/gi)) + path = flag; + else if (flag === 'nocache') + cache = false; + } + } + + if (!extensions.length) { + extensions['jpg'] = true; + extensions['jpeg'] = true; + extensions['png'] = true; + extensions['gif'] = true; + extensions['heic'] = true; + extensions['heif'] = true; + extensions['apng'] = true; + } + + if (extensions['jpg'] && !extensions['jpeg']) + extensions['jpeg'] = true; + else if (extensions['jpeg'] && !extensions['jpg']) + extensions['jpg'] = true; + + F.routes.resize[url] = { fn: fn, path: U.path(path || url), ishttp: path.match(/http:|https:/gi) ? true : false, extension: extensions, cache: cache }; + F.owners.push({ type: 'resize', owner: _owner, id: url }); + return F; +}; - self.component(name); - }); - } +/** + * RESTful routing + * @param {String} url A relative url. + * @param {String Array} flags + * @param {Function} onQuery + * @param {Function(id)} onGet + * @param {Function([id])} onSave + * @param {Function(id)} onDelete + * @return {Framework} + */ +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 = {}; + + if (index !== -1) + flags.splice(index, 1); + + if (onQuery) { + tmp = []; + flags && tmp.push.apply(tmp, flags); + F.route(url, tmp, onQuery); + cors['get'] = true; + } + + var restful = U.path(url) + '{id}'; + + if (onGet) { + cors['get'] = true; + tmp = []; + flags && tmp.push.apply(tmp, flags); + F.route(restful, tmp, onGet); + } + + if (onSave) { + cors['post'] = true; + tmp = ['post']; + flags && tmp.push.apply(tmp, flags); + F.route(url, tmp, onSave); + tmp = ['put']; + cors['put'] = true; + flags && tmp.push.apply(tmp, flags); + F.route(restful, tmp, onSave); + } + + if (onDelete) { + cors['delete'] = true; + tmp = ['delete']; + flags && tmp.push.apply(tmp, flags); + F.route(restful, tmp, onDelete); + } + + if (index !== -1) + F.cors(U.path(url) + '*', Object.keys(cors), flags.indexOf('authorize') === -1); + + return F; +}; + +// 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 = {}; + + if (index !== -1) + flags.splice(index, 1); + + if (onQuery) { + tmp = []; + cors['get'] = true; + flags && tmp.push.apply(tmp, flags); + F.route(url, tmp, onQuery); + } + + if (onGet) { + tmp = []; + cors['get'] = true; + flags && tmp.push.apply(tmp, flags); + F.route(U.path(url) + '{id}', tmp, onGet); + } + + if (onSave) { + tmp = ['post']; + cors['post'] = true; + flags && tmp.push.apply(tmp, flags); + F.route(url, tmp, onSave); + } + + if (onDelete) { + tmp = ['delete']; + cors['delete'] = true; + flags && tmp.push.apply(tmp, flags); + F.route(url, tmp, onDelete); + } + + if (index !== -1) + F.cors(U.path(url) + '*', Object.keys(cors), flags.indexOf('authorize') === -1); + + return F; +}; - dir = path.join(directory, self.config['directory-definitions']); +/** + * Register cors + * @param {String} url + * @param {String Array or String} origin + * @param {String Array or String} methods + * @param {String Array or String} headers + * @param {Boolean} credentials + * @return {Framework} + */ +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 origin = []; + var methods = []; + var headers = []; + var age; + var id; + + if (flags instanceof Array) { + for (var i = 0, length = flags.length; i < length; i++) { + var flag = flags[i]; + var type = typeof(flag); + + if (type === 'string') + flag = flag.toLowerCase(); + else if (type === 'number') { + age = flag; + continue; + } + + if (type === 'boolean' || flag.startsWith('credential')) { + credentials = true; + continue; + } + + if (flag.substring(0, 2) === '//') { + origin.push(flag.substring(2)); + continue; + } + + if (flag.startsWith('http://') || flag.startsWith('https://')) { + origin.push(flag.substring(flag.indexOf('/') + 2)); + continue; + } + + if (flag.substring(0, 3) === 'id:') { + id = flag.substring(3); + continue; + } + + switch (flag) { + case 'post': + case 'put': + case 'delete': + case 'options': + case 'patch': + case 'head': + case 'get': + methods.push(flag.toUpperCase()); + break; + default: + headers.push(flags[i].toLowerCase()); + break; + } + } + } + + 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('*', ''); + + 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.origin = origin.length ? origin : null; + route.methods = methods.length ? methods : null; + route.headers = headers.length ? headers : null; + route.credentials = credentials; + 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._length_cors = F.routes.cors.length; + + F.routes.cors.sort(function(a, b) { + var al = a.url.length; + var bl = b.url.length; + return al > bl ? - 1 : al < bl ? 1 : a.isWILDCARD && b.isWILDCARD ? 1 : 0; + }); + + PERF.OPTIONS = true; + return F; +}; + +function corsextend(a, b) { + for (var i = 0; i < a.length; i++) + b.indexOf(a[i]) === -1 && b.push(a[i]); +} - if (fs.existsSync(dir)) { - fs.readdirSync(dir).forEach(function(o) { - var ext = path.extname(o).toLowerCase(); - if (ext !== EXTENSION_JS && (ext !== EXTENSION_COFFEE)) - return; - var data = fs.readFileSync(path.join(dir, o), 'utf8').toString(); +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 F; +}; + +global.ROUTE = F.web = F.route = function(url, funcExecute, flags, length, language) { + + var name; + var tmp; + var viewname; + var sitemap; + + if (url instanceof Array) { + url.forEach(url => F.route(url, funcExecute, flags, length)); + 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') { + + var sitemapflags = funcExecute instanceof Array ? funcExecute : flags; + if (!(sitemapflags instanceof Array)) + sitemapflags = EMPTYARRAY; + + var index = url.indexOf('/'); + if (index !== -1) { + tmp = url.substring(index); + url = url.substring(0, index); + } else + tmp = ''; + + sitemap = F.sitemap(url, true, language); + + if (sitemap) { + + name = url; + url = sitemap.url; + + if (sitemap.localizeUrl && language === undefined) { + var sitemaproutes = {}; + F.temporary.internal.resources.forEach(function(language) { + var item = F.sitemap(sitemap.id, true, language); + if (item.url && item.url !== url) + sitemaproutes[item.url] = { name: sitemap.id, language: language }; + }); + Object.keys(sitemaproutes).forEach(key => F.route('#' + sitemap.id, funcExecute, flags, length, sitemaproutes[key].language)); + } + + if (tmp) + url += url[url.length - 1] === '/' ? tmp.substring(1) : tmp; + else if (sitemap.wildcard) + url += '*'; + } else + throw new Error('Sitemap item "' + url + '" not found.'); + } else + url = '#' + url; + } + + if (!url) + url = '/'; + + 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 urlcache = url; + + if (!name) + name = url; + + if (method) { + !flags && (flags = []); + flags.push(method); + method = ''; + } + + var priority = 0; + var subdomain = null; + + priority = url.count('/'); + + if (url[0] === '[') { + index = url.indexOf(']'); + if (index > 0) { + subdomain = url.substring(1, index).trim().toLowerCase().split(','); + url = url.substring(index + 1); + priority += subdomain.indexOf('*') !== -1 ? 50 : 100; + } + } + + var isWILDCARD = url.indexOf('*') !== -1; + if (isWILDCARD) { + url = url.replace('*', '').replace('//', '/'); + priority = priority - 100; + } + + var isRaw = false; + var isNOXHR = false; + var schema; + var workflow; + var isMOBILE = false; + var isJSON = false; + var isDELAY = false; + var isROBOT = false; + var isBINARY = false; + var isCORS = false; + var isROLE = false; + var novalidate = false; + var middleware = null; + var timeout; + var options; + var corsflags = []; + var membertype = 0; + var isGENERATOR = false; + var isDYNAMICSCHEMA = false; + var description; + var id = null; + var groups = []; + + if (_flags) { + !flags && (flags = []); + _flags.forEach(flag => flags.indexOf(flag) === -1 && flags.push(flag)); + } + + if (flags) { + + tmp = []; + var count = 0; + + for (var i = 0; i < flags.length; i++) { + + var tt = typeof(flags[i]); + + if (tt === 'number') { + timeout = flags[i]; + continue; + } + + if (tt === 'object') { + options = flags[i]; + continue; + } + + flags[i] = flags[i].replace(/\t/g, ' '); + + var first = flags[i][0]; + if (first === '&') { + groups.push(flags[i].substring(1).trim()); + continue; + } + + // ROUTE identificator + if (flags[i].substring(0, 3) === 'id:') { + id = flags[i].substring(3).trim(); + continue; + } + + if (first === '#') { + !middleware && (middleware = []); + middleware.push(flags[i].substring(1).trim()); + continue; + } + + if (first === '*') { + + workflow = flags[i].trim().substring(1); + index = workflow.indexOf('-->'); + + if (index !== -1) { + schema = workflow.substring(0, index).trim(); + workflow = workflow.substring(index + 3).trim(); + } else { + schema = workflow; + workflow = null; + } + + schema = schema.replace(/\\/g, '/').split('/').trim(); + + 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; + } + + // Comment + if (flags[i].substring(0, 3) === '// ') { + description = flags[i].substring(3).trim(); + continue; + } + + var flag = flags[i].toString().toLowerCase(); + if (flag.startsWith('http://') || flag.startsWith('https://')) { + corsflags.push(flag); + continue; + } + + count++; + + switch (flag) { + + case 'json': + isJSON = true; + continue; + + case 'delay': + count--; + isDELAY = true; + continue; + + case 'binary': + isBINARY = true; + continue; + + case 'cors': + isCORS = true; + count--; + continue; + + case 'credential': + case 'credentials': + corsflags.push(flag); + count--; + continue; + + case 'sync': + case 'yield': + case 'synchronize': + isGENERATOR = true; + count--; + continue; + + case 'novalidate': + novalidate = true; + break; + + case 'noxhr': + case '-xhr': + isNOXHR = true; + continue; + case 'raw': + isRaw = true; + tmp.push(flag); + break; + case 'mobile': + isMOBILE = true; + break; + case 'robot': + isROBOT = true; + F._request_check_robot = true; + break; + case 'authorize': + case 'authorized': + case 'logged': + membertype = 1; + priority += 2; + tmp.push('authorize'); + break; + case 'unauthorize': + case 'unauthorized': + case 'unlogged': + membertype = 2; + priority += 2; + tmp.push('unauthorize'); + break; + case 'referer': + case 'referrer': + tmp.push('referer'); + break; + case 'delete': + case 'get': + case 'head': + case 'options': + case 'patch': + case 'post': + case 'propfind': + case 'put': + case 'trace': + tmp.push(flag); + method += (method ? ',' : '') + flag; + corsflags.push(flag); + PERF[flag.toUpperCase()] = true; + PERF[flag] = true; + break; + default: + if (flag[0] === '@') + isROLE = true; + 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 { + flags = ['get']; + 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, workflow) { + var themeName = U.parseTheme(name); + if (themeName) + name = prepare_viewname(name); + return function(id) { + if (language && !this.language) + this.language = language; + sitemap && this.sitemap(sitemap.id, language); + if (name[0] === '~') + this.themeName = ''; + else if (themeName) + this.themeName = themeName; + if (!this.route.workflow) + return this.view(name); + + var self = this; + if (this.route.workflow instanceof Object) { + workflow.view = name; + if (workflow.id instanceof Array) + controller_json_workflow_multiple.call(self, id); + else + 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, 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); + + index = viewname.lastIndexOf('/'); + if (index !== -1) + viewname = viewname.substring(index + 1); + + if (!viewname || viewname === '/') + viewname = 'index'; + + funcExecute = (function(name, sitemap, language) { + return function(id) { + var self = this; + + 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 + 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 = workflow.id instanceof Array ? controller_json_workflow_multiple : controller_json_workflow; + } + + if (!isGENERATOR) + isGENERATOR = (funcExecute.constructor.name === 'GeneratorFunction' || funcExecute.toString().indexOf('function*') === 0); + + var url2 = framework_internal.preparePath(url.trim()); + var urlraw = U.path(url2) + (isWILDCARD ? '*' : ''); + var hash = url2.hash(); + var routeURL = framework_internal.routeSplitCreate(url2); + var arr = []; + var params = []; + var reg = null; + var regIndex = null; + var dynamicidindex = -1; + + if (url.indexOf('{') !== -1) { + routeURL.forEach(function(o, i) { + if (o.substring(0, 1) !== '{') + return; + + arr.push(i); + + 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] !== '/') + return; + + var index = sub.lastIndexOf('/'); + if (index === -1) + return; + + if (!reg) { + reg = {}; + regIndex = []; + } + + params[params.length - 1] = 'regexp' + (regIndex.length + 1); + reg[i] = new RegExp(sub.substring(1, index), sub.substring(index + 1)); + regIndex.push(i); + }); + + priority -= arr.length + 1; + } + + if (url.indexOf('#') !== -1) + priority -= 100; + + 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'; + priority++; + } + + if (flags.indexOf('upload') !== -1) { + if (flags.indexOf('post') === -1 && flags.indexOf('put') === -1) { + flags.push('post'); + method += (method ? ',' : '') + 'post'; + } + } + + if (flags.indexOf('get') === -1 && flags.indexOf('options') === -1 && flags.indexOf('post') === -1 && flags.indexOf('delete') === -1 && flags.indexOf('put') === -1 && flags.indexOf('upload') === -1 && flags.indexOf('head') === -1 && flags.indexOf('trace') === -1 && flags.indexOf('patch') === -1 && flags.indexOf('propfind') === -1) { + flags.push('get'); + method += (method ? ',' : '') + 'get'; + } + + if (CONF.allow_head && flags.indexOf('get') !== -1) { + flags.append('head'); + method += (method ? ',' : '') + 'head'; + } + + if (flags.indexOf('referer') !== -1) + F._request_check_referer = true; + + if (!F._request_check_POST && (flags.indexOf('delete') !== -1 || flags.indexOf('post') !== -1 || flags.indexOf('put') !== -1 || flags.indexOf('upload') !== -1 || flags.indexOf('json') !== -1 || flags.indexOf('patch') !== -1 || flags.indexOf('options') !== -1)) + F._request_check_POST = true; + + var isMULTIPLE = false; + + if (method.indexOf(',') !== -1) + isMULTIPLE = true; + + if (method.indexOf(',') !== -1 || method === '') + method = undefined; + else + method = method.toUpperCase(); + + if (name[1] === '#') + name = name.substring(1); + + if (isBINARY && !isRaw) { + isBINARY = false; + 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.trim(); + r.groups = flags_to_object(groups); + r.priority = priority; + r.sitemap = sitemap ? sitemap.id : ''; + r.schema = schema; + r.novalidate = novalidate; + r.workflow = workflow; + r.subdomain = subdomain; + r.description = description; + r.controller = _controller ? _controller : 'unknown'; + r.owner = _owner; + 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 || CONF.default_request_maxlength) * 1024; + r.middleware = middleware; + r.timeout = timeout === undefined ? (isDELAY ? 0 : CONF.default_request_timeout) : timeout; + r.isGET = flags.indexOf('get') !== -1; + r.isMULTIPLE = isMULTIPLE; + r.isJSON = isJSON; + r.isXML = flags.indexOf('xml') !== -1; + r.isRAW = isRaw; + r.isBINARY = isBINARY; + r.isMOBILE = isMOBILE; + r.isROBOT = isROBOT; + r.isMOBILE_VARY = isMOBILE; + r.isGENERATOR = isGENERATOR; + r.MEMBER = membertype; + r.isWILDCARD = isWILDCARD; + r.isROLE = isROLE; + r.isREFERER = flags.indexOf('referer') !== -1; + r.isHTTPS = flags.indexOf('https') !== -1; + r.isHTTP = flags.indexOf('http') !== -1; + r.isDEBUG = flags.indexOf('debug') !== -1; + r.isRELEASE = flags.indexOf('release') !== -1; + r.isBOTH = isNOXHR ? false : true; + r.isXHR = flags.indexOf('xhr') !== -1; + r.isUPLOAD = flags.indexOf('upload') !== -1; + r.isSYSTEM = url.startsWith('/#'); + 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; + if (r.isJSON) + PERF.json = true; + if (r.isXML) + PERF.xml = true; + if (r.isBINARY) + PERF.binary = true; + if (r.MEMBER === 1) + PERF.auth = true; + if (r.MEMBER === 2) + PERF.unauth = true; + + var arr = method ? method.split(',') : EMPTYARRAY; + for (var i = 0; i < arr.length; i++) { + PERF[arr[i]] = true; + PERF[arr[i].toLowerCase()] = true; + } + + if (r.isSYSTEM) + F.routes.system[url.substring(1)] = r; + else { + F.routes.web.push(r); + + // Appends cors route + isCORS && F.cors(urlcache, corsflags); + !_controller && F.$routesSort(1); + } + + if (isMOBILE) + F._request_check_mobile = true; + + EMIT('route', 'web', instance); + return instance; +}; + +function flags_to_object(flags) { + var obj = {}; + flags.forEach(flag => obj[flag] = true); + return obj; +} - if (self.isCoffee) - require('coffee-script').eval(data) - else - eval(data); - }); - } +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 = {}; + } +} - return self; -}; +/** + * Get routing by name + * @param {String} name + * @return {Object} + */ +global.ROUTING = F.routing = function(name, flags) { -/* - Inject configuration from URL - @url {String} - @debug {Boolean} :: optional, is debug configuration - @rewrite {Boolean} :: optional (default true), rewrite all values or append new values only - return {Framework} -*/ -Framework.prototype.injectConfig = function(url, debug, rewrite) { + var id = name.substring(0, 3) === 'id:' ? name.substring(3) : null; + if (id) + name = null; - var self = this; + 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(' '); - if (typeof(debug) !== UNDEFINED && self.config.debug !== debug) - return self; + for (var i = 0, length = F.routes.web.length; i < length; i++) { + var route = F.routes.web[i]; + 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 (typeof(rewrite) === UNDEFINED) - rewrite = true; + if (!is) + continue; - utils.request(url, 'GET', '', function(error, data) { + var url = U.path(route.url.join('/')); + if (url[0] !== '/') + url = '/' + url; - if (error) { - self.error(error, 'injectConfig - ' + url, null); - return; - } + return route; + } +}; - self.configure(data.split('\n'), rewrite); +/** + * Merge files + * @param {String} url Relative URL. + * @param {String/String Array} file1 Filename or URL. + * @param {String/String Array} file2 Filename or URL. + * @param {String/String Array} file3 Filename or URL. + * @param {String/String Array} fileN Filename or URL. + * @return {Framework} + */ +global.MERGE = F.merge = function(url) { - }); + F.temporary.other['merge_' + url] = 1; - return self; -}; + if (url[0] === '#') + url = sitemapurl(url.substring(1)); -/* - Inject versions mapping - @url {String} - @rewrite {Boolean} :: optional (default true), rewrite all values or append (+ rewrite old) values (default false) - return {Framework} -*/ -Framework.prototype.injectVersions = function(url, rewrite) { + url = F.$version(framework_internal.preparePath(url)); - var self = this; + if (url === 'auto') { + // auto-generating + var arg = arguments; + setTimeout(function(arg) { + F.merge.apply(F, arg); + }, 500, arg); + return F; + } - if (typeof(rewrite) === UNDEFINED) - rewrite = false; + var arr = []; - utils.request(url, 'GET', '', function(error, data) { + for (var i = 1, length = arguments.length; i < length; i++) { - if (error) { - self.error(error, 'injectVersions - ' + url, null); - return; - } + var items = arguments[i]; + if (!(items instanceof Array)) + items = [items]; - self.configureMapping(data, rewrite); + for (var j = 0, lengthsub = items.length; j < lengthsub; j++) { + var fn = items[j]; + var c = fn[0]; + if (c === '@') + fn = '~' + F.path.package(fn.substring(1)); + else if (c === '=') + fn = '~' + F.path.themes(fn.substring(1)); + else if (c === '#') + fn = '~' + F.path.temp('isomorphic_' + fn.substring(1) + '.min.js'); + arr.push(fn); + } + } - }); + if (url[0] !== '/') + url = '/' + url; - return self; + 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; }; -/* - Inject module from URL - @name {String} :: name of module - @url {String} - return {Framework} -*/ -Framework.prototype.injectModule = function(name, url) { +F.mapping = function() { + return F.map.apply(F, arguments); +}; - var self = this; - var framework = self; +/** + * Send message + * @param {Object} message + * @param {Object} handle + * @return {Framework} + */ +F.send = function(message, handle) { + process.send(message, handle); + return F; +}; - utils.request(url, 'GET', '', function(error, data) { +/** + * Mapping of static file + * @param {String} url + * @param {String} filename Filename or Directory. + * @param {Function(filename) or String Array} filter + * @return {Framework} + */ +global.MAP = F.map = function(url, filename, filter) { + + if (url[0] === '#') + url = sitemapurl(url.substring(1)); + + if (url[0] !== '/') + url = '/' + url; + + var isPackage = false; + + filename = U.$normalize(filename); + url = framework_internal.preparePath(F.$version(url)); + + // isomorphic + if (filename[0] === '#') { + F.owners.push({ type: 'mapping', owner: _owner, id: url }); + F.routes.mapping[url] = F.path.temp('isomorphic_' + filename.substring(1) + '.min.js'); + return F; + } + + var index = filename.indexOf('#'); + var block; + + if (index !== -1) { + var tmp = filename.split('#'); + filename = tmp[0]; + block = tmp[1]; + } + + var c = filename[0]; + + // package + if (c === '@') { + filename = F.path.package(filename.substring(1)); + isPackage = true; + } else if (c === '=') { + if (F.isWindows) + filename = U.combine(CONF.directory_themes, filename.substring(1)); + else + filename = F.path.themes(filename.substring(1)); + isPackage = true; + } + + var isFile = U.getExtension(filename).length > 0; + + // Checks if the directory exists + if (!isPackage && !filename.startsWith(directory)) { + var tmp = filename[0] === '~' ? F.path.root(filename.substring(1)) : F.path.public(filename); + if (existsSync(tmp)) + filename = tmp; + } + + if (isFile) { + F.routes.mapping[url] = filename; + F.owners.push({ type: 'mapping', owner: _owner, id: url }); + if (block) { + F.owners.push({ type: 'blocks', owner: _owner, id: url }); + F.routes.blocks[url] = block; + } + return F; + } + + url = U.path(url); + filename = U.path(filename); + + var replace = filename; + var plus = ''; + var isRoot = false; + + if (replace[0] === '/') + isRoot = true; + + if (replace[0] === '~') { + plus += '~'; + replace = replace.substring(1); + } + + if (replace[0] === '.') { + plus += '.'; + replace = replace.substring(1); + } + + if (!isRoot && replace[0] === '/') { + plus += '/'; + replace = replace.substring(1); + } + + if (filter instanceof Array) { + for (var i = 0, length = filter.length; i < length; i++) { + if (filter[i][0] === '.') + filter[i] = filter[i].substring(1); + filter[i] = filter[i].toLowerCase(); + } + } + + setTimeout(function() { + U.ls(F.isWindows ? filename.replace(/\//g, '\\') : filename, function(files) { + for (var i = 0, length = files.length; i < length; i++) { + + if (F.isWindows) + files[i] = files[i].replace(filename, '').replace(/\\/g, '/'); + + var file = files[i].replace(replace, ''); + + if (filter) { + if (typeof(filter) === 'function') { + if (!filter(file)) + continue; + } else { + if (filter.indexOf(U.getExtension(file)) === -1) + continue; + } + } + + if (file[0] === '/') + file = file.substring(1); + + var key = url + file; + F.routes.mapping[key] = plus + files[i]; + F.owners.push({ type: 'mapping', owner: _owner, id: key }); + + if (block) { + F.owners.push({ type: 'blocks', owner: _owner, id: key }); + F.routes.blocks[key] = block; + } + } + + }); + }, isPackage ? 500 : 1); + + return F; +}; - if (error) { - self.error(error, 'injectModule - ' + name, null); - return; - } +/** + * Add a middleware + * @param {String} name + * @param {Function(req, res, next, options)} funcExecute + * @return {Framework} + */ +global.MIDDLEWARE = F.middleware = function(name, funcExecute) { + F.install('middleware', name, funcExecute); + _owner && F.owners.push({ type: 'middleware', owner: _owner, id: name }); + return F; +}; - try { - var result = eval('(new (function(){var module = this;var exports = {};this.exports=exports;' + data + '})).exports'); - _controller = '#module-' + name; +/** + * Uses middleware + * @name {String or String Array} name + * @url {String} url A url address (optional) + * @types {String Array} It can be `web`, `file` or `websocket` + * @first {Boolean} Optional, add a middleware as first + * @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++) + F.routes.request.push(name[i]); + } else + F.routes.request.push(name); + F._length_request_middleware = F.routes.request.length; + return F; + } + + if (url instanceof Array) { + types = url; + url = null; + } + + if (url === '*') + url = null; + + var route; + + if (url) + url = framework_internal.routeSplitCreate(framework_internal.preparePath(url.trim())).join('/'); + + if (!types || types.indexOf('web') !== -1) { + for (var i = 0, length = F.routes.web.length; i < length; i++) { + route = F.routes.web[i]; + if (url && !route.url.join('/').startsWith(url)) + continue; + !route.middleware && (route.middleware = []); + merge_middleware(route.middleware, name, first); + } + } + + if (!types || types.indexOf('file') !== -1 || types.indexOf('files') !== -1) { + for (var i = 0, length = F.routes.files.length; i < length; i++) { + route = F.routes.files[i]; + if (url && !route.url.join('/').startsWith(url)) + continue; + !route.middleware && (route.middleware = []); + merge_middleware(route.middleware, name, first); + } + } + + if (!types || types.indexOf('websocket') !== -1 || types.indexOf('websockets') !== -1) { + for (var i = 0, length = F.routes.websockets.length; i < length; i++) { + route = F.routes.websockets[i]; + if (url && !route.url.join('/').startsWith(url)) + continue; + !route.middleware && (route.middleware = []); + merge_middleware(route.middleware, name, first); + } + } + + return F; +}; + +function merge_middleware(a, b, first) { + + if (typeof(b) === 'string') + b = [b]; + + for (var i = 0, length = b.length; i < length; i++) { + var index = a.indexOf(b[i]); + if (index === -1) { + if (first) + a.unshift(b[i]); + else + a.push(b[i]); + } + } + + return a; +} - self.routes.web = self.routes.web.remove(function(route) { - return route.name === _controller; - }); +/** + * Add a new websocket route + * @param {String} url + * @param {Function()} funcInitialize + * @param {String Array} flags Optional. + * @param {String Array} protocols Optional, framework compares this array with request protocol (http or https) + * @param {String Array} allow Optional, framework compares this array with "origin" request header + * @param {Number} length Optional, maximum message length. + * @param {String Array} middleware Optional, middlewares. + * @param {Object} options Optional, additional options for middleware. + * @return {Framework} + */ +global.WEBSOCKET = F.websocket = function(url, funcInitialize, flags, length) { + + var tmp; + + var CUSTOM = typeof(url) === 'function' ? url : null; + if (CUSTOM) + url = '/'; + + if (url[0] === '#') { + + var index = url.indexOf('/'); + if (index !== -1) { + tmp = url.substring(index); + url = url.substring(0, index); + } + + url = url.substring(1); + var sitemap = F.sitemap(url, true); + if (sitemap) { + url = sitemap.url; + if (tmp) + url += url[url.length - 1] === '/' ? tmp.substring(1) : tmp; + else if (sitemap.wildcard) + url += '*'; + } else + 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 = '/'; + + // Unicode encoding + url = framework_internal.encodeUnicodeURL(url); + + var priority = 0; + var index = url.indexOf(']'); + var subdomain = null; + var middleware; + var allow; + var options; + var protocols; + var id; + var groups = []; + + priority = url.count('/'); + + if (index > 0) { + subdomain = url.substring(1, index).trim().toLowerCase().split(','); + url = url.substring(index + 1); + priority += subdomain.indexOf('*') !== -1 ? 50 : 100; + } + + var isWILDCARD = url.indexOf('*') !== -1; + if (isWILDCARD) { + url = url.replace('*', '').replace('//', '/'); + priority = (-10) - priority; + } + + var url2 = framework_internal.preparePath(url.trim()); + var routeURL = framework_internal.routeSplitCreate(url2); + var arr = []; + var reg = null; + var regIndex = null; + var hash = url2.hash(); + var urlraw = U.path(url2) + (isWILDCARD ? '*' : ''); + var params = []; + + if (url.indexOf('{') !== -1) { + routeURL.forEach(function(o, i) { + + if (o.substring(0, 1) !== '{') + return; + + arr.push(i); + + var sub = o.substring(1, o.length - 1); + var name = o.substring(1, o.length - 1).trim(); + + params.push(name); + + if (sub[0] !== '/') + return; + + var index = sub.lastIndexOf('/'); + if (index === -1) + return; + + if (!reg) { + reg = {}; + regIndex = []; + } + + params[params.length - 1] = 'regexp' + (regIndex.length + 1); + reg[i] = new RegExp(sub.substring(1, index), sub.substring(index + 1)); + regIndex.push(i); + }); + } + + if (typeof(allow) === 'string') + allow = allow[allow]; + + if (typeof(protocols) === 'string') + protocols = protocols[protocols]; + + tmp = []; + + var isJSON = false; + var isBINARY = false; + var isROLE = false; + var isBUFFER = false; + var count = 0; + var membertype = 0; + + !flags && (flags = []); + _flags && _flags.forEach(flag => flags.indexOf(flag) === -1 && flags.push(flag)); + + for (var i = 0; i < flags.length; i++) { + + var flag = flags[i]; + var type = typeof(flag); + + // Middleware options + if (type === 'object') { + options = flag; + continue; + } + + // Length + if (type === 'number') { + length = flag; + continue; + } + + if (flag.substring(0, 3) === 'id:') { + id = flag.substring(3).trim(); + continue; + } + + // Groups + if (flag[0] === '&') { + groups.push(flag.substring(1).trim()); + continue; + } + + // Middleware + if (flag[0] === '#') { + !middleware && (middleware = []); + middleware.push(flag.substring(1).trim()); + continue; + } + + flag = flag.toString().toLowerCase(); + + // Origin + if (flag.startsWith('http://') || flag.startsWith('https://')) { + !allow && (allow = []); + allow.push(flag); + continue; + } + + count++; + + if (flag === 'json') + isJSON = true; + + if (flag === 'binary') + isBINARY = true; + + if (flag === 'raw') { + isBINARY = false; + isJSON = false; + } + + if (flag === 'buffer') + isBUFFER = true; + + if (flag[0] === '@') { + isROLE = true; + tmp.push(flag); + continue; + } + + if (flag === 'json' || flag === 'binary' || flag === 'raw') + continue; + + switch (flag) { + case 'authorize': + case 'authorized': + case 'logged': + membertype = 1; + priority++; + tmp.push('authorize'); + break; + case 'unauthorize': + case 'unauthorized': + case 'unlogged': + membertype = 2; + priority++; + tmp.push('unauthorize'); + break; + case 'get': + case 'http': + case 'https': + case 'debug': + case 'release': + tmp.push(flag); + break; + default: + !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'); + priority += (count * 2); + + if (subdomain) + F._length_subdomain_websocket++; + + var instance = new FrameworkRoute(); + var r = instance.route; + 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; + r.paramnames = params.length ? params : null; + r.param = arr; + r.subdomain = subdomain; + r.priority = priority; + r.flags = flags || EMPTYARRAY; + r.flags2 = flags_to_object(flags); + r.onInitialize = funcInitialize; + r.protocols = protocols || EMPTYARRAY; + r.allow = allow || []; + 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; + r.isHTTPS = flags.indexOf('https'); + r.isHTTP = flags.indexOf('http'); + r.isDEBUG = flags.indexOf('debug'); + r.isRELEASE = flags.indexOf('release'); + r.CUSTOM = CUSTOM; + r.middleware = middleware ? middleware : null; + r.options = options; + r.isPARAM = arr.length > 0; + r.regexp = reg; + r.regexpIndexer = regIndex; + r.type = 'websocket'; + F.routes.websockets.push(r); + F.initwebsocket && F.initwebsocket(); + EMIT('route', 'websocket', r); + !_controller && F.$routesSort(2); + return instance; +}; + +F.initwebsocket = function() { + if (F.routes.websockets.length && CONF.allow_websocket && F.server) { + F.server.on('upgrade', F.$upgrade); + F.initwebsocket = null; + } +}; - self.routes.files = self.routes.files.remove(function(route) { - return route.name === _controller; - }); +/** + * Create a file route + * @param {String} name + * @param {Function} funcValidation + * @param {Function} fnExecute + * @param {String Array} middleware + * @return {Framework} + */ +global.FILE = F.file = function(fnValidation, fnExecute, flags) { + + var a; + + if (fnValidation instanceof Array) { + a = fnExecute; + var b = flags; + flags = fnValidation; + fnValidation = a; + fnExecute = b; + } else if (fnExecute instanceof Array) { + a = fnExecute; + fnExecute = flags; + flags = a; + } + + if (!fnExecute && fnValidation) { + fnExecute = fnValidation; + fnValidation = undefined; + } + + var extensions; + var middleware; + var options; + var url; + var wildcard = false; + var fixedfile = false; + var id = null; + var urlraw = fnValidation; + var groups = []; + + if (_flags) { + !flags && (flags = []); + _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') + options = flag; + else if (flag[0] === '&') + groups.push(flag.substring(1).trim()); + else if (flag[0] === '#') { + !middleware && (middleware = []); + 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(); + } + } + + if (typeof(fnValidation) === 'string') { + + if (fnValidation === '/') + fnValidation = ''; + + url = fnValidation ? framework_internal.routeSplitCreate(fnValidation) : EMPTYARRAY; + fnValidation = undefined; + a = url.last(); + if (a === '*.*') { + wildcard = true; + url.splice(url.length - 1, 1); + } else if (a) { + var index = a.indexOf('*.'); + if (index !== -1) { + extensions = {}; + extensions[a.substring(index + 2).trim()] = true; + wildcard = false; + url.splice(url.length - 1, 1); + } else if (a === '*') { + wildcard = true; + url.splice(url.length - 1, 1); + } else if (U.getExtension(a)) { + fixedfile = true; + wildcard = false; + } + } + } else if (!extensions && !fnValidation) + fnValidation = fnExecute; + + var instance = new FrameworkRoute(); + 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; + r.fixedfile = fixedfile; + r.wildcard = wildcard; + r.extensions = extensions; + r.onValidate = fnValidation; + r.execute = fnExecute; + r.middleware = middleware; + r.options = options; + r.type = 'file'; + + 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); + 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('/'); + var tmp; + + if (index !== -1) { + tmp = url.substring(index); + url = url.substring(0, index); + } + + var sitemap = F.sitemap(url, true, ''); + if (sitemap) { + url = sitemap.url; + if (tmp) { + if (url[url.length - 1] === '/') + url += tmp.substring(1); + else + url += tmp; + } + } + + return url; +} - self.routes.websockets = self.routes.websockets.remove(function(route) { - return route.name === _controller; - }); +global.LOCALIZE = F.localize = function(url, flags, minify) { - if (typeof(result.install) !== UNDEFINED) { - result.install(self, name); - self._routeSort(); - } + if (typeof(url) === 'function') { + F.onLocale = url; + return; + } - self.modules[name] = result; - _controller = ''; + if (url[0] === '#') + url = sitemapurl(url.substring(1)); - } catch (ex) { - self.error(ex, 'injectModule - ' + name, null); - } - }); + url = url.replace('*.*', ''); - return self; -}; + if (minify == null) + minify = true; -/* - Inject model from URL - @name {String} :: name of model - @url {String} - return {Framework} -*/ -Framework.prototype.injectModel = function(name, url) { + if (flags === true) { + flags = []; + minify = true; + } else if (!flags) + flags = []; - var self = this; - var framework = self; + var index; + var ext = false; - utils.request(url, 'GET', '', function(error, data) { + 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'; + }); - if (error) { - self.error(error, 'injectModel - ' + name, null); - return; - } + var index = url.lastIndexOf('.'); - try { - var result = eval('(new (function(){var module = this;var exports = {};this.exports=exports;' + data + '})).exports'); - self.models[name] = result; + if (!ext) { + if (index === -1) + flags.push('.html', '.htm', '.md', '.txt'); + else { + flags.push(url.substring(index).toLowerCase()); + url = url.substring(0, index).replace('*', ''); + } + } - } catch (ex) { - self.error(ex, 'injectModel - ' + name, null); - } - }); + url = framework_internal.preparePath(url.replace('.*', '')); - return self; + if (minify) + F.file(url, F.$filelocalize, flags); + else + F.file(url, filelocalize_nominify, flags); }; -/* - Inject source from URL - @name {String} :: name of source - @url {String} - return {Framework} -*/ -Framework.prototype.injectSource = function(name, url) { - - var self = this; - var framework = self; - - utils.request(url, 'GET', '', function(error, data) { - - if (error) { - self.error(error, 'injectSource - ' + name, null); - return; - } +function filelocalize_nominify(req, res) { + F.$filelocalize(req, res, true); +} - try { - var result = eval('(new (function(){var module = this;var exports = {};this.exports=exports;' + data + '})).exports'); - self.sources[name] = result; +F.$filelocalize = function(req, res, nominify) { - } catch (ex) { - self.error(ex, 'injectSource - ' + name, null); - } - }); + // options.filename + // options.code + // options.callback + // options.headers + // options.download - return self; -}; -/* - Inject controller from URL - @name {String} :: name of controller - @url {String} - return {Framework} -*/ -Framework.prototype.injectController = function(name, url) { + F.onLocale && (req.$language = F.onLocale(req, res, req.isStaticFile)); - var self = this; + var key = 'locate_' + (req.$language ? req.$language : 'default') + '_' + (req.$key || req.url); + var output = F.temporary.other[key]; - utils.request(url, 'GET', '', function(error, data) { + 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; + } - if (error) { - self.error(error, 'injectController - ' + name, null); - return; - } + var filename = (res.options ? res.options.filename : null) || F.onMapping(req.uri.pathname, req.uri.pathname, true, true); - try { - var result = eval('(new (function(framework){var module = this;var exports = {};this.exports=exports;' + data + '})).exports'); - _controller = name; + Fs.readFile(filename, function(err, content) { - self.routes.web = self.routes.web.remove(function(route) { - return route.name === _controller; - }); + if (err) { + if (!F.routes.filesfallback || !F.routes.filesfallback(req, res)) + res.throw404(); + return; + } - self.routes.files = self.routes.files.remove(function(route) { - return route.name === _controller; - }); + content = framework_internal.markup(F.translator(req.$language, framework_internal.modificators(content.toString(ENCODING), filename, 'static')), filename); - self.routes.websockets = self.routes.websockets.remove(function(route) { - return route.name === _controller; - }); + Fs.lstat(filename, function(err, stats) { - if (typeof(result.install) !== UNDEFINED) { - result.install(self, name); - self._routeSort(); - } + var mtime = stats.mtime.toUTCString(); - self.controllers[name] = result; - _controller = ''; + if (CONF.allow_compile_html && CONF.allow_compile && !nominify && (req.extension === 'html' || req.extension === 'htm')) + content = framework_internal.compile_html(content, filename, true); - } catch (ex) { - self.error(ex, 'injectController - ' + name, null); - } - }); + if (RELEASE) { + F.temporary.other[key] = Buffer.from(content); + F.temporary.other[key].$mtime = mtime; + if (F.$notModified(req, res, mtime)) + return; + } - return self; + HEADERS.responseLocalize['Last-Modified'] = mtime; + res.options.body = content; + res.options.type = U.getContentType(req.extension); + res.options.headers = HEADERS.responseLocalize; + res.$text(); + }); + }); }; -/* - Inject definition from URL - @url {String} - return {Framework} -*/ -Framework.prototype.injectDefinition = function(url) { +F.$notModified = function(req, res, date) { + if (date === req.headers['if-modified-since']) { + HEADERS.responseNotModified['Last-Modified'] = date; + res.success = true; + res.writeHead(304, HEADERS.responseNotModified); + res.end(); + F.stats.response.notModified++; + F.reqstats(false, req.isStaticFile); + return true; + } +}; - var self = this; - var framework = self; +/** + * Error caller + * @param {Error} err + * @param {String} name Controller or Script name. + * @param {Object} uri + * @return {Framework} + */ +F.error = function(err, name, uri) { - utils.request(url, 'GET', '', function(error, data) { + if (!arguments.length) + return F.errorcallback; - if (error) { - self.error(error, 'injectDefinition - ' + url, null); - return; - } + if (!err) + return F; - try { - eval(data); - } catch (ex) { - self.error(ex, 'injectDefinition - ' + url, null); - } - }); + if (F.errors) { + 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(); + } - return self; + F.onError(err, name, uri); + return F; }; -/* - Inject definition from URL - @url {String} - return {Framework} -*/ -Framework.prototype.injectComponent = function(name, url) { +F.errorcallback = function(err) { + err && F.error(err); +}; - var self = this; - var framework = self; +/** + * Registers a new problem + * @param {String} message + * @param {String} name A controller name. + * @param {String} uri + * @param {String} ip + * @return {Framework} + */ +F.problem = F.wtf = function(message, name, uri, ip) { - utils.request(url, 'GET', '', function(error, data) { + // OBSOLETE('F.problem()', 'This method will be removed in v4'); - if (error) { - self.error(error, 'injectComponent - ' + name, null); - return; - } + F.$events.problem && EMIT('problem', message, name, uri, ip); - try { - var result = eval('(new (function(){var module = this;var exports = {};this.exports=exports;' + data + '})).exports'); + if (message instanceof framework_builders.ErrorBuilder) + message = message.plain(); + else if (typeof(message) === 'object') + message = JSON.stringify(message); - if (typeof(result.install) !== UNDEFINED) - result.install(self, name); + var obj = { message: message, name: name, url: uri ? typeof(uri) === 'string' ? uri : Parser.format(uri) : undefined, ip: ip }; + F.logger('problems', obj.message, 'url: ' + obj.url, 'source: ' + obj.name, 'ip: ' + obj.ip); - self.components[name] = result; + if (F.problems) { + F.problems.push(obj); + F.problems.length > 50 && F.problems.shift(); + } - } catch (ex) { - self.error(ex, 'injectComponent - ' + name, null); - } - }); + return F; +}; - return self; +global.PRINTLN = function(msg) { + console.log('------>', '[' + new Date().format('yyyy-MM-dd HH:mm:ss') + ']', msg); }; /** - * Eval code - * @see {@link http://docs.totaljs.com/Framework/#framework.eval|Documentation} - * @param {String or Function} script Function to eval or Code or URL address. + * Registers a new change + * @param {String} message + * @param {String} name A source name. + * @param {String} uri + * @param {String} ip * @return {Framework} */ -Framework.prototype.eval = function(script) { +F.change = function(message, name, uri, ip) { - var self = this; - var framework = self; + OBSOLETE('F.change()', 'This method will be removed in v4.'); - if (typeof(script) === FUNCTION) { - try { - eval('(' + script.toString() + ')()'); - } catch (ex) { - self.error(ex, 'eval - ' + script.toString(), null); - } - return self; - } + F.$events.change && EMIT('change', message, name, uri, ip); - if ((script.startsWith('http://', true) || script.startsWith('https://', true)) && scripts.trim().indexOf('\n') === -1) { - utils.request(script, 'GET', '', function(err, data) { + if (message instanceof framework_builders.ErrorBuilder) + message = message.plain(); + else if (typeof(message) === 'object') + message = JSON.stringify(message); - if (!err) { - // recursive calling - self.eval(data.toString()); - return; - } - self.error(err); - }); - } + var obj = { message: message, name: name, url: uri ? typeof(uri) === 'string' ? uri : Parser.format(uri) : undefined, ip: ip }; + F.logger('changes', obj.message, 'url: ' + obj.url, 'source: ' + obj.name, 'ip: ' + obj.ip); - try { - eval(script); - } catch (ex) { - self.error(ex, 'eval - ' + script, null); - } + if (F.changes) { + F.changes.push(obj); + F.changes.length > 50 && F.changes.shift(); + } - return self; + return F; }; -/* - Error Handler - @err {Error} - @name {String} :: name of Controller (optional) - @uri {Uri} :: optional -*/ -Framework.prototype.onError = function(err, name, uri) { - console.log(err.toString(), err.stack); - console.log('--------------------------------------------------------------------'); - return this; -}; +/** + * Trace + * @param {String} message + * @param {String} name A controller name. + * @param {String} uri + * @param {String} ip + * @return {Framework} + */ +global.TRACE = F.trace = function(message, name, uri, ip) { -/* - Pre-request handler - @req {ServerRequest} - @res {ServerResponse} - return {Boolean} -*/ -Framework.prototype.onRequest = null; + OBSOLETE('TRACE()', 'This method will be removed in v4.'); -/* - Authorization handler - @req {ServerRequest} - @res {ServerResponse} OR {WebSocketClient} - @flags {String array} - @callback {Function} - @callback(Boolean), true is [authorize]d and false is [unauthorize]d -*/ -Framework.prototype.onAuthorization = null; + if (!CONF.trace) + return F; -/* - Prefix delegate - @req {ServerRequest} - return {String} :: return prefix (default return empty string) -*/ -Framework.prototype.onPrefix = null; + F.$events.trace && EMIT('trace', message, name, uri, ip); -/* - Versioning static files (this delegate call LESS CSS by the background property) - @name {String} :: name of static file (style.css or script.js) - return {String} :: return new name of static file (style-new.css or script-new.js) -*/ -Framework.prototype.onVersion = null; + if (message instanceof framework_builders.ErrorBuilder) + message = message.plain(); + else if (typeof(message) === 'object') + message = JSON.stringify(message); -/* - Route validator / Request restriction - @req {ServerRequest} - @res {ServerResponse} - return {Boolean} -*/ -Framework.prototype.onRoute = null; + 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); -/* - Global framework validation - @name {String} - @value {String} - return {Boolean or utils.isValid() or StringErrorMessage}; -*/ -Framework.prototype.onValidation = null; + 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); + F.traces.length > 50 && F.traces.shift(); + } + + return F; +}; /** - * Mail handler - * @type {Function(address, subject, body, callback)} + * Get a module + * @param {String} name + * @return {Object} */ -Framework.prototype.onMail = function(address, subject, body, callback) { +global.MODULE = F.module = function(name) { + return F.modules[name] || null; +}; - var message = Mail.create(subject, body); +/** + * Add a new modificator + * @param {Function(type, filename, content)} fn The `fn` must return modified value. + * @return {Framework} + */ +global.MODIFY = F.modify = function(filename, fn) { - if (address instanceof Array) { - var length = address.length; - for (var i = 0; i < length; i++) - message.to(address[i]); - } else - message.to(address); + if (typeof(filename) === 'function') { + fn = filename; + filename = null; + } - var self = this; + 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); + } - message.from(self.config['mail.address.from'] || '', self.config['name']); + fn.$owner = _owner; + return F; +}; - var tmp = self.config['mail.address.reply']; +F.$bundle = function(callback) { - if (tmp && tmp.length > 0 && tmp.isEmail()) - message.reply(self.config['mail.address.reply']); + var bundledir = F.path.root(CONF.directory_bundles); - tmp = self.config['mail.address.copy']; + var makebundle = function() { - if (tmp && tmp.length > 0 && tmp.isEmail()) - message.bcc(tmp); + var arr = Fs.readdirSync(bundledir); + var url = []; - var options = {}; - var opt = self.config['mail.smtp.options']; + for (var i = 0; i < arr.length; i++) { + if (arr[i].endsWith('.url')) + url.push(arr[i]); + } - if (opt && opt.isJSON()) - options = JSON.parse(opt); + url.wait(function(item, next) { - message.send(self.config['mail.smtp'], options, callback); + 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'); - return self; -}; + F.consoledebug('Download bundle: ' + link); -/* - Validate request data - @data {String} - return {Boolean} -*/ -Framework.prototype.onXSS = function(data) { + U.download(link, FLAGS_INSTALL, function(err, response) { - if (data === null || data.length === 0) - return false; + if (err) { + F.error(err, 'Bundle: ' + link); + next(); + return; + } + + var stream = Fs.createWriteStream(filename); - data = decodeURIComponent(data); - return (data.indexOf('<') !== -1 && data.lastIndexOf('>') !== -1); -}; + 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 = []; + var dir = ''; + + if (!targetdirectory) + targetdirectory = directory; + + targetdirectory = '~' + targetdirectory; + + function listing(directory, level, output, extension, isTheme) { + + if (!existsSync(dir)) + return; + + if (!extension) + extension = '.js'; + + Fs.readdirSync(directory).forEach(function(o) { + var isDirectory = Fs.statSync(Path.join(directory, o)).isDirectory(); + + if (isDirectory && isTheme) { + output.push({ name: o }); + return; + } + + if (isDirectory) { + + if (extension === '.package' && o.endsWith(extension)) { + var name = o.substring(0, o.length - extension.length); + output.push({ name: name[0] === '/' ? name.substring(1) : name, filename: Path.join(dir, o), is: true }); + return; + } + + level++; + listing(Path.join(directory, o), level, output, extension); + return; + } + + var ext = U.getExtension(o); + if (ext) + ext = '.' + ext; + 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 }); + }); + } + + try { + // Reads name of resources + F.temporary.internal.resources = Fs.readdirSync(F.path.resources()).map(n => n.substring(0, n.lastIndexOf('.'))); + } catch (e) { + F.temporary.internal.resources = []; + } + + 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 (can('modules')) { + operations.push(function(resume) { + 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))); + resume(); + }); + } + + if (can('isomorphic')) { + operations.push(function(resume) { + 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))); + resume(); + }); + } + + if (can('packages')) { + operations.push(function(resume) { + dir = U.combine(targetdirectory, isPackage ? '/packages/' : CONF.directory_packages); + arr = []; + listing(dir, 0, arr, '.package'); + var dirtmp = U.$normalize(dir); + + arr.wait(function(item, next2) { + + if (!item.is) { + dependencies.push(next => F.install('package', item.name, item.filename, undefined, undefined, undefined, true, undefined, undefined, next, packageName)); + return next2(); + } + + U.ls(item.filename, function(files, directories) { + var dir = F.path.temp(item.name) + '.package'; + !existsSync(dir) && Fs.mkdirSync(dir); + + for (var i = 0, length = directories.length; i < length; i++) { + var target = F.path.temp(U.$normalize(directories[i]).replace(dirtmp, '') + '/'); + !existsSync(target) && Fs.mkdirSync(target); + } + + files.wait(function(filename, 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() { + + // Windows sometimes doesn't load package and this delay solves the problem. + setTimeout(function() { + dependencies.push(next => F.install('package2', item.name, item.filename, undefined, undefined, undefined, true, undefined, undefined, next, packageName)); + next2(); + }, 50); + + }); + }); + }, resume); + }); + } + + if (can('models')) { + operations.push(function(resume) { + 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))); + resume(); + }); + } + + 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/' : CONF.directory_themes); + listing(dir, 0, arr, undefined, true); + arr.forEach(function(item) { + var themeName = item.name; + var themeDirectory = Path.join(dir, themeName); + var filename = Path.join(themeDirectory, 'index.js'); + F.themes[item.name] = U.path(themeDirectory); + F._length_themes++; + existsSync(filename) && dependencies.push(next => F.install('theme', item.name, filename, undefined, undefined, undefined, true, undefined, undefined, next, packageName)); + }); + resume(); + }); + } + + if (can('definitions')) { + operations.push(function(resume) { + 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))); + resume(); + }); + } + + if (can('controllers')) { + operations.push(function(resume) { + arr = []; + 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 (can('components')) { + operations.push(function(resume) { + arr = []; + 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'); + dependencies.async(function() { + types && types.indexOf('service') === -1 && F.cache.stop(); + F.$routesSort(); + (!types || types.indexOf('dependencies') !== -1) && F.$configure_dependencies(); + F.consoledebug('load dependencies {0}x (done)'.format(count)); + callback && callback(); + }); + }); + + 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(); + }); +} -/* - Render HTML for views - @argument {String params} +F.$startup = function(callback) { - this === controller + var dir = Path.join(directory, '/startup/'); - return {String} -*/ -Framework.prototype.onMeta = function() { - - var self = this; - var builder = ''; - var length = arguments.length; - - for (var i = 0; i < length; i++) { - - var arg = utils.encode(arguments[i]); - if (arg === null || arg.length === 0) - continue; - - switch (i) { - case 0: - builder += '' + (arg + (self.url !== '/' ? ' - ' + self.config['name'] : '')) + ''; - break; - case 1: - builder += ''; - break; - case 2: - builder += ''; - 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)); - builder += ''; - break; - } - } - - return builder; -}; + if (!existsSync(dir)) + return callback(); -// @arguments {Object params} -Framework.prototype.log = function() { + var run = []; - var self = this; - var 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; + Fs.readdirSync(dir).forEach(function(o) { + var extension = U.getExtension(o); + if (JSFILES[extension]) + run.push(o); + }); - for (var i = 0; i < length; i++) - str += (str.length > 0 ? ' ' : '') + (arguments[i] || ''); + if (!run.length) + return callback(); - self._verify_directory('logs'); - fs.appendFile(utils.combine(self.config['directory-logs'], filename + '.log'), time + ' | ' + str + '\n'); - return self; -}; + run.wait(function(filename, next) { + var fn = dir + filename + '_bk'; + Fs.renameSync(dir + filename, fn); + var fork = Child.fork(fn, [], { cwd: directory }); + fork.on('exit', function() { + fork = null; + next(); + }); + }, callback); -/* - Return string of framework usage information - @detailed {Boolean} :: default (false) - return {String} -*/ -Framework.prototype.usage = function(detailed) { - var self = this; - var memory = process.memoryUsage(); - var cache = Object.keys(self.cache.repository); - var resources = Object.keys(self.resources); - var controllers = Object.keys(self.controllers); - var connections = Object.keys(self.connections); - var workers = Object.keys(self.workers); - var modules = Object.keys(self.modules); - var models = Object.keys(self.models); - var components = Object.keys(self.components); - var helpers = Object.keys(self.helpers); - var staticFiles = Object.keys(self.temporary.path); - var staticRange = Object.keys(self.temporary.range); - var redirects = Object.keys(self.routes.redirects); - var size = 0; - var sizeDatabase = 0; - var dir = utils.combine(self.config['directory-temp']); - var output = {}; - - output.framework = { - pid: process.pid, - node: process.version, - version: 'v' + self.version_header, - platform: process.platform, - processor: process.arch, - uptime: Math.floor(process.uptime() / 60), - memoryTotal: (memory.heapTotal / 1024 / 1024).floor(2), - memoryUsage: (memory.heapUsed / 1024 / 1024).floor(2), - mode: self.config.debug ? 'debug' : 'release', - port: self.port, - ip: self.ip, - directory: process.cwd() - }; - - output.counter = { - resource: resources.length, - controller: controllers.length, - module: modules.length, - component: components.length, - cache: cache.length, - worker: workers.length, - connection: connections.length, - helper: helpers.length, - error: self.errors.length, - problem: self.problem.length - }; - - output.routing = { - webpage: self.routes.web.length, - websocket: self.routes.websockets.length, - file: self.routes.files.length, - partial: Object.keys(self.routes.partial).length, - global: self.routes.partialGlobal.length, - redirect: redirects.length - }; - - output.stats = { - request: self.stats.request, - response: self.stats.response - }; - - output.redirects = redirects; - - if (self.restrictions.isRestrictions) { - - output.restrictions = { - allowed: [], - blocked: [], - allowedHeaders: self.restrictions.allowedCustomKeys, - blockedHeaders: self.restrictions.blockedCustomKeys - }; - } - - if (!detailed) - return output; - - output.controllers = []; - - controllers.forEach(function(o) { - var item = self.controllers[o]; - output.controllers.push({ - name: o, - usage: typeof(item.usage) === UNDEFINED ? null : item.usage() - }); - }); - - output.connections = []; - - connections.forEach(function(o) { - output.connections.push({ - name: o, - online: self.connections[o].online - }); - }); - - output.modules = []; - - modules.forEach(function(o) { - var item = self.modules[o]; - output.modules.push({ - name: o, - usage: typeof(item.usage) === UNDEFINED ? null : item.usage() - }); - }); - - output.components = []; - - components.forEach(function(o) { - var item = self.components[o]; - output.components.push({ - name: o, - usage: typeof(item.usage) === UNDEFINED ? null : item.usage() - }); - }); - - output.models = []; - - models.forEach(function(o) { - var item = self.models[o]; - output.models.push({ - name: o, - usage: typeof(item.usage) === UNDEFINED ? null : item.usage() - }); - }); - - output.helpers = helpers; - output.cache = cache; - output.resources = resources; - output.errors = self.errors; - output.problems = self.problems; - output.changes = self.changes; - - return output; + return F; }; -/* - 3rd CSS compiler (Sync) - @filename {String} - @content {String} :: Content of CSS file - return {String} -*/ -Framework.prototype.onCompileCSS = null; +global.UPTODATE = F.uptodate = function(type, url, options, interval, callback, next) { -/* - 3rd JavaScript compiler (Sync) - @filename {String} - @content {String} :: Content of JavaScript file - return {String} -*/ -Framework.prototype.onCompileJS = null; + if (typeof(options) === 'string' && typeof(interval) !== 'string') { + interval = options; + options = null; + } -/* - Compile JavaScript and CSS - @req {ServerRequest} - @filename {String} - return {String or NULL}; -*/ -Framework.prototype.compileStatic = function(req, filename) { + OBSOLETE('UPTODATE()', 'This method is deprecated and it will be removed in v4.'); - if (!fs.existsSync(filename)) - return null; + var obj = { type: type, name: '', url: url, interval: interval, options: options, count: 0, updated: NOW, errors: [], callback: callback }; - var self = this; - var index = filename.lastIndexOf('.'); - var ext = filename.substring(index).toLowerCase(); - var output = fs.readFileSync(filename).toString(ENCODING); + if (!F.uptodates) + F.uptodates = []; - switch (ext) { - case EXTENSION_JS: - output = self.config['allow-compile-js'] ? self.onCompileJS === null ? internal.compile_javascript(output, self) : self.onCompileJS(filename, output) : output; - break; + F.uptodates.push(obj); + F.install(type, url, options, function(err, name) { + err && obj.errors.push(err); + obj.name = name; + obj.callback && obj.callback(err, name); + }, undefined, undefined, undefined, undefined, next); + return F; +}; - case '.css': - output = self.config['allow-compile-css'] ? self.onCompileCSS === null ? internal.compile_css(output) : self.onCompileCSS(filename, output) : output; - var matches = output.match(/url\(.*?\)/g); - if (matches !== null) { - matches.forEach(function(o) { - var url = o.substring(4, o.length - 1); - output = output.replace(o, 'url(' + self._version(url) + ')'); - }); - } +/** + * Install type with its declaration + * @param {String} type Available types: model, module, controller, source. + * @param {String} name Default name (optional). + * @param {String or Function} declaration + * @param {Object} options Custom options, optional. + * @param {Object} internal Internal/Temporary options, optional. + * @param {Boolean} useRequired Internal, optional. + * @param {Boolean} skipEmit Internal, optional. + * @param {String} uptodateName Internal, optional. + * @param {Function} next Internal, optional. + * @param {String} packageName Internal, optional. + * @return {Framework} + */ +global.INSTALL = F.install = function(type, name, declaration, options, callback, internal, useRequired, skipEmit, uptodateName, next, packageName) { + + var obj = null; + + if (type !== 'config' && type !== 'version' && typeof(name) === 'string') { + if (name.startsWith('http://') || name.startsWith('https://')) { + if (typeof(declaration) === 'object') { + callback = options; + options = declaration; + declaration = name; + name = ''; + } + } else if (name[0] === '@') { + declaration = F.path.package(name.substring(1)); + name = Path.basename(name).replace(/\.js$/i, ''); + if (useRequired === undefined) + useRequired = true; + } + } + + var t = typeof(declaration); + var key = ''; + var tmp; + var content; + var err; + + NOW = new Date(); + + if (t === 'object') { + t = typeof(options); + if (t === 'function') + callback = options; + options = declaration; + declaration = undefined; + } + + if (declaration === undefined) { + declaration = name; + name = ''; + } + + if (typeof(options) === 'function') { + callback = options; + 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') { + + if (declaration.startsWith('http://') || declaration.startsWith('https://')) { + if (type === 'package') { + F.consoledebug('download', type, declaration); + U.download(declaration, FLAGS_INSTALL, function(err, response) { + + if (err) { + F.error(err, 'F.install(\'{0}\', \'{1}\')'.format(type, declaration), null); + next && next(); + callback && callback(err); + return; + } + + var id = Path.basename(declaration, '.package'); + var filename = F.path.temp(id + '.download'); + var stream = Fs.createWriteStream(filename); + var md5 = Crypto.createHash('md5'); + + response.on('data', (buffer) => md5.update(buffer)); + response.pipe(stream); + + stream.on('finish', function() { + var hash = md5.digest('hex'); + + if (F.temporary.versions[declaration] === hash) { + next && next(); + callback && callback(null, uptodateName || name, true); + return; + } + + F.temporary.versions[declaration] = hash; + F.install(type, id, filename, options, callback, undefined, undefined, true, uptodateName, next); + }); + }); + return F; + } + + F.consoledebug('download', type, declaration); + U.request(declaration, FLAGS_INSTALL, function(err, data, code) { + + if (code !== 200 && !err) + err = new Error(data); + + if (err) { + F.error(err, 'F.install(\'{0}\', \'{1}\')'.format(type, declaration), null); + next && next(); + callback && callback(err); + } else { + + var hash = data.hash('md5'); + + if (F.temporary.versions[declaration] === hash) { + next && next(); + callback && callback(null, uptodateName || name, true); + return; + } + + F.temporary.versions[declaration] = hash; + F.install(type, name, data, options, callback, declaration, undefined, undefined, uptodateName, next); + } + + }); + return F; + } else { + if (declaration[0] === '~') + declaration = declaration.substring(1); + if (type !== 'config' && type !== 'resource' && type !== 'package' && type !== 'component' && !REG_SCRIPTCONTENT.test(declaration)) { + var relative = F.path.root(declaration); + if (existsSync(relative)) + declaration = relative; + if (!existsSync(declaration)) + throw new Error('The ' + type + ': ' + declaration + ' doesn\'t exist.'); + useRequired = true; + } + } + } + + if (type === 'middleware') { + + 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 = NOW; + } else { + F.dependencies[key] = { name: name, type: type, installed: NOW, updated: null, count: 0 }; + if (internal) + F.dependencies[key].url = internal; + } + + F.dependencies[key].count++; + + setTimeout(function() { + EMIT(type + '#' + name); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; + }, 500); + + F.consoledebug('install', type + '#' + name); + return F; + } + + 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; + EMIT(type + '#' + name, CONF); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; + }, 500); + + F.consoledebug('install', type + '#' + name); + next && next(); + callback && callback(null, name); + return F; + } + + if (type === 'version' || type === 'versions') { + + F.$configure_versions(declaration.toString().split('\n')); + setTimeout(function() { + EMIT(type + '#' + name); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; + }, 500); + + F.consoledebug('install', type + '#' + name); + next && next(); + callback && callback(null, name); + return F; + } + + if (type === 'workflow' || type === 'workflows') { + + F.$configure_workflows(declaration.toString().split('\n')); + setTimeout(function() { + EMIT(type + '#' + name); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; + F.consoledebug('install', type + '#' + name); + }, 500); + + next && next(); + callback && callback(null, name); + return F; + } + + if (type === 'sitemap') { + + F.$configure_sitemap(declaration.toString().split('\n')); + setTimeout(function() { + EMIT(type + '#' + name); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; + F.consoledebug('install', type + '#' + name); + }, 500); + + next && next(); + callback && callback(null, name); + return F; + } + + if (type === 'component') { + + if (!name && internal) + name = U.getName(internal).replace(/\.html/gi, '').trim(); + + F.uninstall(type, uptodateName || name, uptodateName ? 'uptodate' : undefined); + + 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); + + 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; + + 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); + 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 = 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) { + try { + var filecomponent = F.path.temp('component-' + name + '.js'); + _owner = (packageName ? packageName + '@' : '') + type + '#' + name; + Fs.writeFileSync(filecomponent, content.install.trim()); + obj = require(filecomponent); + (function(name) { + setTimeout(function() { + delete require.cache[name]; + }, 1000); + })(require.resolve(filecomponent)); + 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 || CONF[_owner], name); + } catch(e) { + F.error(e, 'F.install(\'component\', \'{0}\')'.format(name)); + } + } else if (!internal) { + var js = declaration.replace(/\.html$/i, '.js'); + if (existsSync(js)) { + _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 || CONF[_owner], name); + (function(name) { + setTimeout(function() { + delete require.cache[name]; + }, 1000); + })(require.resolve(declaration)); + } + } + + 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 + (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 + (DEBUG ? component_debug(name, content.css, 'css') : content.css) + hash.substring(0, hash.length - 1)); + tmp.css = true; + } + + tmp.version = GUID(5); + tmp.links = (tmp.js ? ''.format(link, tmp.version, key) : '') + (tmp.css ? ''.format(link, tmp.version, key) : ''); + } + + !skipEmit && setTimeout(function() { + EMIT(type + '#' + name); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; + }, 500); + + F.consoledebug('install', type + '#' + name); + next && next(); + callback && callback(null, name); + return F; + } + + if (type === 'package') { + + var id = Path.basename(declaration, '.' + U.getExtension(declaration)); + 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; + + var restorecb = function() { + var filename = Path.join(dir, 'index.js'); + if (!existsSync(filename)) { + next && next(); + callback && callback(null, name); + return; + } + + F.install('module', id, filename, options || CONF['package#' + name], function(err) { + setTimeout(function() { + 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; + } + + if (type === 'theme') { + + _owner = (packageName ? packageName + '@' : '') + type + '#' + name; + obj = require(declaration); + obj.$owner = _owner; + F.temporary.owners[_owner] = true; + + typeof(obj.install) === 'function' && obj.install(options || CONF[_owner], name); + + !skipEmit && setTimeout(function() { + EMIT(type + '#' + name); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; + }, 500); + + F.consoledebug('install', type + '#' + name); + next && next(); + callback && callback(null, name); + + (function(name) { + setTimeout(function() { + delete require.cache[name]; + }, 1000); + })(require.resolve(declaration)); + return F; + } + + if (type === 'package2') { + type = type.substring(0, type.length - 1); + var id = U.getName(declaration, '.package'); + 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 || CONF['package#' + name], function(err) { + setTimeout(function() { + 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); + }, internal, useRequired, true); + next && next(); + return F; + } + + var plus = F.id ? 'i-' + F.id + '_' : ''; + if (type === 'view') { + + var item = F.routes.views[name]; + key = type + '.' + name; + + if (item === undefined) { + item = {}; + item.filename = F.path.temporary(plus + 'installed-view-' + U.GUID(10) + '.tmp'); + item.url = internal; + item.count = 0; + F.routes.views[name] = item; + } + + item.count++; + Fs.writeFileSync(item.filename, framework_internal.modificators(declaration, name)); + + setTimeout(function() { + EMIT(type + '#' + name); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; + }, 500); + + F.consoledebug('install', type + '#' + name); + next && next(); + callback && callback(null, name); + return F; + } + + if (type === 'definition' || type === 'eval' || type === 'schema' || type === 'operation' || type === 'task') { + + _controller = ''; + _owner = (packageName ? packageName + '@' : '') + type + '#' + name; + F.temporary.owners[_owner] = true; + err = null; + + try { + + if (useRequired) { + var relative = F.path.root(declaration); + if (existsSync(relative)) + declaration = relative; + delete require.cache[require.resolve(declaration)]; + obj = require(declaration); + + (function(name) { + setTimeout(() => delete require.cache[name], 1000); + })(require.resolve(declaration)); + } else + obj = typeof(declaration) === 'function' ? eval('(' + declaration.toString() + ')()') : eval(declaration); + + } catch (ex) { + err = ex; + } + + if (err) { + F.error(err, 'F.install(\'' + type + '\')', null); + next && next(); + callback && callback(err, name); + return F; + } + + F.consoledebug('install', type + '#' + (name || '::undefined::')); + next && next(); + callback && callback(null, name); + + setTimeout(function() { + EMIT(type + '#' + name); + EMIT('install', type, name); + F.temporary.ready[type + '#' + name] = NOW; + }, 500); + + return F; + } + + if (type === 'isomorphic') { + + content = ''; + err = null; + + OBSOLETE('isomorphic', 'Isomorphic scripts will be removed in v4.'); + + try { + + if (!name && typeof(internal) === 'string') { + var tmp = internal.match(/[a-z0-9]+\.js$/i); + if (tmp) + name = tmp.toString().replace(/\.js/i, ''); + } + + if (useRequired) { + var relative = F.path.root(declaration); + if (existsSync(relative)) + declaration = relative; + delete require.cache[require.resolve(declaration)]; + obj = require(declaration); + content = Fs.readFileSync(declaration).toString(ENCODING); + (function(name) { + setTimeout(() => delete require.cache[name], 1000); + })(require.resolve(declaration)); + } + else { + obj = typeof(declaration) === 'function' ? eval('(' + declaration.toString() + ')()') : eval(declaration); + content = declaration.toString(); + } + + } catch (ex) { + err = ex; + } + + if (err) { + F.error(err, 'F.install(\'' + type + '\')', null); + next && next(); + callback && callback(err, name); + return F; + } + + if (typeof(obj.id) === 'string') + name = obj.id; + else if (typeof(obj.name) === 'string') + name = obj.name; + + if (obj.url) { + if (obj.url[0] !== '/') + obj.url = '/' + obj.url; + } else + obj.url = '/' + name + '.js'; + + tmp = F.path.temp('isomorphic_' + name + '.min.js'); + F.map(framework_internal.preparePath(obj.url), tmp); + F.isomorphic[name] = obj; + + 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() { + EMIT(type + '#' + name, obj); + EMIT('install', type, name, obj); + F.temporary.ready[type + '#' + name] = NOW; + }, 500); + + return F; + } + + if (type === 'model' || type === 'source') { + + _controller = ''; + _owner = (packageName ? packageName + '@' : '') + type + '#' + name; + F.temporary.owners[_owner] = true; + err = null; + + try { + + if (useRequired) { + var relative = F.path.root(declaration); + if (existsSync(relative)) + declaration = relative; + obj = require(declaration); + (function(name) { + setTimeout(() => delete require.cache[name], 1000); + })(require.resolve(declaration)); + } + else { + + if (typeof(declaration) !== 'string') + declaration = declaration.toString(); + + if (!name && typeof(internal) === 'string') { + var tmp = internal.match(/[a-z0-9]+\.js$/i); + if (tmp) + name = tmp.toString().replace(/\.js/i, ''); + } + + var filename = F.path.temporary(plus + 'installed-' + type + '-' + U.GUID(10) + '.js'); + Fs.writeFileSync(filename, declaration); + obj = require(filename); + + (function(name, filename) { + setTimeout(function() { + Fs.unlinkSync(filename); + delete require.cache[name]; + }, 1000); + })(require.resolve(filename), filename); + } + + } catch (ex) { + err = ex; + } + + if (err) { + F.error(err, 'F.install(\'' + type + '\', \'' + name + '\')', null); + next && next(); + callback && callback(err, name); + return F; + } + + if (typeof(obj.id) === 'string') + name = obj.id; + else if (typeof(obj.name) === 'string') + name = obj.name; + + _owner = (packageName ? packageName + '@' : '') + type + '#' + name; + obj.$owner = _owner; + + if (!name) + name = (Math.random() * 10000) >> 0; + + key = type + '.' + name; + tmp = F.dependencies[key]; + + F.uninstall(type, uptodateName || name, uptodateName ? 'uptodate' : undefined); + F.temporary.owners[_owner] = true; + + if (tmp) { + F.dependencies[key] = tmp; + F.dependencies[key].updated = NOW; + } + else { + F.dependencies[key] = { name: name, type: type, installed: NOW, updated: null, count: 0 }; + if (internal) + F.dependencies[key].url = internal; + } + + F.dependencies[key].count++; + + if (obj.reinstall) + F.dependencies[key].reinstall = obj.reinstall.toString().parseDateExpiration(); + else + delete F.dependencies[key]; + + if (type === 'model') + F.models[name] = obj; + else + F.sources[name] = obj; + + typeof(obj.install) === 'function' && obj.install(options || CONF[type + '#' + name], name); + + !skipEmit && setTimeout(function() { + EMIT(type + '#' + name, obj); + EMIT('install', type, name, obj); + F.temporary.ready[type + '#' + name] = NOW; + }, 500); + + F.consoledebug('install', type + '#' + name); + next && next(); + callback && callback(null, name); + return F; + } + + if (type === 'module' || type === 'controller') { + + // for inline routes + var _ID = _controller = 'TMP' + U.random(10000); + _owner = (packageName ? packageName + '@' : '') + type + '#' + name; + err = null; + + try { + if (useRequired) { + var relative = F.path.root(declaration); + if (existsSync(relative)) + declaration = relative; + obj = require(declaration); + (function(name) { + setTimeout(function() { + delete require.cache[name]; + }, 1000); + })(require.resolve(declaration)); + } else { + + if (typeof(declaration) !== 'string') + declaration = declaration.toString(); + + if (!name && typeof(internal) === 'string') { + var tmp = internal.match(/[a-z0-9]+\.js$/i); + if (tmp) + name = tmp.toString().replace(/\.js/i, ''); + } + + filename = F.path.temporary(plus + 'installed-' + type + '-' + U.GUID(10) + '.js'); + Fs.writeFileSync(filename, declaration); + obj = require(filename); + (function(name, filename) { + setTimeout(function() { + Fs.unlinkSync(filename); + delete require.cache[name]; + }, 1000); + })(require.resolve(filename), filename); + } + + } catch (ex) { + err = ex; + } + + if (err) { + F.error(err, 'F.install(\'' + type + '\', \'' + (name ? '' : internal) + '\')', null); + next && next(); + callback && callback(err, name); + return F; + } + + if (typeof(obj.id) === 'string') + name = obj.id; + else if (typeof(obj.name) === 'string') + name = obj.name; + + if (!name) + name = (Math.random() * 10000) >> 0; + + _owner = (packageName ? packageName + '@' : '') + type + '#' + name; + obj.$owner = _owner; + + obj.booting && setTimeout(function() { + + var tmpdir = F.path.temp(name + (U.getExtension(name) === 'package' ? '' : '.package/')); + + if (obj.booting === 'root') { + 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'); + 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'); + F.$configure_sitemap('@' + name + '/sitemap'); + F.$configure_workflows('@' + name + '/workflows'); + } + + F.$bundle(() => F.$load(undefined, tmpdir, undefined, name)); + }, 100); + + key = type + '.' + name; + tmp = F.dependencies[key]; + + F.uninstall(type, uptodateName || name, uptodateName ? 'uptodate' : undefined, undefined, packageName); + F.temporary.owners[_owner] = true; + + if (tmp) { + F.dependencies[key] = tmp; + F.dependencies[key].updated = NOW; + } + else { + F.dependencies[key] = { name: name, type: type, installed: NOW, updated: null, count: 0, _id: _ID }; + if (internal) + F.dependencies[key].url = internal; + } + + F.dependencies[key].dependencies = obj.dependencies; + F.dependencies[key].count++; + F.dependencies[key].processed = false; + + if (obj.reinstall) + F.dependencies[key].reinstall = obj.reinstall.toString().parseDateExpiration(); + else + delete F.dependencies[key].reinstall; + + _controller = _ID; + + if (obj.dependencies instanceof Array) { + for (var i = 0, length = obj.dependencies.length; i < length; i++) { + if (!F.dependencies[type + '.' + obj.dependencies[i]]) { + F.temporary.dependencies[key] = { obj: obj, options: options, callback: callback, skipEmit: skipEmit }; + next && next(); + return F; + } + } + } + + F.install_make(key, name, obj, options, callback, skipEmit, type); + + if (type === 'module') + F.modules[name] = obj; + else + F.controllers[name] = obj; + + F.install_prepare(); + next && next(); + } + + 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++) { + + var k = keys[i]; + var a = F.temporary.dependencies[k]; + var b = F.dependencies[k]; + var skip = false; + + if (b.processed) + continue; + + for (var j = 0, jl = b.dependencies.length; j < jl; j++) { + var d = F.dependencies['module.' + b.dependencies[j]]; + if (!d || !d.processed) { + skip = true; + break; + } + } + + if (skip) + continue; + + delete F.temporary.dependencies[k]; + + if (b.type === 'module') + F.modules[b.name] = a.obj; + else + F.controllers[b.name] = a.obj; + + F.install_make(k, b.name, a.obj, a.options, a.callback, a.skipEmit, b.type); + } - break; - } + keys = Object.keys(F.temporary.dependencies); - self._verify_directory('temp'); + clearTimeout(F.temporary.other.dependencies); + F.temporary.other.dependencies = setTimeout(function() { + var keys = Object.keys(F.temporary.dependencies); + if (keys.length) + throw new Error('Dependency exception, missing dependencies for: ' + keys.join(', ').trim()); + delete F.temporary.other.dependencies; + }, CONF.default_dependency_timeout); - var fileCompiled = utils.combine(self.config['directory-temp'], req.uri.pathname.replace(/\//g, '-').substring(1)); - fs.writeFileSync(fileCompiled, output); + if (!keys.length || noRecursive) + return F; - return fileCompiled; + F.install_prepare(true); + return F; }; -/* - Serve static files - @req {ServerRequest} - @res {ServerResponse} - return {Framework} -*/ -Framework.prototype.responseStatic = function(req, res) { - - var self = this; - - if (res.success) - return self; - - var name = req.url; - var index = name.indexOf('?'); +F.install_make = function(key, name, obj, options, callback, skipEmit, type) { - if (index !== -1) - name = name.substring(0, index); + var me = F.dependencies[key]; + var routeID = me._id; + var type = me.type; - index = name.lastIndexOf('/'); - var resizer = self.routes.resize[name.substring(0, index + 1)] || null; - var isResize = false; + F.temporary.internal[me._id] = name; + _controller = routeID; + _owner = type + '#' + name.replace(/\.package$/gi, ''); - if (resizer !== null) { - name = name.substring(index + 1); - index = name.lastIndexOf('.'); - isResize = resizer.extension === '*' || resizer.extension.indexOf(name.substring(index).toLowerCase()) !== -1; - if (isResize) - name = resizer.path + name; - } + typeof(obj.install) === 'function' && obj.install(options || CONF[_owner], name); + me.processed = true; - var filename = utils.combine(self.config['directory-public'], decodeURIComponent(name)); + var id = (type === 'module' ? '#' : '') + name; + var length = F.routes.web.length; - if (!isResize) { - self.responseFile(req, res, filename, ''); - return self; - } + for (var i = 0; i < length; i++) { + if (F.routes.web[i].controller === routeID) + F.routes.web[i].controller = id; + } - self.responseImage(req, res, filename, function(image) { + 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; + } - if (resizer.width || resizer.height) { - if (resizer.width && resizer.height) - image.resizeCenter(resizer.width, resizer.height); - else - image.resize(resizer.width, resizer.height); - } + length = F.routes.websockets.length; + for (var i = 0; i < length; i++) { + if (F.routes.websockets[i].controller === routeID) + F.routes.websockets[i].controller = id; + } - if (resizer.grayscale) - image.grayscale(); + length = F.routes.files.length; + for (var i = 0; i < length; i++) { + if (F.routes.files[i].controller === routeID) + F.routes.files[i].controller = id; + } - if (resizer.blur) - image.blur(typeof(resizer.blur) === 'number' ? resizer.blur : 1); + F.$routesSort(); + _controller = ''; + name = name.replace(/\.package$/gi, ''); - if (resizer.rotate && typeof(resizer.rotate) == NUMBER) - image.rotate(resizer.rotate); + if (!skipEmit) { + setTimeout(function() { + EMIT(type + '#' + name, obj); + EMIT('install', type, name, obj); + F.temporary.ready[type + '#' + name] = NOW; + }, 500); + } - if (resizer.flop) - image.flop(); - - if (resizer.flip) - image.flip(); - - if (resizer.sepia) - image.sepia(typeof(resizer.sepia) === 'number' ? resizer.sepia : 100); - - image.quality(self.config['default-image-quality']); - image.minify(); - - }); - - return self; + F.consoledebug('install', type + '#' + name); + callback && callback(null, name); + return F; }; /** - * Is processed static file? - * @param {String / Request} filename Filename or Request object. - * @return {Boolean} + * Uninstall type + * @param {String} type Available types: model, module, controller, source. + * @param {String} name + * @param {Object} options Custom options, optional. + * @param {Object} skipEmit Internal, optional. + * @return {Framework} */ -Framework.prototype.isProcessed = function(filename) { - - var self = this; - - if (filename.url) { - var name = filename.url; - var index = name.indexOf('?'); - - if (index !== -1) - name = name.substring(0, index); - - filename = utils.combine(self.config['directory-public'], decodeURIComponent(name)); - } - - if (typeof(self.temporary.path[filename]) !== UNDEFINED) - return true; - - return false; +global.UNINSTALL = F.uninstall = function(type, name, options, skipEmit, packageName) { + + var obj = null; + var k, v, tmp; + + if (type === 'route' || type === 'web') { + k = typeof(name) === 'string' ? name.substring(0, 3) === 'id:' ? 'id' : 'urlraw' : 'execute'; + v = k === 'execute' ? name : k === 'id' ? name.substring(3).trim() : name; + if (k === 'urlraw' && v[0] === '#') + delete F.routes.system[v]; + else + F.routes.web = F.routes.web.remove(k, v); + F.$routesSort(); + F.consoledebug('uninstall', type + '#' + name); + F.temporary.other = {}; + return F; + } + + if (type === 'cors') { + k = typeof(name) === 'string' ? name.substring(0, 3) === 'id:' ? 'id' : 'hash' : 'hash'; + v = k === 'id' ? name.substring(3).trim() : name; + 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; + } + + if (type === 'operation') { + NEWOPERATION(name, null); + F.consoledebug('uninstall', type + '#' + name); + 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); + return F; + } + + var id = (packageName ? packageName + '@' : '') + type + '#' + name; + + if (type === 'websocket') { + k = typeof(name) === 'string' ? name.substring(0, 3) === 'id:' ? 'id' : 'urlraw' : 'onInitialize'; + v = k === 'onInitialize' ? name : k === 'id' ? name.substring(3).trim() : name; + F.routes.websockets = F.routes.websockets.remove(k, v); + F.$routesSort(); + F.consoledebug('uninstall', type + '#' + name); + return F; + } + + if (type === 'file') { + 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; + } + + if (type === 'schema') { + tmp = name.split('/'); + tmp.length === 2 ? framework_builders.remove(tmp[0], tmp[1]) : framework_builders.remove(undefined, tmp[0]); + F.consoledebug('uninstall', type + '#' + name); + } else if (type === 'mapping') { + delete F.routes.mapping[name]; + F.consoledebug('uninstall', type + '#' + name); + } else if (type === 'isomorphic') { + var obj = F.isomorphic[name]; + if (obj.url) + delete F.routes.mapping[F.$version(obj.url)]; + delete F.isomorphic[name]; + delete F.temporary.ready[type + '#' + name]; + F.consoledebug('uninstall', type + '#' + name); + } else if (type === 'middleware') { + + if (!F.routes.middleware[name]) + return F; + + delete F.routes.middleware[name]; + delete F.dependencies[type + '.' + name]; + delete F.temporary.ready[type + '#' + name]; + F._length_middleware = Object.keys(F.routes.middleware).length; + + for (var i = 0, length = F.routes.web.length; i < length; i++) { + tmp = F.routes.web[i]; + if (tmp.middleware && tmp.middleware.length) + tmp.middleware = tmp.middleware.remove(name); + } + + for (var i = 0, length = F.routes.websockets.length; i < length; i++) { + tmp = F.routes.websockets[i]; + if (tmp.middleware && tmp.middleware.length) + tmp.middleware = tmp.middleware.remove(name); + } + + for (var i = 0, length = F.routes.files.length; i < length; i++) { + tmp = F.routes.files[i]; + if (tmp.middleware && tmp.middleware.length) + tmp.middleware = tmp.middleware.remove(name); + } + + F.consoledebug('uninstall', type + '#' + name); + + } else if (type === 'package') { + delete F.routes.packages[name]; + delete F.temporary.ready['package#' + name]; + F.uninstall('module', name, options, true); + F.consoledebug('uninstall', type + '#' + name); + return F; + } else if (type === 'view' || type === 'precompile') { + + obj = F.routes.views[name]; + + if (!obj) + return F; + + delete F.routes.views[name]; + delete F.dependencies[type + '.' + name]; + delete F.temporary.ready[type + '#' + name]; + + fsFileExists(obj.filename, function(e) { + e && Fs.unlink(obj.filename, NOOP); + F.consoledebug('uninstall', type + '#' + name); + }); + + } else if (type === 'model' || type === 'source') { + + obj = type === 'model' ? F.models[name] : F.sources[name]; + + if (!obj) + return F; + + F.$uninstall(id); + typeof(obj.uninstall) === 'function' && obj.uninstall(options, name); + + if (type === 'model') + delete F.models[name]; + else + delete F.sources[name]; + + delete F.dependencies[type + '.' + name]; + delete F.temporary.ready[type + '#' + name]; + F.consoledebug('uninstall', type + '#' + name); + + } else if (type === 'module' || type === 'controller') { + + var isModule = type === 'module'; + obj = isModule ? F.modules[name] : F.controllers[name]; + + if (!obj) + return F; + + F.$uninstall(id, packageName ? '' : ((isModule ? '#' : '') + name)); + delete F.temporary.ready[type + '#' + name]; + F.consoledebug('uninstall', type + '#' + name); + + if (obj) { + obj.uninstall && obj.uninstall(options, name); + if (isModule) + delete F.modules[name]; + else + delete F.controllers[name]; + } + + } else if (type === 'component') { + + if (!F.components.instances[name]) + return F; + + obj = F.components.instances[name]; + + if (obj) { + F.$uninstall(id); + obj.uninstall && obj.uninstall(options, name); + delete F.components.instances[name]; + } + + 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'; + var data; + var index; + var beg = '\n/*' + name.hash() + '*/\n'; + var end = beg.substring(0, beg.length - 1); + var is = false; + + if (F.components.js) { + data = Fs.readFileSync(F.path.temp(temporary + '.js')).toString('utf-8'); + index = data.indexOf(beg); + if (index !== -1) { + data = data.substring(0, index) + data.substring(data.indexOf(end, index + end.length) + end.length); + Fs.writeFileSync(F.path.temp(temporary + '.js'), data); + is = true; + } + } + + if (F.components.css) { + data = Fs.readFileSync(F.path.temp(temporary + '.css')).toString('utf-8'); + index = data.indexOf(beg); + if (index !== -1) { + data = data.substring(0, index) + data.substring(data.indexOf(end, index + end.length) + end.length); + Fs.writeFileSync(F.path.temp(temporary + '.css'), data); + is = true; + } + } + + if (obj.group) { + temporary += '_g' + obj.group.hash(); + tmp = F.components.groups[obj.group]; + if (tmp) { + + if (tmp.js) { + data = Fs.readFileSync(F.path.temp(temporary + '.js')).toString('utf-8'); + index = data.indexOf(beg); + if (index !== -1) { + data = data.substring(0, index) + data.substring(data.indexOf(end, index + end.length) + end.length); + Fs.writeFileSync(F.path.temp(temporary + '.js'), data); + is = true; + } + } + + if (tmp.css) { + data = Fs.readFileSync(F.path.temp(temporary + '.css')).toString('utf-8'); + index = data.indexOf(beg); + if (index !== -1) { + data = data.substring(0, index) + data.substring(data.indexOf(end, index + end.length) + end.length); + Fs.writeFileSync(F.path.temp(temporary + '.css'), data); + is = true; + } + } + + tmp.version = NOW.getTime(); + } + } + + if (is) + F.components.version = NOW.getTime(); + + F.consoledebug('uninstall', type + '#' + name); + } + + !skipEmit && EMIT('uninstall', type, name); + return F; +}; + +F.$uninstall = function(owner, controller) { + + if (!F.temporary.owners[owner]) + return F; + + if (controller) { + F.routes.web = F.routes.web.remove('controller', controller); + F.routes.files = F.routes.files.remove('controller', controller); + F.routes.websockets = F.routes.websockets.remove('controller', controller); + } + + F.routes.web = F.routes.web.remove('owner', owner); + 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); + + 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); + + framework_builders.uninstall(owner); + + var owners = []; + var redirects = false; + + for (var i = 0, length = F.owners.length; i < length; i++) { + + var m = F.owners[i]; + if (m.owner !== owner) { + owners.push(m); + continue; + } + + switch (m.type) { + case 'redirects': + delete F.routes.redirects[m.id]; + redirects = true; + break; + case 'resize': + delete F.routes.resize[m.id]; + break; + case 'merge': + delete F.routes.merge[m.id]; + break; + case 'mapping': + delete F.routes.mapping[m.id]; + break; + case 'blocks': + delete F.routes.blocks[m.id]; + break; + case 'middleware': + UNINSTALL('middleware', m.id); + break; + } + + } + + if (redirects) + F._request_check_redirect = Object.keys(F.routes.redirects).length > 0; + + F.owners = owners; + F.$routesSort(); + delete F.temporary.owners[owner]; + + return F; }; /** - * Processing - * @param {String / Request} filename Filename or Request object. - * @return {Boolean} + * Register internal mapping (e.g. Resource) + * @param {String} path + * @return {Framework} */ -Framework.prototype.isProcessing = function(filename) { - - var self = this; - - if (filename.url) { - var name = filename.url; - var index = name.indexOf('?'); - - if (index !== -1) - name = name.substring(0, index); - - filename = utils.combine(self.config['directory-public'], decodeURIComponent(name)); - } - - var name = this.temporary.processing[filename]; - if (typeof(self.temporary.processing[filename]) !== UNDEFINED) - return true; - return false; +F.register = function(path) { + + var key; + var extension = '.' + U.getExtension(path); + var name = U.getName(path); + var c = path[0]; + + if (c === '@') + path = F.path.package(path.substring(1)); + else if (c === '=') { + if (path[1] === '?') + F.path.themes(CONF.default_theme + path.substring(2)); + else + path = F.path.themes(path.substring(1)); + } + + switch (extension) { + case '.resource': + key = name.replace(extension, ''); + if (F.routes.resources[key]) + F.routes.resources[key].push(path); + else + F.routes.resources[key] = [path]; + // clears cache + delete F.resources[key]; + break; + + default: + throw new Error('Not supported registration type "' + extension + '".'); + } + + return F; }; /** - * Disable HTTP cache for current request/response - * @param {Request} req Request - * @param {Response} res (optional) Response + * Run code + * @param {String or Function} script Function to eval or Code or URL address. * @return {Framework} */ -Framework.prototype.noCache = function(req, res) { - - req.noCache(); - - if (res) - res.noCache(); +F.eval = function(script) { + return F.install('eval', script); +}; - return this; +/** + * Error handler + * @param {Error} err + * @param {String} name + * @param {Object} uri URI address, optional. + * @return {Framework} + */ +F.onError = function(err, name, uri) { + 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; }; /* - Response file - @req {ServerRequest} - @res {ServerResponse} - @filename {String} - @downloadName {String} :: optional - @headers {Object} :: optional - @filepath {String} :: path to file (INTERNAL) - return {Framework} + Authorization handler + @req {Request} + @res {Response} OR {WebSocketClient} + @flags {String array} + @callback {Function} - @callback(Boolean), true is [authorize]d and false is [unauthorize]d */ -Framework.prototype.responseFile = function(req, res, filename, downloadName, headers, key) { +F.onAuthorize = null; - var self = this; +/* + Sets the current language for the current request + @req {Request} + @res {Response} OR {WebSocketClient} + @return {String} +*/ +F.onLocale = null; +// OLD: F.onLocate = null; + +/** + * Sets theme to controller + * @controller {Controller} + * @return {String} + */ +F.onTheme = null; - if (res.success) - return self; +/* + Versioning static files (this delegate call LESS CSS by the background property) + @name {String} :: name of static file (style.css or script.js) + return {String} :: return new name of static file (style-new.css or script-new.js) +*/ +F.onVersion = null; - req.clear(true); +/** + * On mapping static files + * @param {String} url + * @param {String} def Default value. + * @return {String} + */ +F.onMapping = function(url, def, ispublic, encode) { - key = key || filename; - var name = self.temporary.path[key]; + if (url[0] !== '/') + url = '/' + url; - if (framework.config.debug) - name = undefined; + var tmp = url; + if (CONF.default_root) + tmp = tmp.substring(CONF.default_root.length - 1); - if (name === null) { - self.response404(req, res); - return self; - } + // 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; + } - var extension = path.extname(key).substring(1); + if (F.routes.mapping[url]) + return F.routes.mapping[url]; - if (extension.length === 0) - extension = path.extname(name).substring(1); + 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); + } + } - if (self.config['static-accepts'].indexOf('.' + extension) === -1) { - self.response404(req, res); - return self; - } + def = framework_internal.preparePath(def, true); - var etag = utils.etag(req.url, self.config['etag-version']); + if (encode) + def = $decodeURIComponent(def); - if (!self.config.debug && req.headers['if-none-match'] === etag) { + if (ispublic) + def = F.path.public_cache(def); + else + def = def[0] === '~' ? def.substring(1) : def[0] === '.' ? def : F.path.public_cache(def); - res.success = true; - res.writeHead(304); - res.end(); + return def; +}; - self.stats.response.notModified++; - self._request_stats(false, req.isStaticFile); +global.DOWNLOAD = F.download = F.snapshot = function(url, filename, callback) { - if (!req.isStaticFile) - self.emit('request-end', req, res); + if (!F.isLoaded && url[0] === '/') { + setTimeout(F.download, 200, url, filename, callback); + return F; + } - return self; - } + url = framework_internal.preparePath(url); - if (typeof(name) === UNDEFINED) { + if (!REG_HTTPHTTPS.test(url)) { + if (url[0] !== '/') + url = '/' + url; + if (F.isWorker) + 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; + } - if (!fs.existsSync(filename)) { + U.download(url, FLAGS_DOWNLOAD, function(err, response) { - // virtual directory App - var tmpname = self.isWindows ? filename.replace(self.config['directory-public'].replace(/\//g, '\\'), self.config['directory-angular'].replace(/\//g, '\\')) : filename.replace(self.config['directory-public'], self.config['directory-angular']); - var notfound = true; + if (err) { + callback && callback(err); + callback = null; + return; + } - if (tmpname !== filename) { - filename = tmpname; - notfound = !fs.existsSync(filename); - } + var stream = Fs.createWriteStream(filename); - if (notfound) { - self.temporary.path[key] = null; - self.response404(req, res); - return self; - } - } + var done = function(err) { + if (callback) { + callback(err); + callback = null; + } + }; - name = filename; + response.pipe(stream); + response.on('error', done); + stream.on('error', done); + CLEANUP(stream, done); + }); - // compile JavaScript and CSS - if (extension === 'js' || extension === 'css') { - if (name.lastIndexOf('.min.') === -1 && name.lastIndexOf('-min.') === -1) { - name = self.compileStatic(req, name); - self.temporary.path[key] = name; - } - } + return F; +}; - name += ';' + fs.statSync(name).size; +/** + * Find WebSocket connection + * @param {String/RegExp} path + * @return {WebSocket} + */ +F.findConnection = function(path) { + var arr = Object.keys(F.connections); + var is = U.isRegExp(path); + for (var i = 0, length = arr.length; i < length; i++) { + var key = arr[i]; + if (is) { + if (path.test(key)) + return F.connections[key]; + } else { + if (key.indexOf(path) !== -1) + return F.connections[key]; + } + } +}; - self.temporary.path[key] = name; +/** + * Find WebSocket connections + * @param {String/RegExp} path + * @return {WebSocket Array} + */ +F.findConnections = function(path) { + var arr = Object.keys(F.connections); + var is = U.isRegExp(path); + var output = []; + for (var i = 0, length = arr.length; i < length; i++) { + var key = arr[i]; + if (!path) + output.push(F.connections[key]); + else if (is) + path.test(key) && output.push(F.connections[key]); + else + key.indexOf(path) !== -1 && output.push(F.connections[key]); + } + return output; +}; - if (self.config.debug) - delete self.temporary.path[key]; - } +/** + * Global validation + * @param {Function(name, value)} delegate + * @type {Boolean or StringErrorMessage} + */ +F.onValidate = null; - var index = name.lastIndexOf(';'); - var size = null; +/** + * Global XML parsing + * @param {String} value + * @return {Object} + */ +F.onParseXML = function(value, replace) { + var val = U.parseXML(value, replace); + F._length_convertors && F.convert(val); + return val; +}; +F.onParseXML.$def = true; - if (index === -1) - index = name.length; - else - size = name.substring(index + 1); +F.$onParseXML = function(req) { + if (F.onParseXML.$def) { + req.body = U.parseXML(req.buffer_data); + F._length_convertors && F.convert(req.body); + } else + req.body = F.onParseXML(req.buffer_data); +}; - name = name.substring(0, index); +/** + * Global JSON parsing + * @param {String} value + * @return {Object} + */ +F.onParseJSON = function(value) { + if (value) { + try { + return JSON.parse(value); + } catch (e) {} + } +}; +F.onParseJSON.$def = true; - var accept = req.headers['accept-encoding'] || ''; - var returnHeaders = {}; +F.$onParseJSON = function(req) { + req.body = F.onParseJSON.$def ? JSON.parse(req.buffer_data) : F.onParseJSON(req.buffer_data); +}; - returnHeaders['Accept-Ranges'] = 'bytes'; - returnHeaders[RESPONSE_HEADER_CACHECONTROL] = 'public'; - returnHeaders['Expires'] = new Date().add('d', 15); - returnHeaders['Vary'] = 'Accept-Encoding'; +function parseQueryArgumentsDecode(val) { + try { + return decodeURIComponent(val); + } catch (e) { + return ''; + } +} - if (headers) - utils.extend(returnHeaders, headers, true); +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; +} - if (downloadName && downloadName.length > 0) - returnHeaders['Content-Disposition'] = 'attachment; filename="' + downloadName + '"'; +/** + * Global JSON parsing + * @param {String} value + * @return {Object} + */ +F.onParseQuery = function(value) { + if (value) { + // var val = Qs.parse(value, null, null, QUERYPARSEROPTIONS); + var val = parseQueryArguments(value); + F._length_convertors && F.convert(val); + return val; + } + return {}; +}; +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 = parseQueryArguments(req.buffer_data); + F._length_convertors && F.convert(req.body); + } else + req.body = {}; + } else + req.body = F.onParseQuery(req.buffer_data, req); +}; + +F.$onParseQueryUrl = function(req) { + if (F.onParseQuery.$def) { + 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); +}; - if (etag.length > 0) - returnHeaders['Etag'] = etag; +/** + * Schema parser delegate + * @param {Request} req + * @param {String} group + * @param {String} name + * @param {Function(err, body)} callback + */ +F.onSchema = function(req, route, callback) { - if (!returnHeaders[RESPONSE_HEADER_CONTENTTYPE]) - returnHeaders[RESPONSE_HEADER_CONTENTTYPE] = utils.getContentType(extension); + var schema; - var compress = self.config['allow-gzip'] && REQUEST_COMPRESS_CONTENTTYPE.indexOf(returnHeaders[RESPONSE_HEADER_CONTENTTYPE]) !== -1; - var range = req.headers['range'] || ''; - var supportsGzip = accept.lastIndexOf('gzip') !== -1; + 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]); - res.success = true; + if (req.method === 'PATCH' || req.method === 'DELETE') + req.$patch = true; - if (range.length > 0) - return self.responseRange(name, range, returnHeaders, req, res); + if (schema) + schema.make(req.body, route.schema[2], onSchema_callback, callback, route.novalidate, route.workflow ? route.workflow.meta : null, req); + else + callback('Schema "' + (route.isDYNAMICSCHEMA ? req.$schemaname : (route.schema[0] + '/' + route.schema[1])) + '" not found.'); +}; - if (size !== null && size !== '0' && !compress) - returnHeaders['Content-Length'] = size; +function onSchema_callback(err, res, callback) { + if (err) + callback(err); + else + callback(null, res); +} - var stream; +var onmailsendforce = (cb, message) => message.send2(cb); - if (compress && supportsGzip) { +/** + * Mail delegate + * @param {String or Array String} address + * @param {String} subject + * @param {String} body + * @param {Function(err)} callback + * @param {String} replyTo + * @return {MailMessage} + */ +F.onMail = function(address, subject, body, callback, replyTo) { - returnHeaders['Content-Encoding'] = 'gzip'; - res.writeHead(200, returnHeaders); - stream = fs.createReadStream(name).pipe(zlib.createGzip()); - stream.pipe(res); + var tmp; - self.stats.response.file++; - self._request_stats(false, req.isStaticFile); + if (typeof(callback) === 'string') { + tmp = replyTo; + replyTo = callback; + callback = tmp; + } - if (!req.isStaticFile) - self.emit('request-end', req, res); + var message = Mail.create(subject, body); - return self; + if (address instanceof Array) { + for (var i = 0, length = address.length; i < length; i++) + message.to(address[i]); + } else + message.to(address); - } + message.from(CONF.mail_address_from || '', CONF.name); - res.writeHead(200, returnHeaders); - stream = fs.createReadStream(name); - stream.pipe(res); - self.stats.response.file++; - self._request_stats(false, req.isStaticFile); + if (replyTo) + message.reply(replyTo); + else { + tmp = CONF.mail_address_reply; + tmp && tmp.length > 3 && message.reply(tmp); + } - if (!req.isStaticFile) - self.emit('request-end', req, res); + tmp = CONF.mail_address_copy; + tmp && tmp.length > 3 && message.bcc(tmp); - return self; + message.$sending = setImmediate(onmailsendforce, callback, message); + return message; }; -/* - Response PIPE - @req {ServerRequest} - @res {ServerResponse} - @url {String} - @header {Object} :: optional - @timeout {Number} :: optional - @callback {Function} :: optional - return {Framework} -*/ -Framework.prototype.responsePipe = function(req, res, url, headers, timeout, callback) { - - var self = this; +F.onMeta = function() { - if (res.success) - return self; + var builder = ''; + var length = arguments.length; + var self = this; - var uri = parser.parse(url); - var h = {}; + for (var i = 0; i < length; i++) { - h[RESPONSE_HEADER_CACHECONTROL] = 'private'; + var arg = U.encode(arguments[i]); + if (arg == null || !arg.length) + continue; - if (headers) - utils.extend(h, headers, true); + switch (i) { + case 0: + builder += '' + (arg + (F.url !== '/' && !CONF.allow_custom_titles ? ' - ' + CONF.name : '')) + ''; + break; + case 1: + builder += ''; + break; + case 2: + builder += ''; + break; + case 3: + var tmp = arg.substring(0, 6); + var img = tmp === 'http:/' || tmp === 'https:' || arg.substring(0, 2) === '//' ? arg : self.hostname(self.public_image(arg)); + builder += ''; + break; + } + } - h['X-Powered-By'] = 'total.js v' + self.version_header; + return builder; +}; - var options = { - protocol: uri.protocol, - auth: uri.auth, - method: 'GET', - hostname: uri.hostname, - port: uri.port, - path: uri.path, - agent: false, - headers: h - }; - var connection = options.protocol === 'https:' ? https : http; - var supportsGZIP = (req.headers['accept-encoding'] || '').lastIndexOf('gzip') !== -1; +global.AUDIT = function(name, $, type, message) { - var client = connection.get(options, function(response) { + if (message == null) { + message = type; + type = null; + } - var contentType = response.headers['content-type']; - var isGZIP = (response.headers['content-encoding'] || '').lastIndexOf('gzip') !== -1; - var compress = !isGZIP && supportsGZIP && (contentType.indexOf('text/') !== -1 || contentType.lastIndexOf('javascript') !== -1 || contentType.lastIndexOf('json') !== -1); - var attachment = response.headers['content-disposition'] || ''; + var data = {}; - if (attachment.length > 0) - res.setHeader('Content-Disposition', attachment); + if ($.user) { + data.userid = $.user.id; + data.username = $.user.name || $.user.nick || $.user.alias; + } - res.setHeader(RESPONSE_HEADER_CONTENTTYPE, contentType); - res.setHeader('Vary', 'Accept-Encoding'); + if ($.req) { + if ($.req.sessionid) + data.sessionid = $.req.sessionid; + data.ua = $.req.ua; + data.ip = $.ip; + } - if (compress) { - res.setHeader('Content-Encoding', 'gzip'); - response.pipe(zlib.createGzip()).pipe(res); - return; - } + if (type) + data.type = type; - if (!supportsGZIP && isGZIP) - response.pipe(zlib.createGunzip()).pipe(res); - else - response.pipe(res); - }); + if ($.name) + data.caller = ($.schema ? ($.schema.name + '/') : '') + $.name; - if ((timeout || 0) > 0) { - client.setTimeout(timeout || 3000, function() { - self.response408(req, res); - if (callback) - callback(); - }); - } + if (F.id) + data.instance = F.id; - client.on('close', function() { + data.created = NOW = new Date(); - if (res.success) - return; + if (message) + data.message = message; - req.clear(true); - res.success = true; + DEF.onAudit(name, data); +}; - self.stats.response.pipe++; - self._request_stats(false, req.isStaticFile); - res.success = true; +global.NOSQLREADER = function(filename) { + if (!global.framework_nosql) + global.framework_nosql = require('./nosql'); + return new framework_nosql.Database('readonlynosql', filename, true); +}; - if (!req.isStaticFile) - self.emit('request-end', req, res); +global.TABLEREADER = function(filename) { + if (!global.framework_nosql) + global.framework_nosql = require('./nosql'); + return new framework_nosql.Table('readonlytable', filename, true); +}; - if (callback) - callback(); - }); +// @arguments {Object params} +global.LOG = F.log = function() { + + 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; + + for (var i = 0; i < length; i++) { + var val = arguments[i]; + if (val === undefined) + val = 'undefined'; + else if (val === null) + val = 'null'; + else if (typeof(val) === 'object') + val = Util.inspect(val); + str += (str ? ' ' : '') + val; + } + + F.path.verify('logs'); + U.queue('F.log', 5, (next) => Fs.appendFile(U.combine(CONF.directory_logs, filename + '.log'), time + ' | ' + str + '\n', next)); + return F; +}; + +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; + + for (var i = 1; i < length; i++) { + var val = arguments[i]; + if (val === undefined) + val = 'undefined'; + else if (val === null) + val = 'null'; + else if (typeof(val) === 'object') + val = Util.inspect(val); + str += (str ? ' ' : '') + val; + } + + F.path.verify('logs'); + U.queue('F.logger', 5, (next) => Fs.appendFile(U.combine(CONF.directory_logs, arguments[0] + '.log'), dt + ' | ' + str + '\n', next)); + return F; +}; + +global.LOGMAIL = F.logmail = function(address, subject, body, callback) { + + if (typeof(body) === FUNCTION) { + callback = body; + body = subject; + subject = null; + } else if (body === undefined) { + body = subject; + subject = null; + } + + if (!subject) + subject = CONF.name + ' v' + CONF.version; + + 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); + var models = Object.keys(F.models); + var helpers = Object.keys(F.helpers); + var staticFiles = Object.keys(F.temporary.path); + 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: NOW, + pid: process.pid, + node: process.version, + version: 'v' + F.version_header, + platform: process.platform, + processor: process.arch, + uptime: Math.floor(process.uptime() / 60), + memoryTotal: (memory.heapTotal / 1024 / 1024).floor(2), + memoryUsage: (memory.heapUsed / 1024 / 1024).floor(2), + memoryRss: (memory.rss / 1024 / 1024).floor(2), + 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++) + pending += U.queuecache[keys[i]].pending.length; + + output.counter = { + resource: resources.length, + controller: controllers.length, + module: modules.length, + isomorphic: isomorphic.length, + cache: cache.length, + worker: workers.length, + connection: connections.length, + schedule: schedules.length, + helpers: helpers.length, + error: F.errors.length, + problem: F.problems.length, + queue: pending, + files: staticFiles.length, + notfound: staticNotfound.length, + streaming: staticRange.length, + modificator: F.modificators ? F.modificators.length : 0, + viewphrases: $VIEWCACHE.length, + uptodates: F.uptodates ? F.uptodates.length : 0, + nosqlcleaner: nosqlcleaner.length, + commands: commands.length, + sessions: sessions.length, + shortcache: shortcache.length + }; + + output.routing = { + webpage: F.routes.web.length, + sitemap: F.routes.sitemap ? Object.keys(F.routes.sitemap).length : 0, + websocket: F.routes.websockets.length, + file: F.routes.files.length, + middleware: Object.keys(F.routes.middleware).length, + redirect: redirects.length + }; + + output.stats = F.stats; + output.redirects = redirects; + + if (!detailed) + return output; + + output.controllers = []; + 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 = []; + 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 = []; + 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 }); + } + + 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.cache = cache; + output.changes = F.changes; + output.errors = F.errors; + output.files = staticFiles; + 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)); +}; - return self; +/** + * Compiles content in the view @{compile}...@{end}. The function has controller context, this === controler. + * @param {String} name + * @param {String} html HTML content to compile + * @param {Object} model + * @return {String} + */ +// name, html, model +F.onCompileView = function(name, html) { + return html; }; /* - Response custom - @req {ServerRequest} - @res {ServerResponse} + 3rd CSS compiler (Sync) + @filename {String} + @content {String} :: Content of CSS file + return {String} */ -Framework.prototype.responseCustom = function(req, res) { +F.onCompileStyle = null; - var self = this; +/* + 3rd JavaScript compiler (Sync) + @filename {String} + @content {String} :: Content of JavaScript file + return {String} +*/ +F.onCompileScript = null; - if (res.success) - return self; +function compile_file(res) { + fsFileRead(res.options.filename, function(err, buffer) { - req.clear(true); - self.stats.response.custom++; - res.success = true; - self._request_stats(false, req.isStaticFile); + var req = res.req; + var uri = req.uri; - if (!req.isStaticFile) - self.emit('request-end', req, res); + if (err) { + F.error(err, res.options.filename, uri); + F.temporary.notfound[req.$key] = true; + delete F.temporary.processing[req.$key]; + res.$file(); + return; + } - return self; -}; + var file = F.path.temp((F.id ? 'i-' + F.id + '_' : '') + createTemporaryKey(uri.pathname)); + 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 = [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(); + }); + }); +} -/* - Response image - @req {ServerRequest} - @res {ServerResponse} - @filename {String or Stream} - @fnProcess {Function} :: function(FrameworkImage) {} - @headers {Object} :: optional, additional headers - @useImageMagick {Boolean} :: optional, use ImageMagick (otherwise is used GraphicsMagick), default false - return {Framework} -*/ -Framework.prototype.responseImage = function(req, res, filename, fnProcess, headers, useImageMagick) { +function compile_merge(res, repeated) { + + var req = res.req; + var uri = req.uri; + + var merge = F.routes.merge[uri.pathname]; + var filename = merge.filename; + + if (!DEBUG && existsSync(filename)) { + var stats = Fs.statSync(filename); + 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); + var index = 0; + var remove = null; + + merge.files.wait(function(filename, next) { + + var block; + + // Skip isomorphic + if (filename[0] !== '#') { + var blocks = filename.split('#'); + block = blocks[1]; + block && (filename = blocks[0]); + } + + if (filename.startsWith('http://') || filename.startsWith('https://')) { + U.request(filename, FLAGS_DOWNLOAD, function(err, data) { + + var output = compile_content(req.extension, framework_internal.parseBlock(block, data), filename); + + if (JSFILES[req.extension]) { + if (output[output.length - 1] !== ';') + output += ';'; + } else if (req.extension === 'html') { + if (output[output.length - 1] !== NEWLINE) + output += NEWLINE; + } + + DEBUG && merge_debug_writer(writer, filename, req.extension, index++, block); + writer.write(output); + next(); + }); + return; + } + + if (filename[0] !== '~') { + var tmp = F.path.public(filename); + if (F.isVirtualDirectory && !existsSync(tmp)) + tmp = F.path.virtual(filename); + filename = tmp; + } else + filename = filename.substring(1); + + var indexer = filename.indexOf('*'); + if (indexer !== -1) { + + var tmp = filename.substring(indexer + 1).toLowerCase(); + var len = tmp.length; + !remove && (remove = []); + + // Remove directory for all future requests + remove.push(arguments[0]); + + U.ls(filename.substring(0, indexer), function(files) { + for (var j = 0, l = files.length; j < l; j++) + merge.files.push('~' + files[j]); + next(); + }, (path, isDirectory) => isDirectory ? true : path.substring(path.length - len).toLowerCase() === tmp); + return; + } + + fsFileRead(filename, function(err, buffer) { + + if (err) { + F.error(err, merge.filename, uri); + next(); + return; + } + + var output = compile_content(req.extension, framework_internal.parseBlock(block, buffer.toString(ENCODING)), filename); + if (JSFILES[req.extension]) { + if (output[output.length - 1] !== ';') + output += ';' + NEWLINE; + } else if (req.extension === 'html') { + if (output[output.length - 1] !== NEWLINE) + output += NEWLINE; + } + + DEBUG && merge_debug_writer(writer, filename, req.extension, index++, block); + writer.write(output); + next(); + }); + + }, 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) + if (remove) { + for (var i = 0, length = remove.length; i < length; i++) + merge.files.splice(merge.files.indexOf(remove[i]), 1); + } + }); + + return F; +} - var self = this; - var stream = null; +function merge_debug_writer(writer, filename, extension, index, block) { + var plus = '==========================================================================================='; + 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 = 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; +} - if (typeof(filename) === OBJECT) - stream = filename; +F.compile_virtual = function(res) { + + var req = res.req; + var tmpname = res.options.filename.replace(CONF.directory_public, CONF.directory_public_virtual); + + if (tmpname === res.options.filename) { + F.temporary.notfound[req.$key] = true; + delete F.temporary.processing[req.$key]; + res.$file(); + return; + } + + fsFileExists(tmpname, function(e, size, sfile, stats) { + + if (!e) { + F.temporary.notfound[req.$key] = true; + delete F.temporary.processing[req.$key]; + res.$file(); + return; + } + + if (!res.noCompress && COMPRESSIONSPECIAL[req.extension] && CONF.allow_compile && !REG_NOCOMPRESS.test(res.options.filename)) { + res.options.filename = tmpname; + 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] = tmp; + delete F.temporary.processing[req.$key]; + res.$file(); + } + }); + + return; +}; + +function compile_check(res) { + + var req = res.req; + var uri = req.uri; + + if (F.routes.merge[uri.pathname]) { + compile_merge(res); + return; + } + + fsFileExists(res.options.filename, function(e, size, sfile, stats) { + + if (e) { + + if (!res.noCompress && COMPRESSIONSPECIAL[req.extension] && CONF.allow_compile && !REG_NOCOMPRESS.test(res.options.filename)) + return compile_file(res); + + 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]; + } + + } else if (F.isVirtualDirectory) + F.compile_virtual(res); + else { + F.temporary.notfound[req.$key] = true; + delete F.temporary.processing[req.$key]; + res.$file(); + } + }); +} - var key = 'image-' + req.url.substring(1); - var name = self.temporary.path[key]; +function compile_gzip(arr, callback) { - if (name === null) { - self.response404(req, res); - return self; - } + // GZIP compression - if (typeof(name) !== UNDEFINED) { - self.responseFile(req, res, filename, '', headers, key); - return self; - } + var filename = F.path.temp('file' + arr[0].hash().toString().replace('-', '0') + '.gz'); + arr.push(filename); - var im = useImageMagick; - if (typeof(im) === UNDEFINED) - im = self.config['default-image-converter'] === 'im'; + F.stats.performance.open++; + var reader = Fs.createReadStream(arr[0]); + var writer = Fs.createWriteStream(filename); - if (self.isProcessing(key)) { + CLEANUP(writer, function() { + fsFileExists(filename, function(e, size) { + arr.push(size); + callback(arr); + }); + }); - if (req.processing > self.config['default-request-timeout']) { - // timeout - self.response408(req, res); - return; - } + reader.pipe(Zlib.createGzip(GZIPFILE)).pipe(writer); + CLEANUP(reader); +} - req.processing += 500; +function compile_content(extension, content, filename) { + + if (filename && REG_NOCOMPRESS.test(filename)) + return content; + + switch (extension) { + case 'js': + case 'mjs': + return CONF.allow_compile_script ? framework_internal.compile_javascript(content, filename) : content; + + case 'css': + 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, true) + ')'); + } + } + return content; + } + + return content; +} - setTimeout(function() { - self.responseImage(req, res, filename, fnProcess, headers, im); - }, 500); +F.restore = function(filename, target, callback, filter) { + + var buffer_key = Buffer.from(':'); + var buffer_new = Buffer.from('\n'); + var buffer_dir = Buffer.from('#'); + var cache = {}; + var data = null; + var type = 0; + var item = null; + var stream = Fs.createReadStream(filename); + var index = 0; + var parser = {}; + var open = {}; + var pending = 0; + var end = false; + var output = {}; + + output.count = 0; + output.path = target; + + parser.parse_key = function() { + + index = data.indexOf(buffer_key); + if (index === -1) + return; + + index++; + item = data.slice(0, index - 1).toString('utf8').trim(); + data = data.slice(index + (data[index] === 32 ? 1 : 0)); + type = 1; + parser.next(); + }; + + parser.parse_meta = function() { + var path = Path.join(target, item); + + // Is directory? + if (data[0] === buffer_dir[0]) { + if (!cache[path]) { + cache[path] = true; + if (!filter || filter(item, true) !== false) + F.path.mkdir(path); + } + type = 3; + parser.next(); + return; + } + + if (!cache[path]) { + cache[path] = true; + + var npath = path.substring(0, path.lastIndexOf(F.isWindows ? '\\' : '/')); + + var filename = filter && filter(item, false); + + if (!filter || filename || filename == null) + F.path.mkdir(npath); + else { + type = 5; // skip + parser.next(); + return; + } + } + + if (typeof(filename) === 'string') + path = Path.join(target, filename); + + // File + type = 2; + var tmp = open[item] = {}; + tmp.path = path; + tmp.name = item; + tmp.writer = Fs.createWriteStream(path); + tmp.zlib = Zlib.createGunzip(); + tmp.zlib.$self = tmp; + pending++; + + 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); + }); + + tmp.zlib.on('end', function() { + pending--; + var tmp = this.$self; + tmp.writer.end(); + tmp.writer = null; + tmp.zlib = null; + delete open[tmp.name]; + }); + + parser.next(); + }; + + parser.parse_dir = function() { + index = data.indexOf(buffer_new); + if (index !== -1) { + data = data.slice(index + 1); + type = 0; + } + parser.next(); + }; + + parser.parse_data = function() { + + index = data.indexOf(buffer_new); + + var skip = false; + + if (index !== -1) + type = 0; + + if (type) { + var remaining = data.length % 4; + if (remaining) { + 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(Buffer.from(data.toString('ascii'), 'base64')); + data = null; + } + } else { + open[item].zlib.end(Buffer.from(data.slice(0, index).toString('ascii'), 'base64')); + data = data.slice(index + 1); + } + + !skip && data && data.length && parser.next(); + }; + + parser.next = function() { + switch (type) { + case 0: + parser.parse_key(); + break; + case 1: + parser.parse_meta(); + break; + case 2: + parser.parse_data(); + break; + case 3: + parser.parse_dir(); + break; + case 5: + index = data.indexOf(buffer_new); + if (index === -1) + data = null; + else { + data = data.slice(index + 1); + type = 0; + parser.next(); + } + break; + } + + end && !data.length && callback && callback(null, output); + }; + + parser.end = function() { + if (callback) { + if (pending) + setTimeout(parser.end, 100); + else if (end && !data.length) + callback(null, output); + } + }; + + stream.on('data', function(chunk) { + + if (data) { + CONCAT[0] = data; + CONCAT[1] = chunk; + data = Buffer.concat(CONCAT); + } else + data = chunk; + + parser.next(); + }); + + CLEANUP(stream, function() { + end = true; + parser.end(); + }); + + return F; +}; + +F.backup = function(filename, filelist, callback, filter) { + + var padding = 100; + var path = filelist instanceof Array ? F.path.root() : filelist; + + if (!(filelist instanceof Array)) + filelist = ['']; + + var counter = 0; + + Fs.unlink(filename, function() { + + filelist.sort(function(a, b) { + var ac = a.split('/'); + var bc = b.split('/'); + if (ac.length < bc.length) + return -1; + else if (ac.length > bc.length) + return 1; + 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; + + Fs.stat(file, function(err, stats) { + + if (err) { + F.error(err, 'F.backup()', filename); + return next(); + } + + if (stats.isDirectory()) { + var dir = item.replace(/\\/g, '/'); + + if (dir[dir.length - 1] !== '/') + dir += '/'; - return; - } + if (filter && !filter(dir, true)) + return next(); - var Image = require('./image'); - name = self.path.temp(key.replace(/\//g, '-')); + U.ls(file, function(f, d) { - self.temporary.processing[key] = true; + var length = path.length; + if (path[path.length - 1] === '/') + length--; - // STREAM - if (stream !== null) { + d.wait(function(item, next) { - fs.exists(name, function(exist) { + if (filter && !filter(item.substring(length), true)) { + clean(item, f); + return next(); + } - if (exist) { - delete self.temporary.processing[key]; - self.temporary.path[key] = name; - self.responseFile(req, res, name, '', headers, key); - return; - } + writer.write(item.substring(length).padRight(padding) + ':#\n', 'utf8'); + next(); + }, function() { + for (var i = 0; i < f.length; i++) + filelist.push(f[i].substring(length)); + next(); + }); + }); + return; + } - self._verify_directory('temp'); - var image = Image.load(stream, im); + if (filter && !filter(file.substring(path.length - 1), false)) + return next(); - fnProcess(image); + var data = Buffer.alloc(0); + writer.write(item.padRight(padding) + ':'); + Fs.createReadStream(file).pipe(Zlib.createGzip(GZIPFILE)).on('data', function(chunk) { - var extension = path.extname(name); - if (extension.substring(1) !== image.outputType) - name = name.substring(0, name.lastIndexOf(extension)) + '.' + image.outputType; + CONCAT[0] = data; + CONCAT[1] = chunk; + data = Buffer.concat(CONCAT); + + var remaining = data.length % 3; + if (remaining) { + writer.write(data.slice(0, data.length - remaining).toString('base64')); + data = data.slice(data.length - remaining); + } + + }).on('end', function() { + data.length && writer.write(data.toString('base64')); + writer.write('\n', 'utf8'); + counter++; + setImmediate(next); + }); + + }); + }, () => writer.end()); + }); - image.save(name, function(err) { + return F; +}; + +F.exists = function(req, res, max, callback) { - delete self.temporary.processing[key]; + if (typeof(max) === 'function') { + callback = max; + max = 10; + } - if (err) { - self.temporary.path[key] = null; - self.response500(req, res, err); - return; - } + var name = req.$key = createTemporaryKey(req); + var filename = F.path.temp(name); + var httpcachevalid = RELEASE && (req.headers['if-none-match'] === (ETAG + CONF.etag_version)); - self.temporary.path[key] = name + ';' + fs.statSync(name).size; - self.responseFile(req, res, name, '', headers, key); - }); + if (F.isProcessed(name) || httpcachevalid) { + res.options.filename = filename; + res.$file(); + return F; + } - }); + U.queue('F.exists', max, function(next) { + fsFileExists(filename, function(e) { + if (e) { + res.options.filename = filename; + res.options.callback = next; + res.$file(); + } else + callback(next, filename, req, res); + }); + }); - return self; - } + return F; +}; - // FILENAME - fs.exists(filename, function(exist) { +/** + * Is processed static file? + * @param {String / Request} filename Filename or Request object. + * @return {Boolean} + */ +F.isProcessed = function(filename) { - if (!exist) { - delete self.temporary.processing[key]; - self.temporary.path[key] = null; - self.response404(req, res); - return; - } + if (filename.url) { + var name = filename.url; + var index = name.indexOf('?'); + if (index !== -1) + name = name.substring(0, index); + filename = F.path.public($decodeURIComponent(name)); + } - self._verify_directory('temp'); + return !F.temporary.notfound[filename] && F.temporary.path[filename] !== undefined; +}; - var image = Image.load(filename, im); +/** + * Processing + * @param {String / Request} filename Filename or Request object. + * @return {Boolean} + */ +F.isProcessing = function(filename) { - fnProcess(image); + if (!filename.url) + return !!F.temporary.processing[filename]; - var extension = path.extname(name); - if (extension.substring(1) !== image.outputType) - name = name.substring(0, name.lastIndexOf(extension)) + '.' + image.outputType; + var name = filename.url; + var index = name.indexOf('?'); - image.save(name, function(err) { + if (index !== -1) + name = name.substring(0, index); - delete self.temporary.processing[key]; + filename = U.combine(CONF.directory_public, $decodeURIComponent(name)); + return !!F.temporary.processing[filename]; +}; - if (err) { - self.temporary.path[key] = null; - self.response500(req, res, err); - return; - } +/** + * Clears file information in release mode + * @param {String/Request} url + * @return {Framework} + */ +F.touch = function(url) { + if (url) { + var key = createTemporaryKey(url); + delete F.temporary.path[key]; + delete F.temporary.notfound[key]; + } else { + F.temporary.path = {}; + F.temporary.notfound = {}; + } + return F; +}; + +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; +}; + +global.LOAD = F.load = function(debug, types, pwd, ready) { + + if (typeof(types) === 'function') { + ready = types; + types = null; + } + + if (typeof(pwd) === 'function') { + ready = pwd; + pwd = null; + } + + if (!types) + types = ['nobundles', 'nopackages', 'nocomponents', 'nothemes']; + + 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(debug) === 'string') { + switch (debug.toLowerCase().replace(/\.|\s/g, '-')) { + case 'release': + case 'production': + debug = false; + break; + + case 'debug': + case 'develop': + case 'development': + debug = true; + break; + + case 'test-debug': + case 'debug-test': + case 'testing-debug': + debug = true; + F.isTest = true; + break; + + 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; + } + } + + F.isWorker = true; + F.isDebug = debug; + + global.isWORKER = true; + global.DEBUG = debug; + global.RELEASE = !debug; + global.I = global.isomorphic = F.isomorphic; + + 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; + }; + + F.$bundle(function() { + F.consoledebug('startup'); + F.$startup(function() { + + F.consoledebug('startup (done)'); + F.$configure_env(); + F.$configure_configs(); + + if (can('versions')) + F.$configure_versions(); + + if (can('workflows')) + F.$configure_workflows(); + + if (can('sitemap')) + F.$configure_sitemap(); + + F.consoledebug('init'); + + var noservice = true; + + for (var i = 0; i < types.length; i++) { + switch(types[i]) { + case 'service': + case 'services': + noservice = false; + break; + } + if (!noservice) + break; + } + + F.cache.init(noservice); + EMIT('init'); + + F.$load(types, directory, function() { + + F.isLoaded = true; + + process.send && process.send('total:ready'); + + setTimeout(function() { + + try { + EMIT('load'); + EMIT('ready'); + } catch (err) { + F.error(err, 'ON("load/ready")'); + } + + ready && ready(); + + F.removeAllListeners('load'); + F.removeAllListeners('ready'); + + if (F.isTest) { + F.console(); + F.test(); + return F; + } + + // Because this is worker + // setTimeout(function() { + // if (!F.isTest) + // delete F.test; + // }, 5000); - self.temporary.path[key] = name + ';' + fs.statSync(name).size; - self.responseFile(req, res, name, '', headers, key); - }); + }, 500); - }); + if (CONF.allow_debug) { + F.consoledebug('done'); + F.usagesnapshot(); + } + }); + }); + }, can('bundles')); - return self; + return F; }; -/* - Response image - @req {ServerRequest} - @res {ServerResponse} - @filename {String or Stream} - @fnProcess {Function} :: function(FrameworkImage) {} - @headers {Object} :: optional, additional headers - @useImageMagick {Boolean} :: optional, use ImageMagick (otherwise is used GraphicsMagick), default false - return {Framework} -*/ -Framework.prototype.responseImageWithoutCache = function(req, res, filename, fnProcess, headers, useImageMagick) { +/** + * Initialize framework + * @param {Object} http + * @param {Boolean} debug + * @param {Object} options + * @return {Framework} + */ +F.initialize = function(http, debug, options) { - var self = this; - var stream = null; + if (!options) + options = {}; - if (typeof(filename) === OBJECT) - stream = filename; + var port = options.port; + var ip = options.ip; + var listenpath = options.listenpath; - var key = 'image-' + req.url.substring(1); - var im = useImageMagick; - if (typeof(im) === UNDEFINED) - im = self.config['default-image-converter'] === 'im'; + if (options.thread) + global.THREAD = options.thread; + options.config && U.extend_headers2(CONF, options.config); - if (self.isProcessing(key)) { + if (options.debug || options['allow-debug'] || options.allow_debug) + CONF.allow_debug = true; - if (req.processing > self.config['default-request-timeout']) { - // timeout - self.response408(req, res); - return; - } + F.isHTTPS = http.STATUS_CODES === undefined; - req.processing += 500; + if (isNaN(port) && typeof(port) !== 'string') + port = null; - setTimeout(function() { - self.responseImageWithoutCache(req, res, filename, fnProcess, headers, im); - }, 500); + if (options.id) + F.id = options.id; - return; - } + F.isDebug = debug; - var Image = require('./image'); + if (options.bundling != null) + F.$bundling = options.bundling == true; - // STREAM - if (stream !== null) { - var image = Image.load(stream, im); - fnProcess(image); - self.responseStream(req, res, utils.getContentType(image.outputType), image.stream(), null, headers); - return self; - } + global.DEBUG = debug; + global.RELEASE = !debug; + global.I = global.isomorphic = F.isomorphic; - // FILENAME - fs.exists(filename, function(exist) { + if (options.tests) { + F.testlist = options.tests; + for (var i = 0; i < F.testlist.length; i++) + F.testlist[i] = F.testlist[i].replace(/\.js$/, ''); + } - if (!exist) { - self.response404(req, res); - return; - } + F.$bundle(function() { - self._verify_directory('temp'); - var image = Image.load(filename, im); - fnProcess(image); - self.responseStream(req, res, utils.getContentType(image.outputType), image.stream(), null, headers); + 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'); - }); - return self; -}; + if (!port) { + if (CONF.default_port === 'auto') { + var envPort = +(process.env.PORT || ''); + if (!isNaN(envPort)) + port = envPort; + } else + port = CONF.default_port; + } + + F.port = port || 8000; -/* - Response stream - @req {ServerRequest} - @res {ServerResponse} - @contentType {String} - @stream {ReadStream} - @downloadName {String} :: optional - @headers {Object} :: optional - return {Framework} -*/ -Framework.prototype.responseStream = function(req, res, contentType, stream, downloadName, headers) { + 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; + + if (F.ip == null) + F.ip = '0.0.0.0'; + + !listenpath && (listenpath = CONF.default_listenpath); + F.listenpath = listenpath; + + 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.server.close(); + } + + var listen = function() { + + if (options.https) { + + var meta = options.https; + + 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); + + CONF.allow_performance && F.server.on('connection', connection_tunning); + F.initwebsocket && F.initwebsocket(); + F.consoledebug('HTTP listening'); + + if (listenpath) + F.server.listen(listenpath); + else + F.server.listen(F.port, F.ip); + }; + + // clears static files + F.consoledebug('clear temporary'); + F.clear(function() { + + F.consoledebug('clear temporary (done)'); + F.$load(undefined, directory, function() { + + F.isLoaded = true; + process.send && process.send('total:ready'); + + if (options.middleware) + options.middleware(listen); + else + listen(); + + if (CONF.allow_debug) { + F.consoledebug('done'); + F.usagesnapshot(); + } + + if (!process.connected) + F.console(); + + setTimeout(function() { + + 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; +}; + +function connection_tunning(socket) { + socket.setNoDelay(true); + socket.setKeepAlive(true, 10); +} + +/** + * Run framework –> HTTP + * @param {String} mode Framework mode. + * @param {Object} options Framework settings. + * @param {Function(listen)} middleware A middleware for manual calling of HTTP listener + * @return {Framework} + */ +F.http = function(mode, options, middleware) { + F.consoledebug('begin'); - var self = this; + if (typeof(options) === 'function') { + middleware = options; + options = null; + } - if (res.success) - return self; + options == null && (options = {}); + !options.port && (options.port = +process.argv[2]); - req.clear(true); + if (options.port && isNaN(options.port)) + options.port = 0; - if (contentType.lastIndexOf('/') === -1) - contentType = utils.getContentType(contentType); + if (typeof(middleware) === 'function') + options.middleware = middleware; - var compress = self.config['allow-gzip'] && REQUEST_COMPRESS_CONTENTTYPE.indexOf(contentType) !== -1; - var accept = req.headers['accept-encoding'] || ''; - var returnHeaders = {}; + if (options.bundling != null) + F.$bundling = options.bundling; + + var http = require('http'); + extend_request(http.IncomingMessage.prototype); + extend_response(http.ServerResponse.prototype); + return F.mode(http, mode, options); +}; - returnHeaders[RESPONSE_HEADER_CACHECONTROL] = 'public'; - returnHeaders['Expires'] = new Date().add('d', 15); - returnHeaders['Vary'] = 'Accept-Encoding'; +/** + * Run framework –> HTTPS + * @param {String} mode Framework mode. + * @param {Object} options Framework settings. + * @param {Function(listen)} middleware A middleware for manual calling of HTTP listener + * @return {Framework} + */ +F.https = function(mode, options, middleware) { + F.consoledebug('begin'); + var http = require('http'); + + if (typeof(options) === 'function') { + middleware = options; + options = null; + } + + 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; + + extend_request(http.IncomingMessage.prototype); + extend_response(http.ServerResponse.prototype); + return F.mode(require('https'), mode, options); +}; + +F.mode = function(http, name, options) { + + var debug = false; + + if (options.directory) + F.directory = directory = options.directory; + + if (typeof(http) === 'string') { + switch (http) { + case 'debug': + case 'development': + debug = true; + break; + } + DEBUG = debug; + CONF.trace = debug; + F.isDebug = debug; + global.DEBUG = debug; + global.RELEASE = !debug; + return F; + } + + F.isWorker = false; + + switch (name.toLowerCase().replace(/\.|\s/g, '-')) { + case 'release': + case 'production': + break; + + case 'debug': + case 'develop': + case 'development': + debug = true; + break; + + case 'test-debug': + case 'debug-test': + case 'testing-debug': + debug = true; + F.isTest = true; + break; + + 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; + } + + CONF.trace = debug; + F.consoledebug('startup'); + F.$startup(function() { + F.consoledebug('startup (done)'); + F.initialize(http, debug, options); + }); + return F; +}; + +F.custom = function(mode, http, request, response, options) { + var debug = false; + + if (options.directory) + F.directory = directory = options.directory; + + F.consoledebug('begin'); + + extend_request(request); + extend_response(response); + + switch (mode.toLowerCase().replace(/\.|\s/g, '-')) { + case 'release': + case 'production': + break; + + case 'debug': + case 'develop': + case 'development': + debug = true; + break; + + case 'test': + case 'testing': + case 'test-debug': + case 'debug-test': + case 'testing-debug': + debug = true; + F.isTest = true; + break; + + case 'test-release': + case 'release-test': + case 'testing-release': + case 'test-production': + case 'testing-production': + debug = false; + break; + } + + CONF.trace = debug; + F.consoledebug('startup'); + F.$startup(function() { + F.consoledebug('startup (done)'); + 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()); + + // 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 : ' + 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'); + + 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' + (F.id ? ('-' + F.id) : '') + '.log'), JSON.stringify(F.usage(true), null, '\t'), NOOP); + return F; +}; + +F.consoledebug = function() { + + if (!CONF.allow_debug) + return F; + + var arr = [new Date().format('yyyy-MM-dd HH:mm:ss'), '--------->']; + for (var i = 0; i < arguments.length; i++) + arr.push(arguments[i]); + console.log.apply(console, arr); + return F; +}; - if (headers) - utils.extend(returnHeaders, headers, true); +/** + * Re-connect server + * @return {Framework} + */ +F.reconnect = function() { + 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; +}; - downloadName = downloadName || ''; +/** + * Internal service + * @private + * @param {Number} count Run count. + * @return {Framework} + */ +F.service = function(count) { + + 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 = {}; + + 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 = NOW.add('-5 minutes'); + for (var key in F.databases) + F.databases[key] && F.databases[key].inmemorylastusage < dt && F.databases[key].release(); + + releasegc = true; + 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 % 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 % 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 = CONF.default_interval_websocket_ping; + if (ping > 0 && count % ping === 0) { + var has = false; + for (var item in F.connections) { + var conn = F.connections[item]; + if (conn) { + conn.check(); + conn.ping(); + has = true; + } + } + has && CONF.allow_debug && F.consoledebug('ping websocket connections'); + } + + // 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) > NOW) + return next(); + + item.updated = NOW; + item.count++; + + setTimeout(function() { + CONF.allow_debug && F.consoledebug('uptodate', item.type + '#' + item.url); + F.install(item.type, item.url, item.options, function(err, name, skip) { + + CONF.allow_debug && F.consoledebug('uptodate', item.type + '#' + item.url + ' (done)'); + + if (skip) + return next(); + + if (err) { + item.errors.push(err); + item.errors.length > 50 && F.errors.shift(); + } else { + hasUpdate = true; + item.name = name; + F.$events.uptodate && EMIT('uptodate', item.type, name); + } + + item.callback && item.callback(err, name); + next(); + + }, undefined, undefined, undefined, undefined, item.name); + + }, item.name ? 500 : 1); + + }, function() { + if (hasUpdate) { + F.temporary.path = {}; + F.temporary.range = {}; + F.temporary.views = {}; + F.temporary.other = {}; + global.$VIEWCACHE && global.$VIEWCACHE.length && (global.$VIEWCACHE = []); + } + }); + } + + // every 20 minutes (default) service clears resources + if (count % CONF.default_interval_clear_resources === 0) { + F.$events.clear && EMIT('clear', 'resources'); + F.resources = {}; + releasegc = true; + 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 = 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 && EMIT('service', count); + + if (CONF.allow_debug) { + F.consoledebug('service ({0}x)'.format(count)); + F.usagesnapshot(); + } + + releasegc && global.gc && setTimeout(function() { + global.gc(); + CONF.allow_debug && F.consoledebug('gc()'); + }, 1000); + + if (WORKERID > 9999999999) + WORKERID = 0; + + // Run schedules + keys = Object.keys(F.schedules); + + if (!keys.length) + return F; + + var expire = NOW.getTime(); + + 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; +}; - if (downloadName.length > 0) - returnHeaders['Content-Disposition'] = 'attachment; filename=' + encodeURIComponent(downloadName); +/** + * Request processing + * @private + * @param {Request} req + * @param {Response} res + */ +F.listener = function(req, res) { + + req.options = res.options = {}; + res.req = req; + req.res = res; + + if (F._length_wait) + return F.response503(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 && EMIT('request', req, res); + + if (F._request_check_redirect) { + var redirect = F.routes.redirects[req.$protocol + '://' + req.host]; + if (redirect) { + F.stats.response.forward++; + res.options.url = redirect.url + (redirect.path ? req.url : ''); + res.options.permanent = redirect.permanent; + res.$redirect(); + return; + } + } + + 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 = 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) + async_middleware(0, req, res, F.routes.request, requestcontinue_middleware); + else + F.$requestcontinue(req, res, headers); +}; + +function onrequesterror() { + F.reqstats(false); + this.success = true; + if (this.res) + this.res.$aborted = true; +} - returnHeaders[RESPONSE_HEADER_CONTENTTYPE] = contentType; +function requestcontinue_middleware(req, res) { + if (req.$total_middleware) + req.$total_middleware = null; + F.$requestcontinue(req, res, req.headers); +} - if (compress && accept.lastIndexOf('gzip') !== -1) { +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); +} - returnHeaders['Content-Encoding'] = 'gzip'; - res.writeHead(200, returnHeaders); - var gzip = zlib.createGzip(); - stream.pipe(gzip).pipe(res); +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)); +} - self.stats.response.stream++; - self._request_stats(false, req.isStaticFile); +function makeproxycallback(response) { + this.$proxy.after && this.proxy.after(response); + this.$res.writeHead(response.statusCode, response.headers); + response.pipe(this.$res, PROXYOPTIONS); +} - if (!req.isStaticFile) - self.emit('request-end', req, res); - return self; - } +const TRAVELCHARS = { e: 1, E: 1 }; - res.writeHead(200, returnHeaders); - stream.pipe(res); +/** + * Continue to process + * @private + * @param {Request} req + * @param {Response} res + * @param {Object} headers + * @param {String} protocol [description] + * @return {Framework} + */ +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; + } + + 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]; + + var flags = [req.method.toLowerCase()]; + var multipart; + + if (F._request_check_mobile && req.mobile) { + req.$flags += 'a'; + F.stats.request.mobile++; + } else + F.stats.request.desktop++; + + req.$protocol[5] && (req.$flags += req.$protocol[5]); + req.$type = 0; + flags.push(req.$protocol); + + var method = req.method; + var first = method[0]; + + if (first === 'P' || first === 'D') { + multipart = req.headers['content-type'] || ''; + 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'; + flags.push('json'); + req.$type = 1; + multipart = ''; + break; + case 'oded': + req.$type = 3; + multipart = ''; + break; + case 'data': + req.$flags += 'c'; + req.$upload = true; + flags.push('upload'); + break; + case '/xml': + req.$flags += 'd'; + flags.push('xml'); + req.$type = 2; + multipart = ''; + break; + default: + if (multipart) { + // 'undefined' DATA + multipart = ''; + flags.push('raw'); + } else { + req.$type = 3; + multipart = ''; + } + break; + } + } + + if (headers.accept === 'text/event-stream') { + req.$flags += 'g'; + flags.push('sse'); + } + + if (DEBUG) { + req.$flags += 'h'; + flags.push('debug'); + } + + if (req.xhr) { + F.stats.request.xhr++; + req.$flags += 'i'; + flags.push('xhr'); + } + + if (F._request_check_robot && req.robot) + req.$flags += 'j'; + + if (F._request_check_referer) { + var referer = headers['referer']; + if (referer && referer.indexOf(headers['host']) !== -1) { + req.$flags += 'k'; + flags.push('referer'); + } + } + + req.flags = flags; + + 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': + F.stats.request.get++; + if (isCORS) + F.$cors(req, res, cors_callback0); + else + req.$total_end(); + return; + + case 'O': + F.stats.request.options++; + if (isCORS) + F.$cors(req, res, cors_callback0); + else + req.$total_end(); + return; + + case 'H': + F.stats.request.head++; + if (isCORS) + F.$cors(req, res, cors_callback0); + else + req.$total_end(); + return; + + case 'D': + F.stats.request['delete']++; + if (isCORS) + F.$cors(req, res, cors_callback1); + else + req.$total_urlencoded(); + return; + + case 'P': + if (F._request_check_POST) { + if (multipart) { + if (isCORS) + F.$cors(req, res, cors_callback_multipart, multipart); + else + req.$total_multipart(multipart); + } else { + if (method === 'PUT') + F.stats.request.put++; + else if (method === 'PATCH') + F.stats.request.patch++; + else + F.stats.request.post++; + if (isCORS) + F.$cors(req, res, cors_callback1); + else + req.$total_urlencoded(); + } + return; + } + break; + } + + req.$total_status(404); +}; + +function cors_callback0(req) { + req.$total_end(); +} - self.stats.response.stream++; - self._request_stats(false, req.isStaticFile); +function cors_callback1(req) { + req.$total_urlencoded(); +} - if (!req.isStaticFile) - self.emit('request-end', req, res); +function cors_callback_multipart(req, res, multipart) { + req.$total_multipart(multipart); +} - return self; +F.$cors = function(req, res, fn, arg) { + + var isAllowed = F.routes.corsall; + var cors, origin; + var headers = req.headers; + var key; + + if (!isAllowed) { + + 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; + + 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 (!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; + } + } + + 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 (stop) + origin = 'null'; + else + origin = headers.origin; + + res.setHeader('Access-Control-Allow-Origin', origin); + + if (!cors || cors.credentials) + res.setHeader('Access-Control-Allow-Credentials', 'true'); + + var name = 'Access-Control-Allow-Methods'; + var isOPTIONS = req.method === 'OPTIONS'; + + 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 && cors.headers) + res.setHeader(name, cors.headers.join(', ')); + else + res.setHeader(name, headers['access-control-request-headers'] || '*'); + + cors && cors.age && res.setHeader('Access-Control-Max-Age', cors.age); + + if (stop) { + fn = null; + 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); + res.end(); + return; + } + + if (!isOPTIONS) + return fn(req, res, arg); + + fn = null; + 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(); + return F; }; -/* - Internal :: Response Range - @name {String} - @range {String} - @headers {Object} - @res {ServerResponse} - @req {ServerRequest} - return {Framework} -*/ -Framework.prototype.responseRange = function(name, range, headers, req, res) { +/** + * Upgrade HTTP (WebSocket) + * @param {HttpRequest} req + * @param {Socket} socket + * @param {Buffer} head + */ +const REGWS = /websocket/i; +F.$upgrade = function(req, socket, head) { - var self = this; - var arr = range.replace(/bytes=/, '').split('-'); - var beg = parseInt(arr[0] || '0', 10); - var end = parseInt(arr[1] || '0', 10); - var total = self.temporary.range[name] || 0; + if (!REGWS.test(req.headers.upgrade || '') || F._length_wait) + return; - if (total === 0) { - // sync - total = fs.statSync(name).size; - self.temporary.range[name] = total; - } + // disables timeout + socket.setTimeout(0); + socket.on('error', NOOP); - if (end === 0) - end = total - 1; + var headers = req.headers; + req.$protocol = req.connection.encrypted || headers['x-forwarded-protocol'] === 'https' ? 'https' : 'http'; - if (beg > end) { - beg = 0; - end = total - 1; - } + req.uri = framework_internal.parseURI(req); - var length = (end - beg) + 1; + F.$events.websocket && EMIT('websocket', req, socket, head); + F.stats.request.websocket++; - headers['Content-Length'] = length; - headers['Content-Range'] = 'bytes ' + beg + '-' + end + '/' + total; + req.session = null; + req.user = null; + req.flags = [req.secured ? 'https' : 'http', 'get']; - res.writeHead(206, headers); - var stream = fs.createReadStream(name, { - start: beg, - end: end - }); - stream.pipe(res); + req.$wspath = U.path(req.uri.pathname); + var websocket = new WebSocketClient(req, socket, head); - self.stats.response.streaming++; - self._request_stats(false, req.isStaticFile); + req.path = framework_internal.routeSplit(req.uri.pathname); + req.websocket = websocket; - if (!req.isStaticFile) - self.emit('request-end', req, res); + if (F.onLocale) + req.$language = F.onLocale(req, socket); - return self; + if (F._length_request_middleware) + async_middleware(0, req, req.websocket, F.routes.request, websocketcontinue_middleware); + else + F.$websocketcontinue(req, req.$wspath, headers); }; -/* - Set last modified header or Etag - @req {ServerRequest} - @res {ServerResponse} - @value {String or Date} +function websocketcontinue_middleware(req) { + if (req.$total_middleware) + req.$total_middleware = null; + F.$websocketcontinue(req, req.$wspath, req.headers); +} - if @value === {String} set ETag - if @value === {Date} set LastModified +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'); +} - return {Controller}; -*/ -Framework.prototype.setModified = function(req, res, value) { +F.$websocketcontinue = function(req, path) { + 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 (!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 = 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(4004, '404: not found'); + } +}; + +F.$websocketcontinue_process = function(route, req, path) { + + var socket = req.websocket; + + if (!socket.prepare(route.flags, route.protocols, route.allow, route.length)) { + socket.$close(4001, '401: unauthorized'); + return; + } + + var id = path + (route.flags.length ? '#' + route.flags.join('-') : ''); + + if (route.isBINARY) + socket.type = 1; + 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; + route.onInitialize.apply(connection, framework_internal.routeParam(route.param.length ? req.split : req.path, route)); + setImmediate(next_upgrade_continue, socket, connection); + }; + + if (route.middleware) + async_middleware(0, req, req.websocket, route.middleware, next, route.options); + else + next(); +}; + +function next_upgrade_continue(socket, connection) { + socket.upgrade(connection); +} - var self = this; - var isEtag = typeof(value) === STRING; +/** + * Request statistics writer + * @private + * @param {Boolean} beg + * @param {Boolean} isStaticFile + * @return {Framework} + */ +F.reqstats = function(beg) { - if (isEtag) { - res.setHeader('Etag', value + ':' + self.config['etag-version']); - return self; - } + if (beg) + F.stats.request.pending++; + else + F.stats.request.pending--; - value = value || new Date(); - res.setHeader('Last-Modified', value.toUTCString()); + if (F.stats.request.pending < 0) + F.stats.request.pending = 0; - return self; + return F; }; -/* - Check if ETag or Last Modified has modified - @req {ServerRequest} - @res {ServerResponse} - @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 - - this method automatically flush response (if not modified) - --> response 304 +/** + * Get a model + * @param {String} name + * @return {Object} + */ +global.MODEL = F.model = function(name) { + var obj = F.models[name]; + if (obj || obj === null) + return obj; + 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; +}; - return {Boolean}; -*/ -Framework.prototype.notModified = function(req, res, compare, strict) { +/** + * Load a source code + * @param {String} name + * @param {Object} options Custom initial options, optional. + * @return {Object} + */ +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(CONF.directory_source, name + '.js'); + existsSync(filename) && F.install('source', name, filename, options, callback, undefined, true); + return F.sources[name] || null; +}; - var self = this; - var type = typeof(compare); +/** + * Load a source code (alias for F.source()) + * @param {String} name + * @param {Object} options Custom initial options, optional. + * @return {Object} + */ +F.include = function(name, options, callback) { + return F.source(name, options, callback); +}; - if (type === BOOLEAN) { - var tmp = compare; - compare = strict; - strict = tmp; - type = typeof(compare); - } +/** + * Send e-mail + * @param {String or Array} address E-mail address. + * @param {String} subject E-mail subject. + * @param {String} view View name. + * @param {Object} model Optional. + * @param {Function(err)} callback Optional. + * @param {String} language Optional. + * @return {MailMessage} + */ +global.MAIL = F.mail = function(address, subject, view, model, callback, language) { - var isEtag = type === STRING; + if (typeof(callback) === 'string') { + var tmp = language; + language = callback; + callback = tmp; + } - var val = req.headers[isEtag ? 'if-none-match' : 'if-modified-since']; + var controller = EMPTYCONTROLLER; + controller.layoutName = ''; + controller.themeName = U.parseTheme(view); - if (isEtag) { + if (controller.themeName) + view = prepare_viewname(view); + else if (F.onTheme) + controller.themeName = F.onTheme(controller); + else + controller.themeName = ''; - if (typeof(val) === UNDEFINED) - return false; + var replyTo; - var myetag = compare + ':' + self.config['etag-version']; + // Translation + if (typeof(language) === 'string') { + subject = subject.indexOf('@(') === -1 ? F.translate(language, subject) : F.translator(language, subject); + controller.language = language; + } - if (val !== myetag) - return false; + var mail = controller.mail(address, subject, view, model, callback, replyTo); - } else { + if (language != null) + mail.language = language; - if (typeof(val) === UNDEFINED) - return false; + return mail; +}; - var date = typeof(compare) === UNDEFINED ? new Date().toUTCString() : compare.toUTCString(); +/** + * Renders view + * @param {String} name View name. + * @param {Object} model Model. + * @param {String} layout Layout for the view, optional. Default without layout. + * @param {Object} repository A repository object, optional. Default empty. + * @param {String} language Optional. + * @return {String} + */ +global.VIEW = function(name, model, layout, repository, language) { + var controller = EMPTYCONTROLLER; - 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 (typeof(layout) === 'object') { + var tmp = repository; + repository = layout; + layout = tmp; + } - res.success = true; - res.writeHead(304); - res.end(); + controller.layoutName = layout || ''; + controller.language = language || ''; + controller.repository = typeof(repository) === 'object' && repository ? repository : EMPTYOBJECT; - self.stats.response.notModified++; - self._request_stats(false, req.isStaticFile); + var theme = U.parseTheme(name); + if (theme) { + controller.themeName = theme; + name = prepare_viewname(name); + } else if (F.onTheme) + controller.themeName = F.onTheme(controller); + else + controller.themeName = undefined; - if (!req.isStaticFile) - self.emit('request-end', req, res); + return controller.view(name, model, true); +}; - return 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); }; -/* - Response with 400 error - @req {ServerRequest} - @res {ServerResponse} - return {Framework} -*/ -Framework.prototype.response400 = function(req, res) { - var self = this; +/** + * Compiles and renders view + * @param {String} body HTML body. + * @param {Object} model Model. + * @param {String} layout Layout for the view, optional. Default without layout. + * @param {Object} repository A repository object, optional. Default empty. + * @param {String} language Optional. + * @return {String} + */ +global.VIEWCOMPILE = function(body, model, layout, repository, language) { - if (res.success) - return self; + var controller = EMPTYCONTROLLER; - self._request_stats(false, req.isStaticFile); - req.clear(true); + if (typeof(layout) === 'object') { + var tmp = repository; + repository = layout; + layout = tmp; + } - res.success = true; + controller.layoutName = layout || ''; + controller.language = language || ''; + controller.themeName = undefined; + controller.repository = typeof(repository) === 'object' && repository ? repository : EMPTYOBJECT; - var headers = {}; - var status = 400; - headers[RESPONSE_HEADER_CONTENTTYPE] = CONTENTTYPE_TEXTPLAIN; - res.writeHead(status, headers); - res.end(utils.httpStatus(status)); + return controller.view_compile(body, model, true); +}; - if (!req.isStaticFile) - self.emit('request-end', req, res); +F.viewCompile = function(body, model, layout, repository, language) { + OBSOLETE('F.viewCompile()', 'Instead of F.viewCompile() use VIEWCOMPILE()'); + return VIEWCOMPILE(body, model, layout, repository, language); +}; - self.stats.response.error400++; - return self; +/** + * Load tests + * @private + * @param {Boolean} stop Stop framework after end. + * @param {String Array} names Test names, optional. + * @param {Function()} cb + * @return {Framework} + */ +F.test = function() { + F.isTest = true; + F.$configure_configs('config-test', true); + require('./test').load(); + return F; }; -/* - Response with 401 error - @req {ServerRequest} - @res {ServerResponse} - return {Framework} -*/ -Framework.prototype.response401 = function(req, res) { - var self = this; +/** + * Clear temporary directory + * @param {Function} callback + * @param {Boolean} isInit Private argument. + * @return {Framework} + */ +F.clear = function(callback, isInit) { + + var dir = F.path.temp(); + var plus = F.id ? 'i-' + F.id + '_' : ''; + + if (isInit) { + 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'; + }); + } + return F; + } + } + + if (!existsSync(dir) || !F.$bundling) { + callback && callback(); + return F; + } + + U.ls(dir, function(files, directories) { + + if (isInit) { + var arr = []; + for (var i = 0, length = files.length; i < length; i++) { + var filename = files[i].substring(dir.length); + if (plus && !filename.startsWith(plus)) + continue; + (filename.indexOf('/') === -1 || filename.indexOf('.package/') !== -1) && !filename.endsWith('.jsoncache') && arr.push(files[i]); + } + + files = arr; + 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)); + }); + + if (!isInit) { + // clear static cache + F.temporary.path = {}; + F.temporary.range = {}; + F.temporary.notfound = {}; + } + + return F; +}; - if (res.success) - return self; +/** + * Remove files in array + * @param {String Array} arr File list. + * @param {Function} callback + * @return {Framework} + */ +F.unlink = F.path.unlink = function(arr, callback) { - self._request_stats(false, req.isStaticFile); - req.clear(true); + if (typeof(arr) === 'string') + arr = [arr]; - res.success = true; - var headers = {}; - var status = 401; - headers[RESPONSE_HEADER_CONTENTTYPE] = CONTENTTYPE_TEXTPLAIN; - res.writeHead(status, headers); - res.end(utils.httpStatus(status)); + if (!arr.length) { + callback && callback(); + return F; + } - if (!req.isStaticFile) - self.emit('request-end', req, res); + var filename = arr.shift(); + if (filename) + Fs.unlink(filename, () => F.unlink(arr, callback)); + else + callback && callback(); - self.stats.response.error401++; - return self; + return F; }; -/* - Response with 403 error - @req {ServerRequest} - @res {ServerResponse} - return {Framework} -*/ -Framework.prototype.response403 = function(req, res) { - var self = this; +/** + * Remove directories in array + * @param {String Array} arr + * @param {Function} callback + * @return {Framework} + */ +F.rmdir = F.path.rmdir = function(arr, callback) { + if (typeof(arr) === 'string') + arr = [arr]; + + if (!arr.length) { + callback && callback(); + return F; + } + + var path = arr.shift(); + if (path) { + U.ls(path, function(files, directories) { + directories.reverse(); + directories.push(path); + files.wait((item, next) => Fs.unlink(item, next), function() { + directories.wait(function(item, next) { + Fs.rmdir(item, next); + }, () => F.rmdir(arr, callback)); + }); + }); + } else + callback && callback(); + + return F; +}; - if (res.success) - return self; +/** + * Cryptography (encrypt) + * @param {String} value + * @param {String} key Encrypt key. + * @param {Boolean} isUnique Optional, default true. + * @return {String} + */ +global.ENCRYPT = F.encrypt = function(value, key, isUnique) { - self._request_stats(false, req.isStaticFile); - req.clear(true); + if (value == null) + return ''; - res.success = true; - var headers = {}; - var status = 403; - headers[RESPONSE_HEADER_CONTENTTYPE] = CONTENTTYPE_TEXTPLAIN; - res.writeHead(status, headers); - res.end(utils.httpStatus(status)); + var type = typeof(value); - if (!req.isStaticFile) - self.emit('request-end', req, res); + if (typeof(key) === 'boolean') { + var tmp = isUnique; + isUnique = key; + key = tmp; + } - self.stats.response.error403++; - return self; -}; + if (type === 'function') + value = value(); + else if (type === 'number') + value = value.toString(); + else if (type === 'object') + value = JSON.stringify(value); -/* - Response with 404 error - @req {ServerRequest} - @res {ServerResponse} - return {Framework} -*/ -Framework.prototype.response404 = function(req, res) { - var self = this; + if (CONF.default_crypto) { + key = (key || '') + CONF.secret; - if (res.success) - return self; + if (key.length < 32) + key += ''.padLeft(32 - key.length, '0'); - self._request_stats(false, req.isStaticFile); - req.clear(true); + if (key.length > 32) + key = key.substring(0, 32); - res.success = true; - var headers = {}; - var status = 404; - headers[RESPONSE_HEADER_CONTENTTYPE] = CONTENTTYPE_TEXTPLAIN; - res.writeHead(status, headers); - res.end(utils.httpStatus(status)); + if (!F.temporary.keys[key]) + F.temporary.keys[key] = Buffer.from(key); - if (!req.isStaticFile) - self.emit('request-end', req, res); + 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'); + } - self.stats.response.error404++; - return self; + return value.encrypt(CONF.secret + '=' + key, isUnique); }; -/* - Response with 408 error - @req {ServerRequest} - @res {ServerResponse} - return {Framework} -*/ -Framework.prototype.response408 = function(req, res) { - var self = this; +/** + * Cryptography (decrypt) + * @param {String} value + * @param {String} key Decrypt key. + * @param {Boolean} jsonConvert Optional, default true. + * @return {Object or String} + */ +global.DECRYPT = F.decrypt = function(value, key, jsonConvert) { - if (res.success) - return self; + if (typeof(key) === 'boolean') { + var tmp = jsonConvert; + jsonConvert = key; + key = tmp; + } - self._request_stats(false, req.isStaticFile); - req.clear(true); - res.success = true; + if (typeof(jsonConvert) !== 'boolean') + jsonConvert = true; - var headers = {}; - var status = 408; - headers[RESPONSE_HEADER_CONTENTTYPE] = CONTENTTYPE_TEXTPLAIN; - res.writeHead(status, headers); - res.end(utils.httpStatus(status)); + var response; - if (!req.isStaticFile) - self.emit('request-end', req, res); + if (CONF.default_crypto) { - self.stats.response.error408++; - return self; -}; + key = (key || '') + CONF.secret; -/* - Response with 431 error - @req {ServerRequest} - @res {ServerResponse} - return {Framework} -*/ -Framework.prototype.response431 = function(req, res) { - var self = this; + if (key.length < 32) + key += ''.padLeft(32 - key.length, '0'); + + if (key.length > 32) + key = key.substring(0, 32); - if (res.success) - return self; + if (!F.temporary.keys[key]) + F.temporary.keys[key] = Buffer.from(key); - self._request_stats(false, req.isStaticFile); - req.clear(true); + 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; + } + } else + response = (value || '').decrypt(CONF.secret + '=' + key); - res.success = true; - var headers = {}; - var status = 431; - headers[RESPONSE_HEADER_CONTENTTYPE] = CONTENTTYPE_TEXTPLAIN; - res.writeHead(status, headers); - res.end(utils.httpStatus(status)); + return response ? (jsonConvert ? (response.isJSON() ? response.parseJSON(true) : null) : response) : null; +}; + +global.ENCRYPTREQ = function(req, val, key, strict) { - if (!req.isStaticFile) - self.emit('request-end', req, res); + if (req instanceof Controller) + req = req.req; - self.stats.response.error431++; - return self; + var obj = {}; + obj.ua = req.ua; + if (strict) + obj.ip = req.ip; + obj.data = val; + return F.encrypt(obj, key); }; -/* - Response with 500 error - @req {ServerRequest} - @res {ServerResponse} - @error {Error} - return {Framework} -*/ -Framework.prototype.response500 = function(req, res, error) { - var self = this; +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; +}; + +/** + * Create hash + * @param {String} type Type (md5, sha1, sha256, etc.) + * @param {String} value + * @param {String} salt Optional, default false. + * @return {String} + */ +F.hash = function(type, value, salt) { - if (res.success) - return self; + OBSOLETE('F.hash()', 'Use String.prototype.hash()'); - self._request_stats(false, req.isStaticFile); - req.clear(true); + var hash = Crypto.createHash(type); + var plus = ''; - if (error) - framework.error(error, null, req.uri); + if (typeof(salt) === 'string') + plus = salt; + else if (salt !== false) + plus = (CONF.secret || ''); - res.success = true; - var headers = {}; - var status = 500; - headers[RESPONSE_HEADER_CONTENTTYPE] = CONTENTTYPE_TEXTPLAIN; - res.writeHead(status, headers); - res.end(utils.httpStatus(status)); + hash.update(value.toString() + plus, ENCODING); + return hash.digest('hex'); +}; - if (!req.isStaticFile) - self.emit('request-end', req, res); +/** + * Resource reader + * @param {String} name Optional, resource file name. Default: "default". + * @param {String} key Resource key. + * @return {String} String + */ - self.stats.response.error500++; - return self; +const DEFNAME = 'default'; + +global.RESOURCE = F.resource = function(name, key) { + + if (!key) { + key = name; + name = null; + } + + if (!name) + name = DEFNAME; + + var res = F.resources[name]; + 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 = ''; + var filename; + if (routes) { + for (var i = 0, length = routes.length; i < length; i++) { + filename = routes[i]; + if (existsSync(filename)) + body += (body ? '\n' : '') + Fs.readFileSync(filename).toString(ENCODING); + } + } + + var filename = U.combine(CONF.directory_resources, name + '.resource'); + var empty = false; + if (existsSync(filename)) + body += (body ? '\n' : '') + Fs.readFileSync(filename).toString(ENCODING); + else + empty = true; + + var obj = body.parseConfig(); + F.resources[name] = obj; + obj.$empty = empty; + return obj[key] == null ? name == DEFNAME ? '' : obj[key] = F.resource(DEFNAME, key) : obj[key]; }; -/* - Response with 501 error - @req {ServerRequest} - @res {ServerResponse} - return {Framework} -*/ -Framework.prototype.response501 = function(req, res) { - var self = this; +/** + * Translates text + * @param {String} language A resource filename, optional. + * @param {String} text + * @return {String} + */ - if (res.success) - return self; +// var obsolete_translate = false; +global.TRANSLATE = F.translate = function(language, text) { - self._request_stats(false, req.isStaticFile); - req.clear(true); - res.success = true; + if (!text) { + text = language; + language = undefined; + } - var headers = {}; - var status = 501; - headers[RESPONSE_HEADER_CONTENTTYPE] = CONTENTTYPE_TEXTPLAIN; - res.writeHead(status, headers); - res.end(utils.httpStatus(status)); + if (text[0] === '#' && text[1] !== ' ') + return F.resource(language, text.substring(1)); - if (!req.isStaticFile) - self.emit('request-end', req, res); + /* + 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; +}; - self.stats.response.error501++; - return self; +/** + * The translator for the text from the View Engine @(TEXT TO TRANSLATE) + * @param {String} language A resource filename, optional. + * @param {String} text + * @return {String} + */ +global.TRANSLATOR = F.translator = function(language, text) { + return framework_internal.parseLocalization(text, language); }; -/* - Response content - @req {ServerRequest} - @res {ServerResponse} - @code {Number} - @contentBody {String} - @contentType {String} - @compress {Boolean} - @headers {Object} :: optional key/value - return {Framework} -*/ -Framework.prototype.responseContent = function(req, res, code, contentBody, contentType, compress, headers) { - var self = this; +F.$configure_sitemap = function(arr, clean) { - if (res.success) - return self; + if (!arr || typeof(arr) === 'string') { + var filename = prepare_filename(arr || 'sitemap'); + if (existsSync(filename, true)) + arr = Fs.readFileSync(filename).toString(ENCODING).split('\n'); + else + arr = null; + } - req.clear(true); - res.success = true; + if (!arr || !arr.length) + return F; - var accept = req.headers['accept-encoding'] || ''; - var returnHeaders = {}; + if (clean || !F.routes.sitemap) + F.routes.sitemap = {}; + + for (var i = 0, length = arr.length; i < length; i++) { + + var str = arr[i]; + if (!str || str[0] === '#' || str.substring(0, 3) === '// ') + continue; + + var index = str.indexOf(' :'); + if (index === -1) { + index = str.indexOf('\t:'); + if (index === -1) + continue; + } + + var key = str.substring(0, index).trim(); + var val = str.substring(index + 2).trim(); + var a = val.split('-->'); + var url = a[1].trim(); + var wildcard = false; - returnHeaders[RESPONSE_HEADER_CACHECONTROL] = 'private'; - returnHeaders['Vary'] = 'Accept-Encoding'; + if (url.endsWith('*')) { + wildcard = true; + url = url.substring(0, url.length - 1); + } else if (url.endsWith('*)')) { + // localization + wildcard = true; + url = url.substring(0, url.length - 2); + } + + var name = a[0].trim(); + var localizeName = name.startsWith('@('); + var localizeUrl = url.startsWith('@('); + + if (localizeName) + name = name.substring(2, name.length - 1).trim(); + + if (localizeUrl) + url = url.substring(2, url.length - 1).trim(); + + F.routes.sitemap[key] = { name: name, url: url, parent: a[2] ? a[2].trim() : null, wildcard: wildcard, formatName: name.indexOf('{') !== -1, formatUrl: url.indexOf('{') !== -1, localizeName: localizeName, localizeUrl: localizeUrl }; + } + + return F; +}; + +global.SITEMAP = F.sitemap = function(name, me, language) { + + if (!F.routes.sitemap) + return me ? null : EMPTYARRAY; + + if (typeof(me) === 'string') { + language = me; + me = false; + } + + var key = REPOSITORY_SITEMAP + name + '$' + (me ? '1' : '0') + '$' + (language || ''); + + if (F.temporary.other[key]) + return F.temporary.other[key]; + + var sitemap; + var id = name; + var url; + var title; + + if (me === true) { + sitemap = F.routes.sitemap[name]; + var item = { sitemap: id, id: '', name: '', url: '', last: true, selected: true, index: 0, wildcard: false, formatName: false, formatUrl: false }; + if (!sitemap) + return item; + + title = sitemap.name; + if (sitemap.localizeName) + title = F.translate(language, title); + + url = sitemap.url; + var wildcard = sitemap.wildcard; + + if (sitemap.localizeUrl) { + if (sitemap.wildcard) { + if (url[url.length - 1] !== '/') + url += '/'; + url += '*'; + } + + url = F.translate(language, url); + + if (url.endsWith('*')) { + url = url.substring(0, url.length - 1); + wildcard = true; + } else + wildcard = false; + } + + item.sitemap = id; + item.id = name; + item.formatName = sitemap.formatName; + item.formatUrl = sitemap.formatUrl; + item.localizeUrl = sitemap.localizeUrl; + item.localizeName = sitemap.localizeName; + item.name = title; + item.url = url; + item.wildcard = wildcard; + F.temporary.other[key] = item; + return item; + } + + var arr = []; + var index = 0; + + while (true) { + sitemap = F.routes.sitemap[name]; + if (!sitemap) + break; + + title = sitemap.name; + url = sitemap.url; + + var wildcard = sitemap.wildcard; + + if (sitemap.localizeName) + title = F.translate(language, sitemap.name); + + if (sitemap.localizeUrl) { + if (sitemap.wildcard) { + if (url[url.length - 1] !== '/') + url += '/'; + url += '*'; + } + url = F.translate(language, url); + + if (url.endsWith('*')) { + url = url.substring(0, url.length - 1); + wildcard = true; + } else + wildcard = false; + } + + arr.push({ sitemap: id, id: name, name: title, url: url, last: index === 0, first: sitemap.parent ? false : true, selected: index === 0, index: index, wildcard: wildcard, formatName: sitemap.formatName, formatUrl: sitemap.formatUrl, localizeName: sitemap.localizeName, localizeUrl: sitemap.localizeUrl }); + index++; + name = sitemap.parent; + if (!name) + break; + } + + arr.reverse(); + F.temporary.other[key] = arr; + return arr; +}; - // možnosť odoslať vlastné hlavičky - if (headers) - utils.extend(returnHeaders, headers, true); +/** + * Gets a list of all items in sitemap + * @param {String} parent + * @param {String} language Optional, language + * @return {Array} + */ +F.sitemap_navigation = function(parent, language) { - // Safari resolve - if (contentType === 'application/json') - returnHeaders[RESPONSE_HEADER_CACHECONTROL] = 'private, no-cache, no-store, must-revalidate'; + var key = REPOSITORY_SITEMAP + '_n_' + (parent || '') + '$' + (language || ''); + if (F.temporary.other[key]) + return F.temporary.other[key]; - // pridáme UTF-8 do hlavičky - if ((/text|application/).test(contentType)) - contentType += '; charset=utf-8'; + var keys = Object.keys(F.routes.sitemap); + var arr = []; + var index = 0; - returnHeaders[RESPONSE_HEADER_CONTENTTYPE] = contentType; + for (var i = 0, length = keys.length; i < length; i++) { + var item = F.routes.sitemap[keys[i]]; + if ((parent && item.parent !== parent) || (!parent && item.parent)) + continue; - if (compress && accept.lastIndexOf('gzip') !== -1) { - zlib.gzip(new Buffer(contentBody), function(err, data) { + var title = item.name; + var url = item.url; - if (err) { - res.writeHead(code, returnHeaders); - res.end(contentBody, ENCODING); - return; - } + if (item.localizeName) + title = F.translate(language, title); - returnHeaders['Content-Encoding'] = 'gzip'; + if (item.localizeUrl) + url = F.translate(language, url); - res.writeHead(code, returnHeaders); - res.end(data, ENCODING); - }); + arr.push({ id: parent || '', name: title, url: url, last: index === 0, first: item.parent ? false : true, selected: index === 0, index: index, wildcard: item.wildcard, formatName: item.formatName, formatUrl: item.formatUrl }); + index++; + } - self._request_stats(false, req.isStaticFile); + arr.quicksort('name'); + F.temporary.other[key] = arr; + return arr; +}; - if (!req.isStaticFile) - self.emit('request-end', req, res); - - return self; - } +/** + * Adds an item(s) to sitemap + * @param {String|Array} obj - 'ID : Title ---> URL --> [Parent]' parent is optional + * @return {framework} + */ +F.sitemap_add = function (obj) { + F.$configure_sitemap(obj instanceof Array ? obj : [obj]); + return F; +}; + +F.$configure_dependencies = function(arr, callback) { + + if (!arr || typeof(arr) === 'string') { + var filename = prepare_filename(arr || 'dependencies'); + if (existsSync(filename, true)) + arr = Fs.readFileSync(filename).toString(ENCODING).split('\n'); + else + arr = null; + } + + 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; + var dependencies = []; + + for (var i = 0, length = arr.length; i < length; i++) { + + var str = arr[i]; + + if (!str || str[0] === '#' || str.substring(0, 3) === '// ') + continue; + + var index = str.indexOf(' :'); + if (index === -1) { + index = str.indexOf('\t:'); + if (index === -1) + continue; + } + + var key = str.substring(0, index).trim(); + var url = str.substring(index + 2).trim(); + var priority = 0; + + options = undefined; + interval = undefined; + + index = key.indexOf('('); + if (index !== -1) { + interval = key.substring(index, key.indexOf(')', index)).replace(/\(|\)/g, '').trim(); + key = key.substring(0, index).trim(); + } + + index = url.indexOf('-->'); + if (index !== -1) { + var opt = url.substring(index + 3).trim(); + if (opt.isJSON()) + options = opt.parseJSON(true); + url = url.substring(0, index).trim(); + } + + switch (key) { + case 'package': + case 'packages': + case 'pkg': + type = 'package'; + priority = 9; + break; + case 'module': + case 'modules': + type = 'module'; + priority = 10; + break; + case 'model': + case 'models': + type = 'model'; + priority = 8; + break; + case 'source': + case 'sources': + type = 'source'; + priority = 3; + break; + case 'controller': + case 'controllers': + type = 'controller'; + priority = 4; + break; + case 'view': + case 'views': + priority = 3; + type = 'view'; + break; + case 'version': + case 'versions': + priority = 3; + type = 'version'; + break; + case 'config': + case 'configuration': + priority = 11; + type = 'config'; + break; + case 'isomorphic': + case 'isomorphics': + priority = 6; + type = 'isomorphic'; + break; + case 'definition': + case 'definitions': + priority = 5; + type = 'definition'; + break; + case 'middleware': + case 'middlewares': + type = 'middleware'; + priority = 4; + break; + case 'component': + case 'components': + priority = 7; + type = 'component'; + break; + } + + if (type) { + (function(type, url, options, interval) { + if (interval) + dependencies.push({ priority: priority, fn: next => F.uptodate(type, url, options, interval, next) }); + else + dependencies.push({ priority: priority, fn: next => F.install(type, url, options, undefined, undefined, undefined, undefined, undefined, undefined, next) }); + })(type, url, options, interval); + } + } + + dependencies.quicksort('priority', false); + dependencies.wait(function(item, next) { + item.fn(next); + }, callback); + return F; +}; + +F.$configure_workflows = function(arr, clean) { + + if (arr === undefined || typeof(arr) === 'string') { + var filename = prepare_filename(arr || 'workflows'); + if (existsSync(filename, true)) + arr = Fs.readFileSync(filename).toString(ENCODING).split('\n'); + else + arr = null; + } + + if (clean) + F.workflows = {}; + + 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('//')) + return; + var index = line.indexOf(':'); + if (index === -1) + return; + + var key = line.substring(0, index).trim(); + var response = -1; + var builder = []; + + // sub-type + var subindex = key.indexOf('('); + if (subindex !== -1) { + var type = key.substring(subindex + 1, key.indexOf(')', subindex + 1)).trim(); + key = key.substring(0, subindex).trim(); + type = type.replace(/^default\//gi, ''); + key = type + '#' + key; + } + + line.substring(index + 1).split('-->').forEach(function(operation, index) { + + var options = 'options||EMPTYOBJECT'; + operation = operation.trim().replace(/"/g, '\''); + + var oindex = operation.indexOf('{'); + if (oindex !== -1) { + options = operation.substring(oindex, operation.lastIndexOf('}') + 1); + operation = operation.replace(options, '').trim(); + options = 'options||' + options; + } + + if (operation.endsWith('(response)')) { + response = index; + operation = operation.replace('(response)', '').trim(); + } + + var what = operation.split(':'); + if (what.length === 2) + builder.push('$' + what[0].trim() + '(' + what[1].trim() + ', {0})'.format(options)); + else + builder.push('$' + what[0] + '({0})'.format(options)); + + }); + + F.workflows[key] = new Function('model', 'options', 'callback', 'return model.$async(callback' + (response === -1 ? '' : ', ' + response) + ').' + builder.join('.') + ';'); + }); + + return F; +}; + +F.$configure_versions = function(arr, clean) { + + if (arr === undefined || typeof(arr) === 'string') { + var filename = prepare_filename(arr || 'versions'); + if (existsSync(filename, true)) + arr = Fs.readFileSync(filename).toString(ENCODING).split('\n'); + else + arr = null; + } + + if (!arr) { + if (clean) + F.versions = null; + return F; + } + + if (!clean) + F.versions = {}; + + if (!F.versions) + F.versions = {}; + + for (var i = 0, length = arr.length; i < length; i++) { + + var str = arr[i]; + + if (!str || str[0] === '#' || str.substring(0, 3) === '// ') + continue; + + if (str[0] !== '/') + str = '/' + str; + + var index = str.indexOf(' :'); + var ismap = false; + + if (index === -1) { + index = str.indexOf('\t:'); + if (index === -1) { + index = str.indexOf('-->'); + if (index === -1) + continue; + ismap = true; + } + } + + var len = ismap ? 3 : 2; + var key = str.substring(0, index).trim(); + var filename = str.substring(index + len).trim(); + + 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('')); + }); +} - res.writeHead(code, returnHeaders); - res.end(contentBody, ENCODING); +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; +}; + +F.$configure_configs = function(arr, rewrite) { + + var type = typeof(arr); + if (type === 'string') { + var filename = prepare_filename(arr); + if (!existsSync(filename, true)) + return F; + arr = Fs.readFileSync(filename).toString(ENCODING).split('\n'); + } + + if (!arr) { + + var filenameA = U.combine('/', 'config'); + var filenameB = U.combine('/', 'config-' + (DEBUG ? 'debug' : 'release')); + arr = []; + + // read all files from "configs" directory + var configs = PATH.configs(); + if (existsSync(configs)) { + var tmp = Fs.readdirSync(configs); + for (var i = 0, length = tmp.length; i < length; i++) { + var skip = tmp[i].match(/-(debug|release|test)$/i); + if (skip) { + skip = skip[0].toString().toLowerCase(); + if (skip === '-debug' && !F.isDebug) + continue; + if (skip === '-release' && F.isDebug) + continue; + if (skip === '-test' && !F.isTest) + continue; + } + arr = arr.concat(Fs.readFileSync(configs + tmp[i]).toString(ENCODING).split('\n')); + } + } + + if (existsSync(filenameA) && Fs.lstatSync(filenameA).isFile()) + arr = arr.concat(Fs.readFileSync(filenameA).toString(ENCODING).split('\n')); + + if (existsSync(filenameB) && Fs.lstatSync(filenameB).isFile()) + arr = arr.concat(Fs.readFileSync(filenameB).toString(ENCODING).split('\n')); + } + + var done = function() { + 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) { + done(); + return F; + } + + if (rewrite === undefined) + rewrite = true; + + var obj = {}; + var accepts = null; + var length = arr.length; + var tmp; + var subtype; + var value; + var generated = []; + + for (var i = 0; i < length; i++) { + var str = arr[i]; + + if (!str || str[0] === '#' || (str[0] === '/' || str[1] === '/')) + continue; + + var index = str.indexOf(':'); + if (index === -1) + continue; + + var name = str.substring(0, index).trim(); + if (name === 'debug' || name === 'resources') + continue; + + 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(); + } else + subtype = ''; + + switch (name) { + 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': + OBSOLETE(name, 'You need to use "default_websocket_maxlength"'); + obj.default_websocket_maxlength = U.parseInt(value); + break; + case 'default-maximum-file-descriptors': + 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': // 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': // old + case 'static_accepts_custom': + accepts = value.replace(REG_ACCEPTCLEANER, '').split(','); + break; + + case 'default-root': // old + case 'default_root': + name = obsolete_config(name); + if (value) + obj[name] = U.path(value); + break; + + 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++) + obj[name][tmp[j]] = true; + break; + + case 'mail.smtp': + case 'mail.smtp.options': + case 'mail.address.from': + case 'mail.address.copy': + case 'mail.address.bcc': + case 'mail.address.reply': + + if (name === 'mail.address.bcc') + tmp = 'mail_address_copy'; + else + tmp = name.replace(/\./g, '-'); + + OBSOLETE(name, 'is renamed to "' + tmp + '"'); + 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; + break; + + 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 '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'; + break; + + case 'version': + obj[name] = value; + break; + + case 'security.txt': + obj[name] = value ? value.split(',').trim().join('\n') : ''; + break; + + 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)(); + } catch (e) { + F.error(e, 'F.configure(' + name + ')'); + } + } else if (subtype === 'json') + obj[name] = value.parseJSON(); + else if (subtype === 'date' || subtype === 'datetime' || subtype === 'time') + obj[name] = value.parseDate(); + else if (subtype === 'env' || subtype === 'environment') + obj[name] = process.env[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; + } + } + + // Cache for generated passwords + if (generated && generated.length) { + var filenameC = U.combine('/databases/', 'config{0}.json'.format(global.THREAD ? ('_' + global.THREAD) : '')); + var gdata; + + 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)(); + CONF.mail_smtp_options = tmp; + } + + process.env.NODE_TLS_REJECT_UNAUTHORIZED = CONF.allow_ssc_validation === false ? '0' : '1'; + + if (!CONF.directory_temp) + CONF.directory_temp = '~' + U.path(Path.join(Os.tmpdir(), 'totaljs' + F.directory.hash())); + + if (!CONF.etag_version) + CONF.etag_version = CONF.version.replace(/\.|\s/g, ''); + + if (CONF.default_timezone) + process.env.TZ = CONF.default_timezone; + + 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 (CONF.allow_performance) + http.globalAgent.maxSockets = 9999; + + 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=' + CONF.default_response_maxage); + if (subkey === 'X-Powered-By') { + if (xpowered) + HEADERS[key][subkey] = xpowered; + else + delete HEADERS[key][subkey]; + } + }); + }); + + done(); + 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; +} - self._request_stats(false, req.isStaticFile); +/** + * Create URL: JavaScript (according to config['static-url-script']) + * @param {String} name + * @return {String} + */ +F.routeScript = function(name, theme) { + OBSOLETE('F.routeScript()', 'Renamed to F.public_js'); + return F.$public(name, CONF.static_url_script, theme); +}; - if (!req.isStaticFile) - self.emit('request-end', req, res); +/** + * Create URL: CSS (according to config['static-url-style']) + * @param {String} name + * @return {String} + */ +F.routeStyle = function(name, theme) { + OBSOLETE('F.routeStyle()', 'Renamed to F.public_css'); + return F.$public(name, CONF.static_url_style, theme); +}; - return self; +F.routeImage = function(name, theme) { + OBSOLETE('F.routeImage()', 'Renamed to F.public_image'); + return F.$public(name, CONF.static_url_image, theme); }; -/* - Internal function - @req {ServerRequest} - @res {ServerResponse} - @url {String} - @permanent {Boolean} :: optional - return {Framework} -*/ -Framework.prototype.responseRedirect = function(req, res, url, permanent) { +F.routeVideo = function(name, theme) { + OBSOLETE('F.routeVideo()', 'Renamed to F.public_video'); + return F.$public(name, CONF.static_url_video, theme); +}; - var self = this; +F.routeFont = function(name, theme) { + OBSOLETE('F.routeFont()', 'Renamed to F.public_font'); + return F.$public(name, CONF.static_url_font, theme); +}; - if (res.success) - return self; +F.routeDownload = function(name, theme) { + OBSOLETE('F.routeDownload()', 'Renamed to F.public_download'); + return F.$public(name, CONF.static_url_download, theme); +}; - self._request_stats(false, req.isStaticFile); +F.routeStatic = function(name, theme) { + OBSOLETE('F.routeStatic()', 'Renamed to F.public'); + return F.$public(name, CONF.static_url, theme); +}; - req.clear(true); - res.success = true; +F.public_js = function(name, theme) { + return F.$public(name, CONF.static_url_script, theme); +}; - var headers = { - 'Location': url - }; - headers[RESPONSE_HEADER_CONTENTTYPE] = CONTENTTYPE_TEXTHTML + '; charset=utf-8'; +F.public_css = function(name, theme) { + return F.$public(name, CONF.static_url_style, theme); +}; - res.writeHead(permanent ? 301 : 302, headers); - res.end(); +F.public_image = function(name, theme) { + return F.$public(name, CONF.static_url_image, theme); +}; - if (!req.isStaticFile) - self.emit('request-end', req, res); +F.public_video = function(name, theme) { + return F.$public(name, CONF.static_url_video, theme); +}; - return self; +F.public_font = function(name, theme) { + return F.$public(name, CONF.static_url_font, theme); }; -/* - Initialization - @http {HTTP or HTTPS} - @config {Boolean or Object} - @port {Number} - @options {Object} - return {Framework} -*/ -Framework.prototype.init = function(http, config, port, ip, options) { - - var self = this; - self.isHTTPS = typeof(http.STATUS_CODES) === UNDEFINED; - - process.argv.forEach(function(name) { - if (name.toLowerCase().indexOf('coffee') !== -1) - self.isCoffee = true; - }); - - if (isNaN(port) && typeof(port) !== STRING) - port = null; - - if (port !== null && typeof(port) === OBJECT) { - var tmp = options; - options = port; - port = tmp; - } else if (ip !== null && typeof(ip) === OBJECT) { - var tmp = options; - options = ip; - ip = tmp; - } - - if (self.server !== null) - return; - - if (typeof(config) === BOOLEAN) - self.config.debug = config; - else if (typeof(config) === OBJECT) - utils.extend(self.config, config, true); - - self.isDebug = self.config.debug; - self.configure(); - self.configureMapping(); - self.clear(); - - self.cache.init(); - self.install(); - - var module = self.module('#'); - if (module !== null) { - Object.keys(module).forEach(function(o) { - if (o === 'onLoad' || o === 'usage') - return; - self[o] = module[o]; - }); - } - - process.on('uncaughtException', function(e) { - self.error(e, '', null); - - if (e.toString().indexOf('listen EADDRINUSE') !== -1) { - if (typeof(process.send) === FUNCTION) - process.send('stop'); - process.exit(0); - } - }); - - process.on('SIGTERM', function() { - self.stop(); - }); - - process.on('SIGINT', function() { - self.stop(); - }); - - process.on('exit', function() { - - if (self.onExit) - self.onExit(self); - - self.emit('exit'); - }); - - process.on('message', function(msg, h) { - - if (typeof(msg) !== STRING) { - self.emit('message', msg, h); - return; - } - - if (msg === 'debugging') { - framework.console(); - framework.console = utils.noop; - return; - } - - if (msg === 'reconnect') { - self.reconnect(); - return; - } - - if (msg === 'reconfigure') { - self.configure(); - self.configureMapping(); - self.emit(msg); - return; - } - - if (msg === 'reset') { - self.clear(); - self.cache.clear(); - return; - } - - if (msg === 'stop' || msg === 'exit') { - self.stop(); - return; - } - - self.emit('message', msg, h); - }); - - if (options) - self.server = http.createServer(options, self.handlers.onrequest); - else - self.server = http.createServer(self.handlers.onrequest); - - if (self.config['allow-websocket']) - self.server.on('upgrade', self.handlers.onupgrade); - - if (!port) { - if (self.config['default-port'] === 'auto') { - var envPort = process.env.PORT.toString(); - if (isNaN(envPort)) - port = envPort; - else - port = parseInt(envPort); - } else - port = self.config['default-port']; - } - - self.port = port || 8000; +F.public_download = function(name, theme) { + return F.$public(name, CONF.static_url_download, theme); +}; - if (ip !== null) { - self.ip = ip || self.config['default-ip'] || '127.0.0.1'; - if (self.ip === 'null' || self.ip === 'undefined' || self.ip === 'auto') - self.ip = undefined; - } else - self.ip = undefined; +F.public = function(name, theme) { + return F.$public(name, CONF.static_url, theme); +}; - self.server.listen(self.port, self.ip); +F.$public = function(name, directory, theme) { + var key = name + directory + '$' + theme; + var val = F.temporary.other[key]; + if (RELEASE && val) + return val; - if (typeof(self.ip) === UNDEFINED || self.ip === null) - self.ip = 'auto'; - if (module !== null) { - if (typeof(module.onLoad) !== UNDEFINED) { - try { - module.onLoad.call(self, self); - } catch (err) { - self.error(err, '#.onLoad()'); - } - } - } + if (name[0] === '~') { + name = name.substring(name[1] === '~' ? 2 : 1); + theme = ''; + } else if (name[0] === '=') { + // theme + var index = name.indexOf('/'); + if (index !== -1) { + theme = name.substring(1, index); + if (theme === '?') { + theme = CONF.default_theme; + name = name.substring(index); + } else + name = name.substring(index + 1); + } + } - self.isLoaded = true; + var filename; - try { - self.emit('load', self); - } catch (err) { - self.error(err, 'framework.on("load")'); - } + if (REG_ROUTESTATIC.test(name)) + filename = name; + else if (name[0] === '/') + filename = U.join(theme, F.$version(name, true)); + else { + filename = U.join(theme, directory, F.$version(name, true)); + if (REG_HTTPHTTPS.test(filename) && filename[0] === '/') + filename = filename.substring(1); + } - try { - self.emit('ready', self); - } catch (err) { - self.error(err, 'framework.on("ready")'); - } - - if (!process.connected) - self.console(); - - self.removeAllListeners('load'); - self.removeAllListeners('ready'); - - return self; -}; - -// Alias for framework.init -Framework.prototype.run = function(http, config, port, ip, options) { - return this.init(http, config, port, ip, options); -}; - -Framework.prototype.console = function() { - console.log('===================================================='); - console.log('PID : ' + process.pid); - console.log('node.js : ' + process.version); - console.log('total.js : v' + framework.version_header); - console.log('===================================================='); - console.log('Name : ' + framework.config.name); - console.log('Version : ' + framework.config.version); - console.log('Author : ' + framework.config.author); - console.log('Date : ' + new Date().format('yyyy-MM-dd HH:mm:ss')); - console.log('Mode : ' + (framework.config.debug ? 'debug' : 'release')); - console.log('====================================================\n'); - console.log('{2}://{0}:{1}/'.format(framework.ip, framework.port, framework.isHTTPS ? 'https' : 'http')); - console.log(''); + return F.temporary.other[key] = F.$version(framework_internal.preparePath(filename), true); }; -Framework.prototype.reconnect = function() { - var self = this; - - if (typeof(self.config['default-port']) !== UNDEFINED) - self.port = self.config['default-port']; - - if (typeof(self.config['default-ip']) !== UNDEFINED) - self.ip = self.config['default-ip']; +F.$version = function(name, def) { + var tmp; + + if (F.versions) + tmp = F.versions[name] || name; - self.server.close(function() { - self.server.listen(self.port, self.ip); - }); + if (F.onVersion) + tmp = F.onVersion(name) || name; - return self; + return tmp === 'auto' && def ? name : (tmp || name); }; -Framework.prototype._verify_directory = function(name) { +F.$versionprepare = function(html) { + var match = html.match(REG_VERSIONS); + if (!match) + return html; + + for (var i = 0, length = match.length; i < length; i++) { + + var src = match[i].toString(); + var end = 5; - var self = this; - var prop = '$directory-' + name; + // href + if (src[0] === 'h') + end = 6; - if (self.temporary.path[prop]) - return self; + var name = src.substring(end, src.length - 1); + html = html.replace(match[i], src.substring(0, end) + F.$version(name, true) + '"'); + } - var dir = utils.combine(self.config['directory-' + name]); + return html; +}; - if (!fs.existsSync(dir)) - fs.mkdirSync(dir); +/** + * Lookup for the route + * @param {HttpRequest} req + * @param {String} url URL address. + * @param {String Array} flags + * @param {Boolean} membertype Not defined = 0, Authorized = 1, Unauthorized = 2 + * @return {Object} + */ +F.lookup = function(req, url, flags, membertype) { + + var isSystem = url[0] === '#'; + var subdomain = F._length_subdomain_web && req.subdomain ? req.subdomain.join('.') : null; + + if (isSystem) + return F.routes.system[url]; + + if (isSystem) + req.path = [url]; + + var key; + + // helper for 401 http status + req.$isAuthorized = true; + + if (!isSystem) { + key = '1' + url + '$' + membertype + req.$flags + (subdomain ? '$' + subdomain : '') + (req.$roles ? 'R' : ''); + if (F.temporary.other[key]) + return F.temporary.other[key]; + } + + 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; + } else { + if (F._length_subdomain_web && !framework_internal.routeCompareSubdomain(subdomain, route.subdomain)) + continue; + 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) { + skip = true; + break; + } + + if (!route.regexp[route.regexpIndexer[j]].test(p)) { + skip = true; + break; + } + } + + if (skip) + continue; + } + + if (route.flags && route.flags.length) { + var result = framework_internal.routeCompareFlags2(req, route, membertype); + if (result === -1) + req.$isAuthorized = false; // request is not authorized + if (result < 1) + continue; + } + + if (key && route.isCACHE && (req.$isAuthorized || membertype === 1)) + F.temporary.other[key] = route; + + return route; + } + + 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; + var length = F.routes.websockets.length; + + req.$isAuthorized = true; + + for (var i = 0; i < length; i++) { + + var route = F.routes.websockets[i]; + + if (route.CUSTOM) { + if (!route.CUSTOM(url, req)) + continue; + } else { + + if (F._length_subdomain_websocket && !framework_internal.routeCompareSubdomain(subdomain, route.subdomain)) + continue; + if (route.isWILDCARD) { + if (!framework_internal.routeCompare(req.path, route.url, false, true)) + continue; + } else { + if (!framework_internal.routeCompare(req.path, route.url, false)) + 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) { + skip = true; + break; + } + + if (!route.regexp[route.regexpIndexer[j]].test(p)) { + skip = true; + break; + } + } + + if (skip) + continue; + } + + if (route.flags && route.flags.length) { + var result = framework_internal.routeCompareFlags2(req, route, membertype); + if (result === -1) + req.$isAuthorized = false; + if (result < 1) + continue; + } + + return route; + } + + return null; +}; - self.temporary.path[prop] = true; - return self; +/** + * Accept file type + * @param {String} extension + * @param {String} contentType Content-Type for file extension, optional. + * @return {Framework} + */ +F.accept = function(extension, contentType) { + if (extension[0] === '.') + extension = extension.substring(1); + CONF.static_accepts[extension] = true; + contentType && U.setContentType(extension, contentType); + return F; }; -Framework.prototype._upgrade = function(req, socket, head) { +// A temporary variable for generating Worker ID +// It's faster than Date.now() +var WORKERID = 0; + +/** + * Run worker + * @param {String} name + * @param {String} id Worker id, optional. + * @param {Number} timeout Timeout, optional. + * @param {Array} args Additional arguments, optional. + * @return {ChildProcess} + */ +global.WORKER = F.worker = function(name, id, timeout, args, special) { + + var fork = null; + var type = typeof(id); + + if (type === 'number' && timeout === undefined) { + timeout = id; + id = null; + type = 'undefined'; + } - if (req.headers.upgrade !== 'websocket') - return; + if (type === 'string') + fork = F.workers[id]; - var self = this; - var headers = req.headers; + if (id instanceof Array) { + args = id; + id = null; + timeout = undefined; + } - self.stats.request.websocket++; + if (timeout instanceof Array) { + args = timeout; + timeout = undefined; + } - if (self.restrictions.isRestrictions) { - if (self.restrictions.isAllowedIP) { - if (self.restrictions.allowedIP.indexOf(req.ip) === -1) { - self.stats.response.restriction++; - req.connection.destroy(); - return self; - } - } + if (fork) + return fork; - if (self.restrictions.isBlockedIP) { - if (self.restrictions.blockedIP.indexOf(req.ip) !== -1) { - self.stats.response.restriction++; - req.connection.destroy(); - return self; - } - } + var filename = name[0] === '@' ? F.path.package(name.substring(1)) : U.combine(CONF.directory_workers, name); - if (self.restrictions.isAllowedCustom) { - if (!self.restrictions._allowedCustom(headers)) { - self.stats.response.restriction++; - req.connection.destroy(); - return self; - } - } + if (!args) + args = EMPTYARRAY; - if (self.restrictions.isBlockedCustom) { - if (self.restrictions._blockedCustom(headers)) { - self.stats.response.restriction++; - req.connection.destroy(); - return self; - } - } - } + fork = Child.fork(filename[filename.length - 3] === '.' ? filename : filename + '.js', args, special ? HEADERS.workers2 : HEADERS.workers); - req.uri = parser.parse('ws://' + req.headers.host + req.url); - req.data = { - get: {} - }; + if (!id) + id = name + '_' + (WORKERID++); - if (req.uri.query && req.uri.query.length > 0) - req.data.get = qs.parse(req.uri.query); + fork.__id = id; + F.workers[id] = fork; - req.session = null; - req.user = null; - req.flags = [req.isSecure ? 'https' : 'http']; + fork.on('exit', function() { + var self = this; + self.__timeout && clearTimeout(self.__timeout); + delete F.workers[self.__id]; + if (fork) { + fork.removeAllListeners(); + fork = null; + } + }); - var path = utils.path(req.uri.pathname); - var websocket = new WebSocketClient(req, socket, head); + if (typeof(timeout) !== 'number') + return fork; - req.path = internal.routeSplit(req.uri.pathname); + fork.__timeout = setTimeout(function() { + fork && fork.kill('SIGKILL'); + }, timeout); - if (self.onAuthorization === null) { - var route = self.lookup_websocket(req, websocket.uri.pathname, true); + return fork; +}; - if (route === null) { - websocket.close(); - req.connection.destroy(); - return; - } +global.WORKER2 = F.worker2 = function(name, args, callback, timeout) { - self._upgrade_continue(route, req, websocket, path); - return; - } + if (typeof(args) === 'function') { + timeout = callback; + callback = args; + args = undefined; + } else if (typeof(callback) === 'number') { + var tmp = timeout; + timeout = callback; + callback = tmp; + } - self.onAuthorization.call(self, req, websocket, req.flags, function(isLogged, user) { + if (args && !(args instanceof Array)) + args = [args]; - if (user) - req.user = user; + var fork = F.worker(name, null, timeout, args, true); + if (fork.__worker2) + return fork; - req.flags.push(isLogged ? 'authorize' : 'unauthorize'); + var output = Buffer.alloc(0); - var route = self.lookup_websocket(req, websocket.uri.pathname, false); + fork.__worker2 = true; + fork.on('error', function(e) { + callback && callback(e, output); + callback = null; + }); - if (route === null) { - websocket.close(); - req.connection.destroy(); - return; - } + fork.stdout.on('data', function(data) { + CONCAT[0] = output; + CONCAT[1] = data; + output = Buffer.concat(CONCAT); + }); - self._upgrade_continue(route, req, websocket, path); - }); + fork.on('exit', function() { + callback && callback(null, output); + callback = null; + }); + return fork; }; -Framework.prototype._upgrade_continue = function(route, req, socket, path) { +/** + * This method suspends + * @param {String} name Operation name. + * @param {Boolean} enable Enable waiting (optional, default: by the current state). + * @return {Boolean} + */ +global.PAUSESERVER = F.wait = function(name, enable) { + + if (!F.waits) + F.waits = {}; + + if (enable !== undefined) { + if (enable) + F.waits[name] = true; + else + delete F.waits[name]; + F._length_wait = Object.keys(F.waits).length; + return enable; + } + + if (F.waits[name]) + delete F.waits[name]; + else { + F.waits[name] = true; + enable = true; + } + + F._length_wait = Object.keys(F.waits).length; + 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); + }); +}; - var self = this; +// ================================================================================= +// Framework route +// ================================================================================= - if (!socket.prepare(route.flags, route.protocols, route.allow, route.length, self.version_header)) { - socket.close(); - req.connection.destroy(); - return; - } - - var id = path + (route.flags.length > 0 ? '#' + route.flags.join('-') : ''); - - if (route.isBINARY) - socket.type = 1; - else if (route.isJSON) - socket.type = 3; - - if (typeof(self.connections[id]) === UNDEFINED) { - var connection = new WebSocket(self, path, route.name, id); - self.connections[id] = connection; - route.onInitialize.apply(connection, internal.routeParam(route.param.length > 0 ? internal.routeSplit(req.uri.pathname, true) : req.path, route)); - } - - socket.upgrade(self.connections[id]); -}; - -Framework.prototype._service = function(count) { - var self = this; - - if (self.config.debug) - self.resources = {}; +function FrameworkRoute() { + this.route = {}; +} - // every 20 minute service clears resources - if (count % 20 === 0) { - self.emit('clear', 'resources'); - self.resources = {}; +FrameworkRoute.prototype = { + get id() { + return this.route.id; + }, + set id(value) { + this.route.id = value; + }, + get description() { + return this.route.description; + }, + set description(value) { + this.route.description = value; + }, + get maxlength() { + return this.route.length; + }, + set maxlength(value) { + this.route.length = value; + }, + get options() { + return this.route.options; + }, + set options(value) { + this.route.options = value; + }, + get url() { + return this.route.urlraw; + }, + get flags() { + return this.route.flags || EMPTYARRAY; + }, + set groups(value) { + this.route.groups = value; + }, + get groups() { + return this.route.groups; + } +}; + +const FrameworkRouteProto = FrameworkRoute.prototype; + +FrameworkRouteProto.make = function(fn) { + fn && fn.call(this, this); + return this; +}; + +FrameworkRouteProto.setId = function(value) { + this.route.id = value; + return this; +}; + +FrameworkRouteProto.setDecription = function(value) { + this.route.description = value; + return this; +}; + +FrameworkRouteProto.setTimeout = function(value) { + this.route.timeout = value; + return this; +}; + +FrameworkRouteProto.setMaxLength = function(value) { + this.route.length = value; + return this; +}; + +FrameworkRouteProto.setOptions = function(value) { + this.route.options = value; + return this; +}; - if (typeof(gc) !== UNDEFINED) - gc(); - } +// ================================================================================= +// Framework path +// ================================================================================= - // every 3 minute service clears static cache - if (count % 3 === 0) { - self.emit('clear', 'temporary', self.temporary); - self.temporary.path = {}; - self.temporary.range = {}; - self.temporary.views = {}; - } +function FrameworkPath() {} +const FrameworkPathProto = FrameworkPath.prototype; - self.emit('service', count); +FrameworkPathProto.verify = function(name) { + var prop = '$directory-' + name; + if (F.temporary.path[prop]) + return F; + var directory = CONF['directory_' + name] || name; + var dir = U.combine(directory); + try { + !existsSync(dir) && Fs.mkdirSync(dir); + } catch (e) {} + F.temporary.path[prop] = true; + return F; }; -Framework.prototype._request = function(req, res) { +FrameworkPathProto.mkdir = function(p, cache) { - var self = this; + var key = '$directory-' + p; - if (self.config['allow-performance']) { - req.connection.setNoDelay(true); - req.connection.setTimeout(0); - } + if (cache && F.temporary.path[key]) + return F; - if (self.onRequest !== null && self.onRequest(req, res)) - return; + F.temporary.path[key] = true; - res.setHeader('X-Powered-By', 'total.js v' + self.version_header); + var is = F.isWindows; + var s = ''; - var headers = req.headers; - var protocol = req.connection.encrypted ? 'https' : 'http'; + if (p[0] === '/') { + s = is ? '\\' : '/'; + p = p.substring(1); + } - if (self._request_check_redirect) { - var redirect = self.routes.redirects[protocol + '://' + req.host]; - if (redirect) { - self.stats.response.forwarding++; - self.responseRedirect(req, res, redirect.url + (redirect.path ? req.url : ''), redirect.permanent); - return self; - } - } + var l = p.length - 1; + var beg = 0; - if (self.restrictions.isRestrictions) { - if (self.restrictions.isAllowedIP) { - if (self.restrictions.allowedIP.indexOf(req.ip) === -1) { - self.stats.response.restriction++; - req.connection.destroy(); - return self; - } - } - - if (self.restrictions.isBlockedIP) { - if (self.restrictions.blockedIP.indexOf(req.ip) !== -1) { - self.stats.response.restriction++; - req.connection.destroy(); - return self; - } - } + if (is) { + if (p[l] === '\\') + p = p.substring(0, l); - if (self.restrictions.isAllowedCustom) { - if (!self.restrictions._allowedCustom(headers)) { - self.stats.response.restriction++; - req.connection.destroy(); - return self; - } - } - - if (self.restrictions.isBlockedCustom) { - if (self.restrictions._blockedCustom(headers)) { - self.stats.response.restriction++; - req.connection.destroy(); - return self; - } - } - } - - if (self.config.debug) - res.setHeader('Mode', 'debug'); - - res.success = false; - req.uri = parser.parse(protocol + '://' + req.host + req.url); - req.path = internal.routeSplit(req.uri.pathname); - req.processing = 0; - - // if is static file, return file - if (utils.isStaticFile(req.uri.pathname)) { - - req.isStaticFile = true; - self.stats.request.file++; - self._request_stats(true, true); - - if (self._length_files === 0) { - self.responseStatic(req, res); - return; - } - - new Subscribe(self, req, res, 3).file(); - return; - } - - req.xhr = headers['x-requested-with'] === 'XMLHttpRequest'; - req.isProxy = headers['x-proxy'] === 'total.js'; - - req.data = { - get: {}, - post: {}, - files: [] - }; - req.flags = null; - - req.buffer_exceeded = false; - req.buffer_data = ''; - req.buffer_has = false; - - req.session = null; - req.user = null; - req.prefix = ''; - req.isAuthorized = true; - - var isXSS = false; - var accept = headers.accept; - - self._request_stats(true, false); - self.stats.request.web++; - - if (req.uri.query && req.uri.query.length > 0) { - if (self.onXSS !== null) - isXSS = self.onXSS(req.uri.query); - req.data.get = qs.parse(req.uri.query); - } - - if (self.onRoute !== null) { - try { - if (!self.onRoute(req, res)) { - - if (!res.success) { - self._request_stats(false, false); - self.stats.request.blocked++; - req.connection.destroy(); - } - - return; - } - } catch (err) { - self.response500(req, res, err); - return; - } - } - - var flags = [req.method.toLowerCase()]; - var multipart = req.headers['content-type'] || ''; - - flags.push(protocol); + if (p[1] === ':') + beg = 1; - if (multipart.indexOf('multipart/form-data') === -1) { + } else { + if (p[l] === '/') + p = p.substring(0, l); + } - if (multipart.indexOf('application/json') !== -1) - flags.push('json'); + if (existsSync(p)) + return F; - if (multipart.indexOf('mixed') === -1) - multipart = ''; - else - flags.push('mmr'); - } + var arr = is ? p.replace(/\//g, '\\').split('\\') : p.split('/'); + var directory = s; - if (multipart.length > 0) - flags.push('upload'); + for (var i = 0, length = arr.length; i < length; i++) { + var name = arr[i]; + if (is) + directory += (i && directory ? '\\' : '') + name; + else + directory += (i && directory ? '/' : '') + name; - if (req.isProxy) - flags.push('proxy'); + if (i >= beg && !existsSync(directory)) + Fs.mkdirSync(directory); + } - if (accept === 'text/event-stream') - flags.push('sse'); + return F; +}; + +FrameworkPathProto.exists = function(path, callback) { + Fs.lstat(path, (err, stats) => callback(err ? false : true, stats ? stats.size : 0, stats ? stats.isFile() : false)); + return F; +}; - if (self.config.debug) - flags.push('debug'); +FrameworkPathProto.public = function(filename) { + return U.combine(CONF.directory_public, filename); +}; - req.prefix = self.onPrefix === null ? '' : self.onPrefix(req) || ''; +FrameworkPathProto.public_cache = function(filename) { + var key = 'public_' + filename; + var item = F.temporary.other[key]; + return item ? item : F.temporary.other[key] = U.combine(CONF.directory_public, filename); +}; - if (req.prefix.length > 0) - flags.push('#' + req.prefix); +FrameworkPathProto.private = function(filename) { + return U.combine(CONF.directory_private, filename); +}; - flags.push('+xhr'); +FrameworkPathProto.isomorphic = function(filename) { + return U.combine(CONF.directory_isomorphic, filename); +}; - if (req.xhr) { - self.stats.request.xhr++; - flags.push('xhr'); - } +FrameworkPathProto.configs = function(filename) { + return U.combine(CONF.directory_configs, filename); +}; - if (isXSS) { - flags.push('xss'); - self.stats.request.xss++; - } +FrameworkPathProto.virtual = function(filename) { + return U.combine(CONF.directory_public_virtual, filename); +}; - if (self._request_check_referer) { - var referer = headers['referer'] || ''; - if (referer !== '' && referer.indexOf(headers['host']) !== -1) - flags.push('referer'); - } +FrameworkPathProto.logs = function(filename) { + this.verify('logs'); + return U.combine(CONF.directory_logs, filename); +}; - req.flags = flags; +FrameworkPathProto.models = function(filename) { + return U.combine(CONF.directory_models, filename); +}; - // call event request - self.emit('request-begin', req, res); +FrameworkPathProto.temp = function(filename) { + this.verify('temp'); + return U.combine(CONF.directory_temp, filename); +}; - if (req.method === 'GET' || req.method === 'DELETE' || req.method === 'OPTIONS') { - if (req.method === 'DELETE') - self.stats.request['delete']++; - else - self.stats.request.get++; +FrameworkPathProto.temporary = function(filename) { + return this.temp(filename); +}; - new Subscribe(self, req, res, 0).end(); - return; - } +FrameworkPathProto.views = function(filename) { + return U.combine(CONF.directory_views, filename); +}; - if (self._request_check_POST && (req.method === 'POST' || req.method === 'PUT')) { - if (multipart.length > 0) { - self.stats.request.upload++; - new Subscribe(self, req, res, 2).multipart(multipart); - } else { +FrameworkPathProto.updates = function(filename) { + return U.combine(CONF.directory_updates, filename); +}; - if (req.method === 'PUT') - self.stats.request.put++; - else - self.stats.request.post++; +FrameworkPathProto.workers = function(filename) { + return U.combine(CONF.directory_workers, filename); +}; - new Subscribe(self, req, res, 1).urlencoded(); - } - return; - } +FrameworkPathProto.databases = function(filename) { + this.verify('databases'); + return U.combine(CONF.directory_databases, filename); +}; - self.emit('request-end', req, res); - self._request_stats(false, false); - self.stats.request.blocked++; - req.connection.destroy(); +FrameworkPathProto.modules = function(filename) { + return U.combine(CONF.directory_modules, filename); }; -Framework.prototype._request_stats = function(beg, isStaticFile) { +FrameworkPathProto.schemas = function(filename) { + return U.combine(CONF.directory_schemas, filename); +}; - var self = this; +FrameworkPathProto.operations = function(filename) { + return U.combine(CONF.directory_operations, filename); +}; - if (beg) - self.stats.request.pending++; - else - self.stats.request.pending--; +FrameworkPathProto.tasks = function(filename) { + return U.combine(CONF.directory_tasks, filename); +}; - if (self.stats.request.pending < 0) - self.stats.request.pending = 0; +FrameworkPathProto.controllers = function(filename) { + return U.combine(CONF.directory_controllers, filename); +}; - return self; +FrameworkPathProto.definitions = function(filename) { + return U.combine(CONF.directory_definitions, filename); }; -/* - Get a model - @name {String} - return {Object} -*/ -Framework.prototype.model = function(name) { - var self = this; - var model = self.models[name]; +FrameworkPathProto.tests = function(filename) { + return U.combine(CONF.directory_tests, filename); +}; - if (model) - return model; +FrameworkPathProto.resources = function(filename) { + return U.combine(CONF.directory_resources, filename); +}; - var filename = path.join(directory, self.config['directory-models'], name); +FrameworkPathProto.services = function(filename) { + return U.combine(CONF.directory_services, filename); +}; - if (self.isCoffee) { - if (fs.existsSync(filename + EXTENSION_COFFEE)) - filename += EXTENSION_COFFEE; - else - filename += EXTENSION_JS; - } else - filename += EXTENSION_JS; +FrameworkPathProto.packages = function(filename) { + return U.combine(CONF.directory_packages, filename); +}; - model = require(filename); - self.models[name] = model; - return model; +FrameworkPathProto.themes = function(filename) { + return U.combine(CONF.directory_themes, filename); }; -/* - Get a source - @name {String} - return {Object} -*/ -Framework.prototype.source = function(name) { - var self = this; - var source = self.sources[name]; +FrameworkPathProto.components = function(filename) { + return U.combine(CONF.directory_components, filename); +}; - if (source) - return source; +FrameworkPathProto.root = function(filename) { + var p = Path.join(directory, filename || ''); + return F.isWindows ? p.replace(/\\/g, '/') : p; +}; - var filename = path.join(directory, self.config['directory-source'], name); +FrameworkPathProto.package = function(name, filename) { - if (self.isCoffee) { - if (fs.existsSync(filename + EXTENSION_COFFEE)) - filename += EXTENSION_COFFEE; - else - filename += EXTENSION_JS; - } else - filename += EXTENSION_JS; + if (filename === undefined) { + var index = name.indexOf('/'); + if (index !== -1) { + filename = name.substring(index + 1); + name = name.substring(0, index); + } + } - source = require(filename); - self.sources[name] = source; - return source; + 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; }; -/** - * Add a test function or test request - * @param {String} name Test name. - * @param {Url or Function} url Url or Callback function(next, name) {}. - * @param {Array} flags Routed flags (GET, POST, PUT, XHR, JSON ...). - * @param {Function} callback Callback. - * @param {Object or String} data Request data. - * @param {Object} cookies Request cookies. - * @param {Object} headers Additional headers. - * @return {Framework} - */ -Framework.prototype.assert = function(name, url, flags, callback, data, cookies, headers) { +// ================================================================================= +// Cache declaration +// ================================================================================= + +function FrameworkCache() { + this.items = {}; + this.count = 1; + this.interval; + this.$sync = true; +} + +const FrameworkCacheProto = FrameworkCache.prototype; - var self = this; +FrameworkCacheProto.init = function(notimer) { + var self = this; - if (typeof(url) === FUNCTION) { - self.tests[_test + ': ' + name] = { - run: url - }; - return self; - } + if (!notimer) + self.init_timer(); + + if (CONF.allow_cache_snapshot) + self.load(() => self.loadpersistent()); + else + 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; +}; + +FrameworkCacheProto.save = function() { + Fs.writeFile(F.path.temp((F.id ? 'i-' + F.id + '_' : '') + 'framework_cachesnapshot.jsoncache'), JSON.stringify(this.items), NOOP); + return this; +}; + +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) { + try { + data = JSON.parse(data.toString('utf8'), (key, value) => typeof(value) === 'string' && value.isJSONDate() ? new Date(value) : value); + self.items = data; + } catch (e) {} + } + callback && callback(); + }); + return self; +}; + +FrameworkCacheProto.savepersistent = function() { + setTimeout2('framework_cachepersist', function(self) { + var keys = Object.keys(self.items); + var obj = {}; + + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i]; + var item = self.items[key]; + if (item && item.persist) + obj[key] = item; + } + + Fs.writeFile(F.path.temp((F.id ? 'i-' + F.id + '_' : '') + 'framework_cachepersist.jsoncache'), JSON.stringify(obj), NOOP); + }, 1000, 50, this); + return this; +}; + +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) { + try { + data = JSON.parse(data.toString('utf8'), (key, value) => typeof(value) === 'string' && value.isJSONDate() ? new Date(value) : value); + var keys = Object.keys(data); + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i]; + var item = data[key]; + if (item.expire >= NOW) + self.items[key] = item; + } + } catch (e) {} + } + callback && callback(); + }); + return self; +}; - var method = 'GET'; - var length = 0; - var isJSON = false; +FrameworkCacheProto.stop = function() { + clearInterval(this.interval); + return this; +}; - headers = utils.extend({}, headers || {}); +FrameworkCacheProto.clear = function() { + this.items = {}; + F.isCluster && CONF.allow_cache_cluster && process.send(CLUSTER_CACHE_CLEAR); + this.savepersistent(); + return this; +}; - if (flags instanceof Array) { - length = flags.length; - for (var i = 0; i < length; i++) { +FrameworkCacheProto.recycle = function() { - switch (flags[i].toLowerCase()) { + var items = this.items; + var persistent = false; - case 'xhr': - headers['X-Requested-With'] = 'XMLHttpRequest'; - break; + NOW = new Date(); + this.count++; - case 'json': - headers['Content-Type'] = 'application/json'; - isJSON = true; - break; + for (var o in items) { + var value = items[o]; + if (!value) + delete items[o]; + else if (value.expire < NOW) { + if (value.persist) + 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]; + } + } - case 'get': - case 'delete': - case 'options': - method = flags[i].toUpperCase(); - break; + 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; +}; - case 'upload': - headers['Content-Type'] = 'multipart/form-data'; - break; +FrameworkCacheProto.set2 = function(name, value, expire) { + return this.set(name, value, expire, true); +}; + +FrameworkCacheProto.set = FrameworkCacheProto.add = function(name, value, expire, persist) { - case 'post': - case 'put': + 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 (typeof(expire)) { + case 'string': + expire = expire.parseDateExpiration(); + break; + case 'undefined': + expire = NOW.add('m', 5); + break; + } - method = flags[i].toUpperCase(); + var obj = { value: value, expire: expire }; - if (!headers['Content-Type']) - headers['Content-Type'] = 'application/x-www-form-urlencoded'; + if (persist) { + obj.persist = true; + this.savepersistent(); + } - break; - } - } - } + this.items[name] = obj; + 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; +}; - headers['X-Assertion-Testing'] = '1'; - headers['X-Powered-By'] = 'total.js v' + self.version_header; +FrameworkCacheProto.read = FrameworkCacheProto.get = function(key, def) { - if (cookies) { - var builder = []; - var keys = Object.keys(cookies); + var value = this.items[key]; + if (!value) + return def; - length = keys.length; + NOW = new Date(); - for (var i = 0; i < length; i++) - builder.push(keys[i] + '=' + encodeURIComponent(cookies[keys[i]])); + if (value.expire < NOW) { + this.items[key] = undefined; + F.$events['cache-expire'] && EMIT('cache-expire', key, value.value); + F.$events.cache_expired && EMIT('cache_expired', key, value.value); + return def; + } - if (builder.length > 0) - headers['Cookie'] = builder.join('; '); - } + return value.value; +}; - if (typeof(data) !== STRING) - data = isJSON ? JSON.stringify(data) : qs.stringify(data); +FrameworkCacheProto.read2 = FrameworkCacheProto.get2 = function(key, def) { + var value = this.items[key]; - if (data && data.length > 0) - headers['Content-Length'] = data.length; + if (!value) + return def; - var obj = { - url: url, - callback: callback, - method: method, - data: data || '', - headers: headers - }; + if (value.expire < NOW) { + this.items[key] = undefined; + F.$events['cache-expire'] && EMIT('cache-expire', key, value.value); + F.$events.cache_expired && EMIT('cache_expired', key, value.value); + return def; + } - self.tests[_test + ': ' + name] = obj; - return self; + return value.value; }; -/** - * Test in progress - * @private - * @param {Boolean} stop Stop application. - * @param {Function} callback Callback. - * @return {Framework} - */ -Framework.prototype.testing = function(stop, callback) { +FrameworkCacheProto.setExpire = function(name, expire) { + var obj = this.items[name]; + if (obj) + obj.expire = typeof(expire) === 'string' ? expire.parseDateExpiration() : expire; + return this; +}; - if (typeof(stop) === UNDEFINED) - stop = true; +FrameworkCacheProto.remove = function(name) { + var value = this.items[name]; - var self = this; - var keys = Object.keys(self.tests); + if (value) { + this.items[name].persist && this.savepersistent(); + this.items[name] = undefined; + } - if (keys.length === 0) { + if (F.isCluster && CONF.allow_cache_cluster && this.$sync) { + CLUSTER_CACHE_REMOVE.name = name; + process.send(CLUSTER_CACHE_REMOVE); + } - if (callback) - callback(); + return value; +}; - if (stop) - self.stop(); +FrameworkCacheProto.removeAll = function(search) { + var count = 0; + var isReg = typeof(search) === 'object'; - return self; - } + for (var key in this.items) { - var key = keys[0]; - var test = self.tests[key]; - var caption = 'Success .............. ' + key; + if (isReg) { + if (!search.test(key)) + continue; + } else { + if (key.indexOf(search) === -1) + continue; + } - delete self.tests[key]; + this.remove(key); + count++; + } - if (test.run) { + if (F.isCluster && CONF.allow_cache_cluster && this.$sync) { + CLUSTER_CACHE_REMOVEALL.search = search; + process.send(CLUSTER_CACHE_REMOVEALL); + } - try { - test.run.call(self, function() { - console.log(caption); - self.testing(stop, callback); - }, key); - } catch (e) { - setTimeout(function() { - self.stop(); - }, 500); - throw e; - } - return self; - } + return count; +}; - var response = function(res) { +FrameworkCacheProto.fn = function(name, fnCache, fnCallback, options) { - res._buffer = ''; + var self = this; + var value = self.read2(name); - res.on('data', function(chunk) { - this._buffer += chunk.toString(ENCODING); - }); + if (value) { + fnCallback && fnCallback(value, true, options); + return self; + } - res.on('end', function() { + fnCache(function(value, expire) { + self.add(name, value, expire); + fnCallback && fnCallback(value, false, options); + }, options); - var cookie = res.headers['cookie'] || ''; - var cookies = {}; + return self; +}; - if (cookie.length !== 0) { +function subscribe_timeout(req) { + req.controller && req.controller.precache && req.controller.precache(null, null, null); + req.$total_cancel(); +} - var arr = cookie.split(';'); - var length = arr.length; +function subscribe_timeout_middleware(req) { + if (req.$total_middleware) + req.$total_middleware = null; + req.$total_execute2(); +} - for (var i = 0; i < length; i++) { - var c = arr[i].trim().split('='); - cookies[c.shift()] = unescape(c.join('=')); - } - } +function subscribe_validate_callback(req, code) { + req.$total_execute(code); +} - try { - test.callback(null, res._buffer, res.statusCode, res.headers, cookies, key); - console.log(caption); - self.testing(stop, callback); - } catch (e) { - setTimeout(function() { - self.stop(); - }, 500); - throw e; - } - }); +/** + * FrameworkController + * @class + * @param {String} name Controller name. + * @param {Request} req + * @param {Response} res + * @param {FrameworkSubscribe} subscribe + */ +function Controller(name, req, res, currentView) { + + this.name = name; + // this.exception; + + // Sets the default language + if (req) { + this.language = req.$language; + this.req = req; + this.route = req.$total_route; + } else + this.req = EMPTYREQUEST; + + // controller.type === 0 - classic + // controller.type === 1 - server sent events + // this.type = 0; + + // this.layoutName =CONF.default_layout; + // this.themeName =CONF.default_theme; + // this.status = 200; + + // this.isLayout = false; + // this.isCanceled = false; + // this.isTimeout = false; + // this.isTransfer = false; + + this.isConnected = true; + this.isController = true; + + // render output + // this.output = null; + // this.outputPartial = null; + // this.$model = null; + + this._currentView = currentView; + + if (res) { + this.res = res; + this.req.controller = this.res.controller = this; + } else + this.res = EMPTYOBJECT; +} - res.resume(); - }; +Controller.prototype = { - var options = parser.parse((test.url.indexOf('http://') > 0 || test.url.indexOf('https://') > 0 ? '' : 'http://' + self.ip + ':' + self.port) + test.url); - var con = options.protocol === 'https:' ? https : http; - var req = test.method === 'POST' || test.method === 'PUT' ? con.request(options, response) : con.get(options, response); + get breadcrumb() { + return this.repository[REPOSITORY_SITEMAP]; + }, + + get repository() { + if (this.$repository) + return this.$repository; + else + return this.$repository ? this.$repository : (this.$repository = {}); + }, + + set repository(val) { + this.$repository = val; + }, + + get schema() { + return this.route.schema ? this.route.schema[0] === 'default' ? this.route.schema[1] : this.route.schema.join('/') : ''; + }, + + get workflow() { + return this.route.schema_workflow; + }, + + get sseID() { + return this.req.headers['last-event-id'] || null; + }, + + get options() { + return this.route.options; + }, + + get split() { + return this.req.split; + }, + + get flags() { + return this.route.flags; + }, + + get path() { + OBSOLETE('controller.path', 'Use: PATH'); + return F.path; + }, + + get query() { + 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; + }, + + get subdomain() { + return this.req.subdomain; + }, + + get ip() { + return this.req.ip; + }, + + get xhr() { + return this.req.xhr; + }, + + set xhr(val) { + this.req.xhr = val; + }, + + get url() { + return U.path(this.req.uri.pathname); + }, + + get uri() { + 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() { + OBSOLETE('controller.config', 'Use: CONF'); + return CONF; + }, + + get controllers() { + OBSOLETE('controller.controllers', 'This property will be removed in v4.'); + return F.controllers; + }, + + 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; + }, + + get secured() { + return this.req.secured; + }, + + get session() { + return this.req.session; + }, + + set session(value) { + this.req.session = value; + }, + + get user() { + return this.req.user; + }, + + get referrer() { + return this.req.headers['referer'] || ''; + }, + + set user(value) { + this.req.user = value; + }, + + get mobile() { + 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; + }, + + get sitemapid() { + return this.$sitemapid || this.route.sitemap; + }, + + get params() { + if (this.$params) + return this.$params; + var route = this.route; + var names = route.paramnames; + if (names) { + var obj = {}; + for (var i = 0; i < names.length; i++) + obj[names[i]] = this.req.split[route.param[i]]; + this.$params = obj; + return obj; + } else { + // Because in some cases are overwritten + return this.$params = {}; + } + }, + + set params(val) { + this.$params = val; + }, + + get ua() { + return this.req ? this.req.ua : null; + } +}; - req.on('error', function(error) { +// ====================================================== +// PROTOTYPES +// ====================================================== - setTimeout(function() { - self.stop(); - }, 500); +const ControllerProto = Controller.prototype; - throw error; - }); +ControllerProto.$get = ControllerProto.$read = function(helper, callback) { - if (test.data.length > 0) - req.end(test.data, ENCODING); - else - req.end(); + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } - return self; + this.getSchema().get(helper, callback, this); + return this; }; -/* - Make a tests - @stop {Boolean} :: stop framework (default true) - @names {String array} :: only tests in names (optional) - @callback {Functions} :: on complete test handler (optional) - return {Framework} -*/ -Framework.prototype.test = function(stop, names, cb) { +ControllerProto.$query = function(helper, callback) { - var self = this; + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } + + this.getSchema().query(helper, callback, this); + return this; +}; - if (typeof(names) === FUNCTION) { - cb = names; - names = []; - } else - names = names || []; +ControllerProto.$save = function(helper, callback) { - var counter = 0; - self.isTest = true; + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } - var dir = self.config['directory-tests']; + var self = this; + if (self.body && self.body.$$schema) { + self.body.$$controller = self; + self.body.$save(helper, callback); + } else { + var model = self.getSchema().default(); + model.$$controller = self; + model.$save(helper, callback); + } + return self; +}; - if (!fs.existsSync(utils.combine(dir))) { - if (cb) cb(); - if (stop) setTimeout(function() { - framework.stop(); - }, 500); - return self; - } +ControllerProto.$insert = function(helper, callback) { - fs.readdirSync(utils.combine(dir)).forEach(function(name) { + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } - var filename = path.join(directory, dir, name); - var ext = path.extname(filename).toLowerCase(); + var self = this; + if (self.body && self.body.$$schema) { + self.body.$$controller = self; + self.body.$insert(helper, callback); + } else { + var model = self.getSchema().default(); + model.$$controller = self; + model.$insert(helper, callback); + } + return self; +}; - if (ext !== EXTENSION_JS && ext !== EXTENSION_COFFEE) - return; +ControllerProto.$update = function(helper, callback) { - if (names.length > 0 && names.indexOf(name.substring(0, name.length - 3)) === -1) - return; + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } - var test = require(filename); + var self = this; + if (self.body && self.body.$$schema) { + self.body.$$controller = self; + self.body.$update(helper, callback); + } else { + var model = self.getSchema().default(); + model.$$controller = self; + model.$update(helper, callback); + } + return self; +}; - try { - var isRun = typeof(test.run) !== UNDEFINED; - var isInstall = typeof(test.isInstall) !== UNDEFINED; - var isInit = typeof(test.init) !== UNDEFINED; - var isLoad = typeof(test.load) !== UNDEFINED; +ControllerProto.$patch = function(helper, callback) { - _test = name; + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } - if (isRun) - test.run(self, name); - else if (isInstall) - test.install(self, name); - else if (isInit) - test.init(self, name); - else if (isLoad) - test.load(self, name); + 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; +}; - counter++; +ControllerProto.$remove = function(helper, callback) { - } catch (ex) { - setTimeout(function() { - framework.stop(); - }, 500); - throw ex; - } - }); + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } - _test = ''; + var self = this; + self.getSchema().remove(helper, callback, self); + return this; +}; - if (counter === 0) { - if (cb) cb(); - if (stop) setTimeout(function() { - framework.stop(); - }, 500); - return self; - } +ControllerProto.$workflow = function(name, helper, callback) { + var self = this; - setTimeout(function() { - console.log('====== TESTING ======'); - console.log(''); - self.testing(stop, function() { - self.isTest = false; - if (cb) - cb(); - }); - }, 100); + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } - return self; + 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; }; -/* - Clear temporary directory - return {Framework} -*/ -Framework.prototype.clear = function() { +ControllerProto.$workflow2 = function(name, helper, callback) { - var self = this; - var dir = utils.combine(self.config['directory-temp']); + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = null; + } - if (!fs.existsSync(dir)) - return self; + var self = this; + self.getSchema().workflow2(name, helper, callback, self); + return self; +}; - fs.readdir(dir, function(err, files) { - if (err) - return; +ControllerProto.$hook = function(name, helper, callback) { + var self = this; - var arr = []; - var length = files.length; - for (var i = 0; i < length; i++) - arr.push(utils.combine(self.config['directory-temp'], files[i])); + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = EMPTYOBJECT; + } - self.unlink(arr); - }); + if (self.body && self.body.$$schema) { + self.body.$$controller = self; + self.body.$hook(name, helper, callback); + } else + self.getSchema().hook2(name, helper, callback, self); - // clear static cache - self.temporary.path = {}; - self.temporary.range = {}; - return self; + return self; }; -/* - INTERNAL: Force remove files - return {Framework} -*/ -Framework.prototype.unlink = function(arr, callback) { - var self = this; +ControllerProto.$hook2 = function(name, helper, callback) { - if (typeof(arr) === STRING) - arr = [arr]; + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = EMPTYOBJECT; + } - if (arr.length === 0) { - if (callback) - callback(); - return; - } + var self = this; + self.getSchema().hook2(name, helper, callback, self); + return self; +}; - var filename = arr.shift(); - if (!filename) { - if (callback) - callback(); - return; - } +ControllerProto.$transform = function(name, helper, callback) { - fs.unlink(filename, function() { - self.unlink(arr, callback); - }); + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = EMPTYOBJECT; + } - return self; + var self = this; + if (self.body && self.body.$$schema) { + self.body.$$controller = self; + self.body.$transform(name, helper, callback); + } else + self.getSchema().transform2(name, helper, callback, self); + return self; }; -/* - Cryptography (encrypt) - @value {String} - @key {String} - @isUniqe {Boolean} :: optional, default true - return {String} -*/ -Framework.prototype.encrypt = function(value, key, isUnique) { +ControllerProto.$transform2 = function(name, helper, callback) { - var self = this; - var type = typeof(value); + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = EMPTYOBJECT; + } - if (type === UNDEFINED) - return ''; + var self = this; + self.getSchema().transform2(name, helper, callback, self); + return self; +}; - if (typeof(key) === BOOLEAN) { - var tmp = isUnique; - isUnique = key; - key = tmp; - } +ControllerProto.$operation = function(name, helper, callback) { - if (type === FUNCTION) - value = value(); + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = EMPTYOBJECT; + } - if (type === NUMBER) - value = value.toString(); + var self = this; + if (self.body && self.body.$$schema) { + self.body.$$controller = self; + self.body.$operation(name, helper, callback); + } else + self.getSchema().operation2(name, helper, callback, self); + return self; +}; - if (type === OBJECT) - value = JSON.stringify(value); +ControllerProto.operation = function(name, value, callback, options) { + OPERATION(name, value, callback, options, this); + return this; +}; - return value.encrypt(self.config.secret + '=' + key, isUnique); +ControllerProto.tasks = function() { + var tb = new TaskBuilder(this); + // tb.callback(this.callback()); + return tb; }; -/* - Cryptography (decrypt) - @value {String} - @key {String} - @jsonConvert {Boolean} :: optional (convert string to JSON) - return {String or Object} -*/ -Framework.prototype.decrypt = function(value, key, jsonConvert) { +ControllerProto.$operation2 = function(name, helper, callback) { - if (typeof(key) === BOOLEAN) { - var tmp = jsonConvert; - jsonConvert = key; - key = tmp; - } + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = EMPTYOBJECT; + } + + var self = this; + self.getSchema().operation2(name, helper, callback, self); + return self; +}; - if (typeof(jsonConvert) !== BOOLEAN) - jsonConvert = true; +ControllerProto.$exec = function(name, helper, callback) { + var self = this; - var self = this; - var result = (value || '').decrypt(self.config.secret + '=' + key); + if (callback == null && typeof(helper) === 'function') { + callback = helper; + helper = EMPTYOBJECT; + } - if (result === null) - return null; + if (callback == null) + callback = self.callback(); - if (jsonConvert) { - if (result.isJSON()) - return JSON.parse(result); - return null; - } + if (self.body && self.body.$$schema) { + self.body.$$controller = self; + self.body.$exec(name, helper, callback); + return self; + } - return result; + var tmp = self.getSchema().create(); + tmp.$$controller = self; + tmp.$exec(name, helper, callback); + return self; }; -/* - Hash value - @type {String} :: sha1, sha256, sha512, md5 - @value {Object} - @salt {String or Boolean} :: custom salt {String} or secret as salt {undefined or Boolean} - return {String} -*/ -Framework.prototype.hash = function(type, value, salt) { - var hash = crypto.createHash(type); - var plus = ''; +ControllerProto.$async = function(callback, index) { + var self = this; - if (typeof(salt) === STRING) - plus = salt; - else if (salt !== false) - plus = (this.config.secret || ''); + if (self.body && self.body.$$schema) { + self.body.$$controller = self; + return self.body.$async(callback, index); + } - hash.update(value.toString() + plus, ENCODING); - return hash.digest('hex'); + var model = self.getSchema().default(); + model.$$controller = self; + return model.$async(callback, index); }; -/* - Resource reader - @name {String} :: filename of resource - @key {String} - return {String} -*/ -Framework.prototype.resource = function(name, key) { +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 = 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])); +}; - if (typeof(key) === UNDEFINED || name.length === 0) { - key = name; - name = 'default'; - } +/** + * Renders component + * @param {String} name A component name + * @param {Object} settings Optional, settings. + * @model {Object} settings Optional, model for the component. + * @return {String} + */ +ControllerProto.component = function(name, settings, model) { + var filename = F.components.views[name]; + if (filename) { + 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 ''; +}; + +ControllerProto.$components = function(group, settings) { + + if (group) { + var keys = Object.keys(F.components.instances); + var output = []; + for (var i = 0, length = keys.length; i < length; i++) { + var component = F.components.instances[keys[i]]; + if (component.group === group) { + 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'); + } + + return ''; +}; - var self = this; - var res = self.resources[name]; +/** + * Reads / Writes cookie + * @param {String} name + * @param {String} value + * @param {String/Date} expires + * @param {Object} options + * @return {String/Controller} + */ +ControllerProto.cookie = function(name, value, expires, options) { + var self = this; + if (value === undefined) + return self.req.cookie(name); + self.res.cookie(name, value, expires, options); + return self; +}; - if (typeof(res) !== UNDEFINED) - return res[key]; +/** + * Clears uploaded files + * @return {Controller} + */ +ControllerProto.clear = function() { + var self = this; + self.req.clear(); + return self; +}; - var fileName = utils.combine(self.config['directory-resources'], name + '.resource'); +/** + * Translates text + * @param {String} text + * @return {String} + */ +ControllerProto.translate = function(language, text) { - if (!fs.existsSync(fileName)) - return ''; + if (!text) { + text = language; + language = this.language; + } - var obj = fs.readFileSync(fileName).toString(ENCODING).configuration(); - self.resources[name] = obj; - return obj[key] || ''; + return F.translate(language, text); }; -Framework.prototype.configureMapping = function(content, rewrite) { +/** + * Exec middleware + * @param {String Array} names Middleware name. + * @param {Object} options Custom options for middleware. + * @param {Function} callback + * @return {Controller} + */ +ControllerProto.middleware = function(names, options, callback) { - var self = this; - var filename = utils.combine('/', 'versions'); + if (typeof(names) === 'string') + names = [names]; - if (typeof(rewrite) === UNDEFINED) - rewrite = true; + if (typeof(options) === 'function') { + var tmp = callback; + callback = options; + options = tmp; + } - if (!fs.existsSync(filename)) { - self.versions = null; - return; - } + if (!options) + options = EMPTYOBJECT; - content = (typeof(content) !== STRING ? fs.readFileSync(filename).toString(ENCODING) : content); + var self = this; - if (content.length === 0) { - self.versions = null; - return self; - } + if (self.req.$total_middleware) + self.req.$total_middleware = null; - var mapping = content.configuration(); - var arr = Object.keys(mapping); + async_middleware(0, self.req, self.res, names, () => callback && callback(), options, self); + return self; +}; - if (rewrite) { - self.versions = arr.length === 0 ? null : mapping; - return self; - } +ControllerProto.nocache = function() { + this.req.nocache(); + return this; +}; - if (arr.length === 0) - return self; +/** + * Creates a pipe between the current request and target URL + * @param {String} url + * @param {Object} headers Optional, custom headers. + * @param {Function(err)} callback Optional. + * @return {Controller} + */ +ControllerProto.pipe = function(url, headers, callback) { + this.res.proxy(url, headers, null, callback); + return this; +}; - if (self.versions === null) - self.versions = {}; +ControllerProto.encrypt = function() { + return F.encrypt.apply(framework, arguments); +}; - var length = arr.length; +ControllerProto.decrypt = function() { + return F.decrypt.apply(framework, arguments); +}; - for (var i = 0; i < length; i++) { - var key = arr[i]; - self.versions[key] = mapping[key]; - } +/** + * Creates a hash (alias for F.hash()) + * @return {Controller} + */ +ControllerProto.hash = function() { + OBSOLETE('controller.hash()', 'Use String.prototype.hash()'); + return F.hash.apply(framework, arguments); +}; - return self; +/** + * Sets a response header + * @param {String} name + * @param {String} value + * @return {Controller} + */ +ControllerProto.header = function(name, value) { + this.res.setHeader(name, value); + return this; }; -/* - INTERNAL: Framework configure - @arr {String Array or String (filename)} :: optional - @rewrite {Boolean} :: optional, default true - return {Framework} -*/ -Framework.prototype.configure = function(arr, rewrite) { - - var self = this; - var type = typeof(arr); +/** + * Gets a hostname + * @param {String} path + * @return {Controller} + */ +ControllerProto.host = function(path) { + return this.req.hostname(path); +}; - if (type === STRING) { - var filename = utils.combine('/', arr); - if (!fs.existsSync(filename)) - return self; - arr = fs.readFileSync(filename).toString(ENCODING).split('\n'); - } +ControllerProto.hostname = function(path) { + return this.req.hostname(path); +}; - if (type === UNDEFINED) { +ControllerProto.resource = function(name, key) { + return F.resource(name, key); +}; - var filenameA = utils.combine('/', 'config'); - var filenameB = utils.combine('/', 'config-' + (self.config.debug ? 'debug' : 'release')); +/** + * Error caller + * @param {Error/String} err + * @return {Controller/Function} + */ +ControllerProto.error = function(err) { + var self = this; - arr = []; + // Custom errors + if (err instanceof ErrorBuilder) { + self.content(err); + return self; + } - if (fs.existsSync(filenameA)) - arr = arr.concat(fs.readFileSync(filenameA).toString(ENCODING).split('\n')); + var result = F.error(typeof(err) === 'string' ? new Error(err) : err, self.name, self.uri); + if (err === undefined) + return result; - if (fs.existsSync(filenameB)) - arr = arr.concat(fs.readFileSync(filenameB).toString(ENCODING).split('\n')); - } + self.req.$total_exception = err; + self.exception = err; + return self; +}; - if (!arr instanceof Array) - return self; +ControllerProto.invalid = function(status) { - if (arr.length === 0) - return self; + var self = this; - if (typeof(rewrite) === UNDEFINED) - rewrite = true; + if (status instanceof ErrorBuilder) { + setImmediate(next_controller_invalid, self, status); + return status; + } - var obj = {}; - var accepts = null; - var length = arr.length; + var type = typeof(status); - for (var i = 0; i < length; i++) { - var str = arr[i]; + if (type === 'number') + self.status = status; - if (str === '' || str[0] === '#' || (str[0] === '/' || str[1] === '/')) - continue; + var builder = new ErrorBuilder(); - var index = str.indexOf(':'); - if (index === -1) - continue; + if (type === 'string') + builder.push(status); + else if (status instanceof Error) + builder.push(status); - var name = str.substring(0, index).trim(); + setImmediate(next_controller_invalid, self, builder); + return builder; +}; - if (name === 'debug' || name === 'resources') - continue; +function next_controller_invalid(self, builder) { + self.content(builder); +} - var value = str.substring(index + 1).trim(); +/** + * Registers a new problem + * @param {String} message + * @return {Controller} + */ +ControllerProto.wtf = ControllerProto.problem = function(message) { + F.problem(message, this.name, this.uri, this.ip); + return this; +}; - switch (name) { - case 'default-request-length': - case 'default-websocket-request-length': - case 'default-request-timeout': - obj[name] = utils.parseInt(value); - break; - case 'static-accepts-custom': - accepts = value.replace(/\s/g, '').split(','); - break; - case 'static-accepts': - obj[name] = value.replace(/\s/g, '').split(','); - break; - case 'default-websocket-encodedecode': - case 'allow-gzip': - case 'allow-websocket': - case 'allow-compile-css': - case 'allow-compile-js': - obj[name] = value.toLowerCase() === 'true' || value === '1'; - break; - case 'version': - obj[name] = value; - break; - default: - obj[name] = value.isNumber() ? utils.parseInt(value) : value.isNumber(true) ? utils.parseFloat(value) : value.isBoolean() ? value.toLowerCase() === 'true' : value; - break; - } - } +/** + * Registers a new change + * @param {String} message + * @return {Controller} + */ +ControllerProto.change = function(message) { + F.change(message, this.name, this.uri, this.ip); + return this; +}; - utils.extend(self.config, obj, rewrite); +/** + * Trace + * @param {String} message + * @return {Controller} + */ +ControllerProto.trace = function(message) { + F.trace(message, this.name, this.uri, this.ip); + return this; +}; - if (self.config['etag-version'] === '') - self.config['etag-version'] = self.config.version.replace(/\.|\s/g, ''); +/** + * Transfer to new route + * @param {String} url Relative URL. + * @param {String Array} flags Route flags (optional). + * @return {Boolean} + */ +ControllerProto.transfer = function(url, flags) { - process.title = 'total: ' + self.config.name.removeDiacritics().toLowerCase().replace(/\s/g, '-').substring(0, 8); + var self = this; + var length = F.routes.web.length; + var path = framework_internal.routeSplit(url.trim()); - if (accepts !== null && accepts.length > 0) { - accepts.forEach(function(accept) { - if (self.config['static-accepts'].indexOf(accept) === -1) - self.config['static-accepts'].push(accept); - }); - } + var isSystem = url[0] === '#'; + var noFlag = !flags || flags.length === 0 ? true : false; + var selected = null; - if (self.config['allow-performance']) - http.globalAgent.maxSockets = 9999; + self.req.$isAuthorized = true; - self.emit('configure', self.config); - return self; -}; + for (var i = 0; i < length; i++) { -/* - Static file routing - @name {String} :: filename - return {String} -*/ -Framework.prototype.routeJS = function(name) { - var self = this; + var route = F.routes.web[i]; - if (name.lastIndexOf(EXTENSION_JS) === -1) - name += EXTENSION_JS; + if (route.isWILDCARD) { + if (!framework_internal.routeCompare(path, route.url, isSystem, true)) + continue; + } else { + if (!framework_internal.routeCompare(path, route.url, isSystem)) + continue; + } - return self._routeStatic(name, self.config['static-url-js']); -}; + if (noFlag) { + selected = route; + break; + } -/* - Static file routing - @name {String} :: filename - return {String} -*/ -Framework.prototype.routeCSS = function(name) { - var self = this; + if (route.flags && route.flags.length) { + var result = framework_internal.routeCompareFlags(route.flags, flags, true); + if (result === -1) + self.req.$isAuthorized = false; + if (result < 1) + continue; + } - if (name.lastIndexOf('.css') === -1) - name += '.css'; + selected = route; + break; + } - return self._routeStatic(name, self.config['static-url-css']); -}; + if (!selected) + return false; -/* - Static file routing - @name {String} :: filename - return {String} -*/ -Framework.prototype.routeImage = function(name) { - var self = this; - return self._routeStatic(name, self.config['static-url-image']); -}; + self.cancel(); + self.req.path = EMPTYARRAY; + self.req.$total_transfer = true; + self.req.$total_success(); -/* - Static file routing - @name {String} :: filename - return {String} -*/ -Framework.prototype.routeVideo = function(name) { - var self = this; - return self._routeStatic(name, self.config['static-url-video']); -}; + // Because of dynamic params + // Hidden variable + self.req.$path = framework_internal.routeSplit(url, true); -/* - Static file routing - @name {String} :: filename - return {String} -*/ -Framework.prototype.routeFont = function(name) { - var self = this; - return self._routeStatic(name, self.config['static-url-font']); + self.route = self.req.$total_route = selected; + self.req.$total_execute(404); + return true; }; -/* - Static file routing - @name {String} :: filename - return {String} -*/ -Framework.prototype.routeDownload = function(name) { - var self = this; - return self._routeStatic(name, self.config['static-url-download']); +ControllerProto.cancel = function() { + this.isCanceled = true; + return this; }; -/* - Static file routing - @name {String} :: filename - return {String} -*/ -Framework.prototype.routeStatic = function(name) { - var self = this; - return self._routeStatic(name, self.config['static-url']); +ControllerProto.log = function() { + F.log.apply(F, arguments); + return this; }; -/* - Internal static file routing - @name {String} :: filename - @directory {String} :: directory - return {String} -*/ -Framework.prototype._routeStatic = function(name, directory) { - return directory + this._version(name); +ControllerProto.logger = function() { + F.logger.apply(F, arguments); + return this; }; -/* - Internal mapping function - @name {String} :: filename - return {String} -*/ -Framework.prototype._version = function(name) { - var self = this; - - if (self.versions !== null) - name = self.versions[name] || name; +ControllerProto.meta = function() { + var self = this; - if (self.onVersion !== null) - name = self.onVersion(name) || name; + if (arguments[0]) + self.repository[REPOSITORY_META_TITLE] = arguments[0].encode(); - return name; -}; + if (arguments[1]) + self.repository[REPOSITORY_META_DESCRIPTION] = arguments[1].encode(); -/* - Internal function - @req {HttpRequest} - @url {String} - @flags {String Array} - @noLoggedUnlogged {Boolean} :: optional, default false - return {ControllerRoute} -*/ -Framework.prototype.lookup = function(req, url, flags, noLoggedUnlogged) { + if (arguments[2] && arguments[2].length) + self.repository[REPOSITORY_META_KEYWORDS] = (arguments[2] instanceof Array ? arguments[2].join(', ') : arguments[2]); - var self = this; - var isSystem = url[0] === '#'; + if (arguments[3]) + self.repository[REPOSITORY_META_IMAGE] = arguments[3]; - if (isSystem) - req.path = [url]; + return self; +}; - var subdomain = req.subdomain === null ? null : req.subdomain.join('.'); - var length = self.routes.web.length; +ControllerProto.$dns = function() { - for (var i = 0; i < length; i++) { + var builder = ''; + var length = arguments.length; - var route = self.routes.web[i]; + for (var i = 0; i < length; i++) + builder += ''; - if (!internal.routeCompareSubdomain(subdomain, route.subdomain)) - continue; + this.head(builder); + return ''; +}; - if (route.isASTERIX) { - if (!internal.routeCompare(req.path, route.url, isSystem, true)) - continue; - } else { - if (!internal.routeCompare(req.path, route.url, isSystem)) - continue; - } +ControllerProto.$prefetch = function() { - if (isSystem) - return route; + var builder = ''; + var length = arguments.length; - if (route.flags !== null && route.flags.length > 0) { + for (var i = 0; i < length; i++) + builder += ''; - var result = internal.routeCompareFlags(flags, route.flags, noLoggedUnlogged ? true : route.isMEMBER); - if (result === -1) - req.isAuthorized = false; + this.head(builder); + return ''; +}; - if (result < 1) - continue; +ControllerProto.$prerender = function() { - } else { + var builder = ''; + var length = arguments.length; - if (flags.indexOf('xss') !== -1) - continue; - } + for (var i = 0; i < length; i++) + builder += ''; - return route; - } + this.head(builder); + return ''; +}; - return null; +ControllerProto.$next = function(value) { + this.head(''); + return ''; }; -/* - Internal function - @req {HttpRequest} - @url {String} - return {WebSocketRoute} -*/ -Framework.prototype.lookup_websocket = function(req, url, noLoggedUnlogged) { +ControllerProto.$prev = function(value) { + this.head(''); + return ''; +}; - var self = this; - var subdomain = req.subdomain === null ? null : req.subdomain.join('.'); - var length = self.routes.websockets.length; +ControllerProto.$canonical = function(value) { + this.head(''); + return ''; +}; - for (var i = 0; i < length; i++) { +ControllerProto.$meta = function() { + var self = this; - var route = self.routes.websockets[i]; + if (arguments.length) { + self.meta.apply(self, arguments); + return ''; + } - if (!internal.routeCompareSubdomain(subdomain, route.subdomain)) - continue; + 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]); +}; - if (route.isASTERIX) { - if (!internal.routeCompare(req.path, route.url, false, true)) - continue; - } else { - if (!internal.routeCompare(req.path, route.url, false)) - continue; - } +ControllerProto.title = function(value) { + this.$title(value); + return this; +}; - if (route.flags !== null && route.flags.length > 0) { +ControllerProto.description = function(value) { + this.$description(value); + return this; +}; - var result = internal.routeCompareFlags(req.flags, route.flags, noLoggedUnlogged ? true : route.isMEMBER); +ControllerProto.keywords = function(value) { + this.$keywords(value); + return this; +}; - if (result === -1) - req.isAuthorized = false; +ControllerProto.author = function(value) { + this.$author(value); + return this; +}; - if (result < 1) - continue; +ControllerProto.$title = function(value) { + if (value) + this.repository[REPOSITORY_META_TITLE] = value.encode(); + return ''; +}; - } +ControllerProto.$title2 = function(value) { + var current = this.repository[REPOSITORY_META_TITLE]; + if (value) + this.repository[REPOSITORY_META_TITLE] = (current ? current : '') + value.encode(); + return ''; +}; - return route; - } +ControllerProto.$description = function(value) { + if (value) + this.repository[REPOSITORY_META_DESCRIPTION] = value.encode(); + return ''; +}; - return null; +ControllerProto.$keywords = function(value) { + if (value && value.length) + this.repository[REPOSITORY_META_KEYWORDS] = (value instanceof Array ? value.join(', ') : value).encode(); + return ''; }; -/* - Accepts file - @extension {String} - @contentType {String} :: optional - return {Framework} -*/ -Framework.prototype.accepts = function(extension, contentType) { +ControllerProto.$author = function(value) { + if (value) + this.repository[REPOSITORY_META_AUTHOR] = value.encode(); + return ''; +}; - var self = this; +ControllerProto.sitemap_navigation = function(name, language) { + return F.sitemap_navigation(name || this.sitemapid, language || this.language); +}; - if (extension[0] !== '.') - extension = '.' + extension; +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) : ''; +}; - if (self.config['static-accepts'].indexOf(extension) === -1) - self.config['static-accepts'].push(extension); +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) : ''; +}; - if (contentType) - utils.setContentType(extension, contentType); +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) : ''; +}; - return self; +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) : ''; }; -/* - @name {String} - @id {String} :: optional, Id of process - @timeout {Number} :: optional, timeout - default undefined (none) - return {Worker(fork)} -*/ -Framework.prototype.worker = function(name, id, timeout) { +ControllerProto.sitemap_add = function(parent, name, url) { - var self = this; - var fork = null; - var type = typeof(id); + var self = this; + var sitemap = self.repository[REPOSITORY_SITEMAP]; - if (type === NUMBER && typeof(timeout) === UNDEFINED) { - timeout = id; - id = null; - type = UNDEFINED; - } + if (!sitemap) { + sitemap = self.sitemap(self.sitemapid || name); + if (!sitemap) + return EMPTYARRAY; + } - if (type === STRING) - fork = self.workers[id] || null; + var index = sitemap.findIndex('id', parent); + if (index === -1) + return sitemap; - if (fork !== null) - return fork; + var obj = { sitemap: '', id: '', name: name, url: url, last: false, first: false, index: index, wildcard: false, formatName: false, formatUrl: false, localizeName: false, localizeUrl: false }; - var filename = utils.combine(self.config['directory-workers'], name); + sitemap.splice(index + 1, 0, obj); - if (self.isCoffee) { - if (fs.existsSync(filename + EXTENSION_COFFEE)) - filename += EXTENSION_COFFEE; - else - filename += EXTENSION_JS; - } else - filename += EXTENSION_JS; + if (index) { + var tmp = index; + for (var i = index + 1; i > -1; i--) + sitemap[i].index = tmp++; + } - fork = child.fork(filename, { - cwd: directory - }); - id = name + '_' + new Date().getTime(); - fork.__id = id; - self.workers[id] = fork; + return sitemap; +}; - fork.on('exit', function() { - var self = this; - if (self.__timeout) - clearTimeout(self.__timeout); +ControllerProto.sitemap_change = function(name, type, a, b, c, d, e, f) { - delete framework.workers[self.__id]; - }); + var self = this; + var sitemap = self.repository[REPOSITORY_SITEMAP]; - if (typeof(timeout) !== NUMBER) - return fork; + if (!sitemap) { + sitemap = self.sitemap(self.sitemapid || name); + if (!sitemap) + return EMPTYARRAY; + } - fork.__timeout = setTimeout(function() { + if (!sitemap.$cloned) { + sitemap = U.clone(sitemap); + sitemap.$cloned = true; + self.repository[REPOSITORY_SITEMAP] = sitemap; + } - fork.kill(); - fork = null; + var isFn = typeof(a) === 'function'; - }, timeout); + for (var i = 0, length = sitemap.length; i < length; i++) { - return fork; -}; + var item = sitemap[i]; + if (item.id !== name) + continue; -// ********************************************************************************* -// ================================================================================= -// Framework Restrictions -// 1.01 -// ================================================================================= -// ********************************************************************************* - -function FrameworkRestrictions(framework) { - this.framework = framework; - this.isRestrictions = false; - this.isAllowedIP = false; - this.isBlockedIP = false; - this.isAllowedCustom = false; - this.isBlockedCustom = false; - this.allowedIP = []; - this.blockedIP = []; - this.allowedCustom = {}; - this.blockedCustom = {}; - this.allowedCustomKeys = []; - this.blockedCustomKeys = []; -}; - -/* - Allow IP or custom header - @name {String} :: IP or Header name - @value {RegExp} :: optional, header value - return {Framework} -*/ -FrameworkRestrictions.prototype.allow = function(name, value) { - - var self = this; - - // IP address - if (typeof(value) === UNDEFINED) { - self.allowedIP.push(name); - self.refresh(); - return self.framework; - } - - // Custom header - if (typeof(self.allowedCustom[name]) === UNDEFINED) - self.allowedCustom[name] = [value]; - else - self.allowedCustom[name].push(value); - - self.refresh(); - return self.framework; - -}; - -/* - Disallow IP or custom header - @name {String} :: IP or Header name - @value {RegExp} :: optional, header value - return {Framework} -*/ -FrameworkRestrictions.prototype.disallow = function(name, value) { - - var self = this; - - // IP address - if (typeof(value) === UNDEFINED) { - self.blockedIP.push(name); - self.refresh(); - return self.framework; - } - - // Custom header - if (typeof(self.blockedCustom[name]) === UNDEFINED) - self.blockedCustom[name] = [value]; - else - self.blockedCustom[name].push(value); - - self.refresh(); - return self.framework; - -}; - -/* - INTERNAL: Refresh internal informations - return {Framework} -*/ -FrameworkRestrictions.prototype.refresh = function() { - - var self = this; - - self.isAllowedIP = self.allowedIP.length > 0; - self.isBlockedIP = self.blockedIP.length > 0; - - self.isAllowedCustom = !utils.isEmpty(self.allowedCustom); - self.isBlockedCustom = !utils.isEmpty(self.blockedCustom); - - self.allowedCustomKeys = Object.keys(self.allowedCustom); - self.blockedCustomKeys = Object.keys(self.blockedCustom); - - self.isRestrictions = self.isAllowedIP || self.isBlockedIP || self.isAllowedCustom || self.isBlockedCustom; - - return self.framework; -}; - -/* - Clear all restrictions for IP - return {Framework} -*/ -FrameworkRestrictions.prototype.clearIP = function() { - var self = this; - self.allowedIP = []; - self.blockedIP = []; - self.refresh(); - return self.framework; -} - -/* - Clear all restrictions for custom headers - return {Framework} -*/ -FrameworkRestrictions.prototype.clearHeaders = function() { - var self = this; - self.allowedCustom = {}; - self.blockedCustom = {}; - self.allowedCustomKeys = []; - self.blockedCustomKeys = []; - self.refresh(); - return self.framework; -} - -/* - INTERNAL: Restrictions using - return {Framework} -*/ -FrameworkRestrictions.prototype._allowedCustom = function(headers) { - - var self = this; - var length = self.allowedCustomKeys.length; - - for (var i = 0; i < length; i++) { - - var key = self.allowedCustomKeys[i]; - var value = headers[key]; - if (typeof(value) === UNDEFINED) - return false; - - var arr = self.allowedCustom[key]; - var max = arr.length; - - for (var j = 0; j < max; j++) { - - if (value.search(arr[j]) !== -1) - return false; - - } - } - - return true; -}; - -/* - INTERNAL: Restrictions using - return {Framework} -*/ -FrameworkRestrictions.prototype._blockedCustom = function(headers) { - - var self = this; - var length = self.blockedCustomKeys.length; - - for (var i = 0; i < length; i++) { - - var key = self.blockedCustomKeys[i]; - var value = headers[key]; - - if (typeof(value) === UNDEFINED) - return false; - - var arr = self.blockedCustom[key]; - var max = arr.length; - - for (var j = 0; j < max; j++) { - if (value.search(arr[j]) !== -1) - return true; - } - - } - - return false; -}; - -// ********************************************************************************* -// ================================================================================= -// Framework File System -// 1.01 -// ================================================================================= -// ********************************************************************************* - -function FrameworkFileSystem(framework) { - - this.framework = framework; - this.config = framework.config; - - this.create = { - css: this.createCSS.bind(this), - js: this.createJS.bind(this), - view: this.createView.bind(this), - content: this.createContent.bind(this), - template: this.createTemplate.bind(this), - resource: this.createResource.bind(this), - temporary: this.createTemporary.bind(this), - worker: this.createWorker.bind(this), - file: this.createFile.bind(this) - }; - - this.rm = { - css: this.deleteCSS.bind(this), - js: this.deleteJS.bind(this), - view: this.deleteView.bind(this), - content: this.deleteContent.bind(this), - template: this.deleteTemplate.bind(this), - resource: this.deleteResource.bind(this), - temporary: this.deleteTemporary.bind(this), - worker: this.deleteWorker.bind(this), - file: this.deleteFile.bind(this) - }; -} - -/* - Delete a file - CSS - @name {String} - return {Boolean} -*/ -FrameworkFileSystem.prototype.deleteCSS = function(name) { - var self = this; - - if (name.lastIndexOf('.css') === -1) - name += '.css'; - - var filename = utils.combine(self.config['directory-public'], self.config['static-url-css'], name); - return self.deleteFile(filename); -}; - -/* - Delete a file - JS - @name {String} - return {Boolean} -*/ -FrameworkFileSystem.prototype.deleteJS = function(name) { - var self = this; - - if (name.lastIndexOf(EXTENSION_JS) === -1) - name += EXTENSION_JS; - - var filename = utils.combine(self.config['directory-public'], self.config['static-url-js'], name); - return self.deleteFile(filename); -}; - -/* - Delete a file - View - @name {String} - return {Boolean} -*/ -FrameworkFileSystem.prototype.deleteView = function(name) { - var self = this; - - if (name.lastIndexOf('.html') === -1) - name += '.html'; - - var filename = utils.combine(self.config['directory-views'], name); - return self.deleteFile(filename); -}; - -/* - Delete a file - Content - @name {String} - return {Boolean} -*/ -FrameworkFileSystem.prototype.deleteContent = function(name) { - var self = this; - - if (name.lastIndexOf('.html') === -1) - name += '.html'; - - var filename = utils.combine(self.config['directory-contents'], name); - return self.deleteFile(filename); -}; - -/* - Delete a file - Worker - @name {String} - return {Boolean} -*/ -FrameworkFileSystem.prototype.deleteWorker = function(name) { - var self = this; - - if (name.lastIndexOf(EXTENSION_JS) === -1) - name += EXTENSION_JS; - - var filename = utils.combine(self.config['directory-workers'], name); - return self.deleteFile(filename); -}; - -/* - Delete a file - Template - @name {String} - return {Boolean} -*/ -FrameworkFileSystem.prototype.deleteTemplate = function(name) { - var self = this; - - if (name.lastIndexOf('.html') === -1) - name += '.html'; - - var filename = utils.combine(self.config['directory-templates'], name); - return self.deleteFile(filename); -}; - -/* - Delete a file - Resource - @name {String} - return {Boolean} -*/ -FrameworkFileSystem.prototype.deleteResource = function(name) { - var self = this; - - if (name.lastIndexOf('.resource') === -1) - name += '.resource'; - - var filename = utils.combine(self.config['directory-resources'], name); - return self.deleteFile(filename); -}; - -/* - Delete a file - Temporary - @name {String} - return {Boolean} -*/ -FrameworkFileSystem.prototype.deleteTemporary = function(name) { - var self = this; - var filename = utils.combine(self.config['directory-temp'], name); - return self.deleteFile(filename); -}; - -/* - Internal :: Delete a file - @name {String} - return {Boolean} -*/ -FrameworkFileSystem.prototype.deleteFile = function(filename) { - var self = this; - - fs.exists(filename, function(exist) { - if (!exist) - return; - fs.unlink(filename); - }); - - return true; -}; - -/* - Create a file with the CSS - @name {String} - @content {String} - @rewrite {Boolean} :: optional (default false) - @append {Boolean} :: optional (default false) - return {Boolean} -*/ -FrameworkFileSystem.prototype.createCSS = function(name, content, rewrite, append) { - - var self = this; - - if ((content || '').length === 0) - return false; - - if (name.lastIndexOf('.css') === -1) - name += '.css'; - - var filename = utils.combine(self.config['directory-public'], self.config['static-url-css'], name); - return self.createFile(filename, content, append, rewrite); -}; - -/* - Create a file with the JavaScript - @name {String} - @content {String} - @rewrite {Boolean} :: optional (default false) - @append {Boolean} :: optional (default false) - return {Boolean} -*/ -FrameworkFileSystem.prototype.createJS = function(name, content, rewrite, append) { - - var self = this; - - if ((content || '').length === 0) - return false; - - if (name.lastIndexOf(EXTENSION_JS) === -1) - name += EXTENSION_JS; - - var filename = utils.combine(self.config['directory-public'], self.config['static-url-js'], name); - return self.createFile(filename, content, append, rewrite); -}; - -/* - Create a file with the template - @name {String} - @content {String} - @rewrite {Boolean} :: optional (default false) - @append {Boolean} :: optional (default false) - return {Boolean} -*/ -FrameworkFileSystem.prototype.createTemplate = function(name, content, rewrite, append) { - - var self = this; - - if ((content || '').length === 0) - return false; - - if (name.lastIndexOf('.html') === -1) - name += '.html'; - - self.framework._verify_directory('templates'); - - var filename = utils.combine(self.config['directory-templates'], name); - return self.createFile(filename, content, append, rewrite); -}; - -/* - Create a file with the view - @name {String} - @content {String} - @rewrite {Boolean} :: optional (default false) - @append {Boolean} :: optional (default false) - return {Boolean} -*/ -FrameworkFileSystem.prototype.createView = function(name, content, rewrite, append) { - - var self = this; - - if ((content || '').length === 0) - return false; - - if (name.lastIndexOf('.html') === -1) - name += '.html'; - - self.framework._verify_directory('views'); - - var filename = utils.combine(self.config['directory-views'], name); - return self.createFile(filename, content, append, rewrite); -}; - -/* - Create a file with the content - @name {String} - @content {String} - @rewrite {Boolean} :: optional (default false) - @append {Boolean} :: optional (default false) - return {Boolean} -*/ -FrameworkFileSystem.prototype.createContent = function(name, content, rewrite, append) { - - var self = this; - - if ((content || '').length === 0) - return false; - - if (name.lastIndexOf('.html') === -1) - name += '.html'; - - self.framework._verify_directory('contents'); - - var filename = utils.combine(self.config['directory-contents'], name); - return self.createFile(filename, content, append, rewrite); -}; - -/* - Create a file with the worker - @name {String} - @content {String} - @rewrite {Boolean} :: optional (default false) - @append {Boolean} :: optional (default false) - return {Boolean} -*/ -FrameworkFileSystem.prototype.createWorker = function(name, content, rewrite, append) { - - var self = this; - - if ((content || '').length === 0) - return false; - - if (name.lastIndexOf(EXTENSION_JS) === -1) - name += EXTENSION_JS; - - self.framework._verify_directory('workers'); - - var filename = utils.combine(self.config['directory-workers'], name); - return self.createFile(filename, content, append, rewrite); -}; - -/* - Create a file with the resource - @name {String} - @content {String or Object} - @rewrite {Boolean} :: optional (default false) - @append {Boolean} :: optional (default false) - return {Boolean} -*/ -FrameworkFileSystem.prototype.createResource = function(name, content, rewrite, append) { - - var self = this; - - if ((content || '').length === 0) - return false; - - if (name.lastIndexOf('.resource') === -1) - name += '.resource'; - - var builder = content; - - if (typeof(content) === OBJECT) { - builder = ''; - Object.keys(content).forEach(function(o) { - builder += o.padRight(20, ' ') + ': ' + content[o] + '\n'; - }); - } - - self.framework._verify_directory('resources'); - - var filename = utils.combine(self.config['directory-resources'], name); - return self.createFile(filename, builder, append, rewrite); -}; - -/* - Create a temporary file - @name {String} - @stream {Stream} - @callback {Function} :: function(err, filename) {} - return {Boolean} -*/ -FrameworkFileSystem.prototype.createTemporary = function(name, stream, callback) { - var self = this; - - self.framework._verify_directory('temp'); - - var filename = utils.combine(self.config['directory-temp'], name); - var writer = fs.createWriteStream(filename); - - if (callback) { - writer.on('error', function(err) { - callback(err, filename); - }); - writer.on('end', function() { - callback(null, filename); - }); - } - - stream.pipe(writer); - return self; -}; - -/* - Internal :: Create a file with the content - @filename {String} - @content {String} - @append {Boolean} - @rewrite {Boolean} - @callback {Function} :: optional - return {Boolean} -*/ -FrameworkFileSystem.prototype.createFile = function(filename, content, append, rewrite, callback) { - - var self = this; - - if (content.substring(0, 7) === 'http://' || content.substring(0, 8) === 'https://') { - - utils.request(content, 'GET', null, function(err, data) { - - if (!err) - self.createFile(filename, data, append, rewrite); - - if (typeof(callback) === FUNCTION) - callback(err, filename); - - }); - - return true; - } - - if ((content || '').length === 0) - return false; - - var exists = fs.existsSync(filename); - - if (exists && append) { - var data = fs.readFileSync(filename).toString(ENCODING); - - if (data.indexOf(content) === -1) { - fs.appendFileSync(filename, '\n' + content); - return true; - } - - return false; - } - - if (exists && !rewrite) - return false; - - fs.writeFileSync(filename, content, ENCODING); - - if (typeof(callback) === FUNCTION) - callback(null, filename); - - return true; -}; - -// ********************************************************************************* -// ================================================================================= -// Framework path -// ================================================================================= -// ********************************************************************************* - -function FrameworkPath(framework) { - this.framework = framework; - this.config = framework.config; -} - -/* - @filename {String} :: optional - return {String} -*/ -FrameworkPath.prototype.public = function(filename) { - var self = this; - self.framework._verify_directory('public'); - return utils.combine(self.config['directory-public'], filename || '').replace(/\\/g, '/'); -}; - -/* - @filename {String} :: optional - return {String} -*/ -FrameworkPath.prototype.logs = function(filename) { - var self = this; - self.framework._verify_directory('logs'); - return utils.combine(self.config['directory-logs'], filename || '').replace(/\\/g, '/'); -}; - -/* - @filename {String} :: optional - return {String} -*/ -FrameworkPath.prototype.components = function(filename) { - var self = this; - return utils.combine(self.config['directory-components'], filename || '').replace(/\\/g, '/'); -}; - -/* - @filename {String} :: optional - return {String} -*/ -FrameworkPath.prototype.models = function(filename) { - var self = this; - return utils.combine(self.config['directory-models'], filename || '').replace(/\\/g, '/'); -}; -/* - @filename {String} :: optional - return {String} -*/ -FrameworkPath.prototype.temp = function(filename) { - var self = this; - self.framework._verify_directory('temp'); - return utils.combine(self.config['directory-temp'], filename || '').replace(/\\/g, '/'); -}; - -FrameworkPath.prototype.temporary = function(filename) { - return this.temp(filename); -}; - -/* - @filename {String} :: optional - return {String} -*/ -FrameworkPath.prototype.views = function(filename) { - var self = this; - self.framework._verify_directory('views'); - return utils.combine(self.config['directory-views'], filename || '').replace(/\\/g, '/'); -}; - -/* - @filename {String} :: optional - return {String} -*/ -FrameworkPath.prototype.templates = function(filename) { - var self = this; - self.framework._verify_directory('templates'); - return utils.combine(self.config['directory-templates'], filename || '').replace(/\\/g, '/'); -}; - -/* - @filename {String} :: optional - return {String} -*/ -FrameworkPath.prototype.workers = function(filename) { - var self = this; - self.framework._verify_directory('workers'); - return utils.combine(self.config['directory-workers'], filename || '').replace(/\\/g, '/'); -}; - -/* - @filename {String} :: optional - return {String} -*/ -FrameworkPath.prototype.databases = function(filename) { - var self = this; - self.framework._verify_directory('databases'); - return utils.combine(self.config['directory-databases'], filename || '').replace(/\\/g, '/'); -}; - -/* - @filename {String} :: optional - return {String} -*/ -FrameworkPath.prototype.contents = function(filename) { - var self = this; - self.framework._verify_directory('contents'); - return utils.combine(self.config['directory-contents'], filename || '').replace(/\\/g, '/'); -}; - -/* - @filename {String} :: optional - return {String} -*/ -FrameworkPath.prototype.modules = function(filename) { - var self = this; - self.framework._verify_directory('modules'); - return utils.combine(self.config['directory-modules'], filename || '').replace(/\\/g, '/'); -}; - -/* - @filename {String} :: optional - return {String} -*/ -FrameworkPath.prototype.controllers = function(filename) { - var self = this; - self.framework._verify_directory('controllers'); - return utils.combine(self.config['directory-controllers'], filename || '').replace(/\\/g, '/'); -}; - -/* - @filename {String} :: optional - return {String} -*/ -FrameworkPath.prototype.definitions = function(filename) { - var self = this; - self.framework._verify_directory('definitions'); - return utils.combine(self.config['directory-definitions'], filename || '').replace(/\\/g, '/'); -}; - -/* - @filename {String} :: optional - return {String} -*/ -FrameworkPath.prototype.tests = function(filename) { - var self = this; - self.framework._verify_directory('tests'); - return utils.combine(self.config['directory-tests'], filename || '').replace(/\\/g, '/'); -}; - -/* - @filename {String} :: optional - return {String} -*/ -FrameworkPath.prototype.resources = function(filename) { - var self = this; - self.framework._verify_directory('resources'); - return utils.combine(self.config['directory-resources'], filename || '').replace(/\\/g, '/'); -}; - -/* - @filename {String} :: optional - return {String} -*/ -FrameworkPath.prototype.root = function(filename) { - return path.join(directory, filename || ''); -}; - -// ********************************************************************************* -// ================================================================================= -// Cache declaration -// ================================================================================= -// ********************************************************************************* - -/* - Cache class - @framework {Framework} -*/ -function FrameworkCache(framework) { - this.repository = {}; - this.framework = framework; - this.count = 1; - this.interval = null; -} - -/* - Cache init - return {Cache} -*/ -FrameworkCache.prototype.init = function(interval) { - - var self = this; - - self.interval = setInterval(function() { - framework.cache.recycle(); - }, interval || 1000 * 60); - - return self; -}; - -FrameworkCache.prototype.stop = function() { - var self = this; - clearInterval(self.interval); - return self; -}; - -FrameworkCache.prototype.clear = function() { - var self = this; - self.repository = {}; - return self; -}; - -/* - Internal function - return {Cache} -*/ -FrameworkCache.prototype.recycle = function() { - - var self = this; - var repository = self.repository; - var keys = Object.keys(repository); - var length = keys.length; - - self.count++; - - if (length === 0) { - self.framework.handlers.onservice(self.count); - return self; - } - - var expire = new Date(); - - for (var i = 0; i < length; i++) { - var o = keys[i]; - var value = repository[o]; - if (value.expire < expire) { - self.framework.emit('expire', o, value.value); - delete repository[o]; - } - } - - self.framework.handlers.onservice(self.count); - return self; -}; - -/* - Add item to cache - @name {String} - @value {Object} - @expire {Date} - return @value -*/ -FrameworkCache.prototype.add = function(name, value, expire) { - var self = this; - - if (typeof(expire) === UNDEFINED) - expire = new Date().add('m', 5); - - self.repository[name] = { - value: value, - expire: expire - }; - return value; -}; - -/* - Read item from cache - @name {String} - return {Object} -*/ -FrameworkCache.prototype.read = function(name) { - var self = this; - var value = self.repository[name] || null; - - if (value === null) - return null; - - if (value.expire < new Date()) - return null; - - return value.value; -}; - -/* - Update cache item expiration - @name {String} - @expire {Date} - return {Cache} -*/ -FrameworkCache.prototype.setExpire = function(name, expire) { - var self = this; - var obj = self.repository[name]; - - if (typeof(obj) === UNDEFINED) - return self; - - obj.expire = expire; - return self; -}; - -/* - Remove item from cache - @name {String} - return {Object} :: return value; -*/ -FrameworkCache.prototype.remove = function(name) { - var self = this; - var value = self.repository[name] || null; - - delete self.repository[name]; - return value; -}; - -/* - Remove all - @search {String} - return {Number} -*/ -FrameworkCache.prototype.removeAll = function(search) { - var self = this; - var count = 0; - var keys = Object.keys(self.repository); - var length = keys.length; - var isReg = utils.isRegExp(search); - - for (var i = 0; i < length; i++) { - - if (isReg) { - if (!search.test(keys[i])) - continue; - } else { - if (keys[i].indexOf(search) === -1) - continue; - } - - self.remove(keys[i]); - count++; - } - - return count; -}; - -/* - Cache function value - @name {String} - @fnCache {Function} :: params, @value {Object}, @expire {Date} - @fnCallback {Function} :: params, @value {Object} - return {Cache} -*/ -FrameworkCache.prototype.fn = function(name, fnCache, fnCallback) { - - var self = this; - var value = self.read(name); - - if (value !== null) { - if (fnCallback) - fnCallback(value); - return self; - } - - fnCache(function(value, expire) { - self.add(name, value, expire); - if (fnCallback) - fnCallback(value); - }); - - return self; -}; - -// ********************************************************************************* -// ================================================================================= -// Framework.Subscribe -// ================================================================================= -// ********************************************************************************* - -var REPOSITORY_HEAD = '$head'; -var REPOSITORY_ANGULAR = '$angular'; -var REPOSITORY_ANGULAR_LOCALE = '$angular-locale'; -var REPOSITORY_ANGULAR_COMMON = '$angular-common'; -var REPOSITORY_ANGULAR_CONTROLLER = '$angular-controller'; -var REPOSITORY_ANGULAR_OTHER = '$angular-other'; -var REPOSITORY_META = '$meta'; -var REPOSITORY_PLACE = '$place'; -var REPOSITORY_META_TITLE = '$title'; -var REPOSITORY_META_DESCRIPTION = '$description'; -var REPOSITORY_META_KEYWORDS = '$keywords'; -var REPOSITORY_META_IMAGE = '$image'; -var ATTR_END = '"'; - -function Subscribe(framework, req, res, type) { - this.framework = framework; - - this.handlers = { - _execute: this._execute.bind(this), - _cancel: this._cancel.bind(this), - _end: this._end.bind(this) - }; - - // type = 0 - GET, DELETE - // type = 1 - POST, PUT - // type = 2 - POST MULTIPART - // type = 3 - file routing - - // OPTIMALIZATION: saving memory and processor - if (type !== 3 && framework.onAuthorization !== null) - this.handlers._authorization = this._authorization.bind(this); - - if (type === 3) - this.handlers._endfile = this._endfile.bind(this); - else if (type === 1) - this.handlers._parsepost = this._parsepost.bind(this); - - this.controller = null; - this.req = req; - this.res = res; - this.route = null; - this.timeout = null; - this.isCanceled = false; - this.isMixed = false; - this.header = ''; - this.error = null; -} - -Subscribe.prototype.success = function() { - var self = this; - - if (self.timeout) - clearTimeout(self.timeout); - - self.timeout = null; - self.isCanceled = true; - return self; -}; - -Subscribe.prototype.file = function() { - var self = this; - self.req.on('end', self.handlers._endfile); - self.req.resume(); - return self; -}; - -/* - @header {String} :: Content-Type -*/ -Subscribe.prototype.multipart = function(header) { - - var self = this; - self.route = self.framework.lookup(self.req, self.req.uri.pathname, self.req.flags, true); - self.header = header; - - if (self.route === null) { - self.framework._request_stats(false, false); - self.framework.stats.request.blocked++; - self.req.connection.destroy(); - return; - } - - if (header.indexOf('mixed') === -1) { - self.framework._verify_directory('temp'); - internal.parseMULTIPART(self.req, header, self.route.maximumSize, self.framework.config['directory-temp'], self.framework.handlers.onxss, self.handlers._end); - return; - } - - self.isMixed = true; - self.execute(); -}; - -Subscribe.prototype.urlencoded = function() { - - var self = this; - self.route = self.framework.lookup(self.req, self.req.uri.pathname, self.req.flags, true); - - if (self.route === null) { - self.req.clear(true); - self.framework.stats.request.blocked++; - self.framework._request_stats(false, false); - self.req.connection.destroy(); - return; - } - - self.req.buffer_has = true; - self.req.buffer_exceeded = false; - self.req.on('data', self.handlers._parsepost); - self.end(); -}; - -Subscribe.prototype.end = function() { - var self = this; - self.req.on('end', self.handlers._end); - self.req.resume(); -}; - -/* - @status {Number} :: HTTP status -*/ -Subscribe.prototype.execute = function(status) { - - var self = this; - if (status > 399 && (self.route === null || self.route.name[0] === '#')) { - switch (status) { - case 400: - self.framework.stats.response.error400++; - break; - case 401: - self.framework.stats.response.error401++; - break; - case 403: - self.framework.stats.response.error403++; - break; - case 404: - self.framework.stats.response.error404++; - break; - case 408: - self.framework.stats.response.error408++; - break; - case 431: - self.framework.stats.response.error431++; - break; - case 500: - self.framework.stats.response.error500++; - break; - case 501: - self.framework.stats.response.error501++; - break; - } - } - - if (self.route === null) { - self.framework.responseContent(self.req, self.res, status || 404, utils.httpStatus(status || 404), CONTENTTYPE_TEXTPLAIN, self.framework.config['allow-gzip']); - return self; - } - - var name = self.route.name; - - self.controller = new Controller(name, self.req, self.res, self); - self.controller.exception = self.exception; - - if (!self.isCanceled && !self.isMixed && self.route.timeout > 0) - self.timeout = setTimeout(self.handlers._cancel, self.route.timeout); - - if (self.framework._length_partial_private === 0 && self.framework._length_partial_global === 0) { - self.handlers._execute(); - return self; - } - - if (self.framework._length_partial_global === 0 && self.route.partial === null) { - self.handlers._execute(); - return self; - } - - var funcs = []; - var count = 0; - - if (self.framework._length_partial_global > 0) { - for (var i = 0; i < self.framework._length_partial_global; i++) { - var partial = self.framework.routes.partialGlobal[i]; - funcs.push(partial.bind(self.controller)); - } - } - - if (self.route.partial !== null) { - var length = self.route.partial.length; - for (var i = 0; i < length; i++) { - var partialFn = self.framework.routes.partial[self.route.partial[i]]; - if (!partialFn) - continue; - count++; - funcs.push(partialFn.bind(self.controller)); - } - } - - if (count === 0 && self.framework._length_partial_global === 0) { - self.handlers._execute(); - return; - } - - funcs.async(self.handlers._execute); - return self; -}; - -/* - @flags {String Array} - @url {String} -*/ -Subscribe.prototype.prepare = function(flags, url) { - - var self = this; - - if (self.framework.onAuthorization !== null) { - self.framework.onAuthorization(self.req, self.res, flags, self.handlers._authorization); - return; - } - - if (self.route === null) - self.route = self.framework.lookup(self.req, self.req.buffer_exceeded ? '#431' : url || self.req.uri.pathname, flags); - - if (self.route === null) - self.route = self.framework.lookup(self.req, self.req.flags.indexOf('xss') === -1 ? '#404' : '#400', []); - - self.execute(self.req.buffer_exceeded ? 431 : 404); -}; - -Subscribe.prototype._execute = function() { - - var self = this; - var name = self.route.name; - self.controller.isCanceled = false; - - try { - self.framework.emit('controller', self.controller, name); - - var isModule = name[0] === '#' && name[1] === 'm'; - var o = isModule ? self.framework.modules[name.substring(8)] : self.framework.controllers[name]; - - if (o && o.request) - o.request.call(self.controller, self.controller); - - } catch (err) { - self.framework.error(err, name, self.req.uri); - } - - try { - - if (self.controller.isCanceled) - return; - - if (!self.isMixed) { - self.route.onExecute.apply(self.controller, internal.routeParam(self.route.param.length > 0 ? internal.routeSplit(self.req.uri.pathname, true) : self.req.path, self.route)); - return; - } - - self.framework._verify_directory('temp'); - - internal.parseMULTIPART_MIXED(self.req, self.header, self.framework.config['directory-temp'], function(file) { - self.route.onExecute.call(self.controller, file); - }, self.handlers._end); - - } catch (err) { - self.controller = null; - self.framework.error(err, name, self.req.uri); - self.route = self.framework.lookup(self.req, '#500', []); - self.execute(500); - } -}; - -/* - @isLogged {Boolean} -*/ -Subscribe.prototype._authorization = function(isLogged, user) { - var self = this; - - if (user) - self.req.user = user; - - self.req.flags.push(isLogged ? 'authorize' : 'unauthorize'); - self.route = self.framework.lookup(self.req, self.req.buffer_exceeded ? '#431' : self.req.uri.pathname, self.req.flags); - - if (self.route === null) - self.route = self.framework.lookup(self.req, self.req.isAuthorized ? '#404' : '#401', []); - - self.execute(self.req.buffer_exceeded ? 431 : 404); -}; - -Subscribe.prototype._end = function() { - - var self = this; - - if (self.isMixed) { - self.req.clear(true); - var headers = {}; - headers[RESPONSE_HEADER_CONTENTTYPE] = 'text/plain; charset=utf-8'; - headers[RESPONSE_HEADER_CACHECONTROL] = 'private, max-age=0'; - self.res.writeHead(200, headers); - self.res.end('END'); - self.framework._request_stats(false, false); - self.framework.emit('request-end', self.req, self.res); - return; - } - - if (self.req.buffer_exceeded) { - self.route = self.framework.lookup(self.req, '#431', []); - - if (self.route === null) { - self.framework.response431(self.req, self.res); - return; - } - - self.execute(431); - return; - } - - if (self.req.buffer_data.length === 0) { - - // POST, MULTIPART - if (self.route !== null && !self.route.isXSS && self.req.flags.indexOf('xss') !== -1) { - self.route400(); - return; - } - - self.prepare(self.req.flags, self.req.uri.pathname); - return; - } - - if (self.route.isJSON) { - try { - if (!self.req.buffer_data.isJSON()) { - self.route400(); - return; - } - - self.req.data.post = JSON.parse(self.req.buffer_data); - self.req.buffer_data = null; - self.prepare(self.req.flags, self.req.uri.pathname); - - } catch (err) { - self.route400(); - } - - return; - } - - // A route has not allowed XSS - if (!self.route.isXSS && self.framework.onXSS !== null) { - if (self.framework.onXSS(self.req.buffer_data)) { - self.req.flags.push('xss'); - self.framework.stats.request.xss++; - self.route400(); - return; - } - } - - if (self.route !== null && self.route.isRAW) { - self.req.data.post = self.req.buffer_data; - } else { - if ((self.req.headers['content-type'] || '').indexOf('x-www-form-urlencoded') === -1) { - self.route400(); - return; - } - self.req.data.post = qs.parse(self.req.buffer_data); - } - - self.prepare(self.req.flags, self.req.uri.pathname); -}; - -Subscribe.prototype.route400 = function() { - var self = this; - self.route = self.framework.lookup(self.req, '#400', []); - self.execute(400); -} - -Subscribe.prototype._endfile = function() { - - var self = this; - - if (self.req.uri.query && self.req.uri.query.length > 0) { - self.req.data = {}; - self.req.data.get = qs.parse(self.req.uri.query); - } - - for (var i = 0; i < self.framework._length_files; i++) { - var file = self.framework.routes.files[i]; - try { - - if (file.onValidation.call(self.framework, self.req, self.res, true)) { - file.onExecute.call(self.framework, self.req, self.res, false); - return; - } - - } catch (err) { - self.framework.error(err, file.controller + ' :: ' + file.name, self.req.uri); - self.framework.responseContent(self.req, self.res, 500, '500 - internal server error', CONTENTTYPE_TEXTPLAIN, self.framework.config['allow-gzip']); - return; - } - } - - self.framework.responseStatic(self.req, self.res); -}; - -Subscribe.prototype._parsepost = function(chunk) { - - var self = this; - - if (self.req.buffer_exceeded) - return; - - if (!self.req.buffer_exceeded) - self.req.buffer_data += chunk.toString(); - - if (self.req.buffer_data.length < self.route.maximumSize) - return; - - self.req.buffer_exceeded = true; - self.req.buffer_data = ''; -}; - -Subscribe.prototype._cancel = function() { - var self = this; - - self.framework.stats.response.timeout++; - clearTimeout(self.timeout); - self.timeout = null; - - if (self.controller === null) - return; - - self.controller.isTimeout = true; - self.controller.isCanceled = true; - self.route = self.framework.lookup(self.req, '#408', []); - self.execute(408); -}; - -// ********************************************************************************* -// ================================================================================= -// Framework.Controller -// ================================================================================= -// ********************************************************************************* - -/* - Controller class - @name {String} - @req {ServerRequest} - @res {ServerResponse} - @substribe {Object} - return {Controller}; -*/ -function Controller(name, req, res, subscribe) { - - this.subscribe = subscribe; - this.name = name; - this.framework = subscribe.framework; - this.req = req; - this.res = res; - this.exception = null; - - this.boundary = null; - - // controller.type === 0 - classic - // controller.type === 1 - server sent events - // controller.type === 2 - multipart/x-mixed-replace - this.type = 0; - - this.layoutName = subscribe.framework.config['default-layout']; - - this.status = 200; - - this.isLayout = false; - this.isCanceled = false; - this.isConnected = true; - this.isTimeout = false; - - this.repository = {}; - - // render output - this.output = null; - this.outputPartial = null; - this.$model = null; - this.prefix = req.prefix; - - if (typeof(this.prefix) === UNDEFINED || this.prefix.length === 0) - this.prefix = ''; - else - this.prefix = this.prefix; - - this._currentImage = ''; - this._currentDownload = ''; - this._currentVideo = ''; - this._currentJS = ''; - this._currentCSS = ''; - this._currentTemplate = ''; - this._currentView = name[0] !== '#' && name !== 'default' ? '/' + name + '/' : ''; - this._currentContent = ''; -} - -Controller.prototype = { - - get sseID() { - return this.req.headers['last-event-id'] || null; - }, - - get flags() { - return this.subscribe.route.flags; - }, - - get path() { - return this.framework.path; - }, - - get fs() { - return this.framework.fs; - }, - - get get() { - return this.req.data.get; - }, - - get post() { - return this.req.data.post; - }, - - get files() { - return this.req.data.files; - }, - - get language() { - return this.req.language; - }, - - get subdomain() { - return this.req.subdomain; - }, - - get ip() { - return this.req.ip; - }, - - get xhr() { - return this.req.xhr; - }, - - get url() { - return utils.path(this.req.uri.pathname); - }, - - get uri() { - return this.req.uri; - }, - - get cache() { - return this.framework.cache; - }, - - get config() { - return this.framework.config; - }, - - get controllers() { - return this.framework.controllers; - }, - - get isProxy() { - return this.req.isProxy; - }, - - get isDebug() { - return this.framework.config.debug; - }, - - get isTest() { - return this.req.headers['x-assertion-testing'] === '1'; - }, - - get isSecure() { - return this.req.isSecure; - }, - - get session() { - return this.req.session; - }, - - set session(value) { - this.req.session = value; - }, - - get user() { - return this.req.user; - }, - - set user(value) { - this.req.user = value; - }, - - get global() { - return this.framework.global; - }, - - set global(value) { - this.framework.global = value; - }, - - get async() { - - var self = this; - - if (typeof(self._async) === UNDEFINED) - self._async = new utils.Async(self); - - return self._async; - } -}; - -// ====================================================== -// PROTOTYPES -// ====================================================== - -/* - Validation / alias for validate - @model {Object} - @properties {String Array} - @prefix {String} :: optional - prefix in a resource - @name {String} :: optional - a resource name - return {ErrorBuilder} -*/ -Controller.prototype.validation = function(model, properties, prefix, name) { - return this.validate(model, properties, prefix, name); -}; - -Controller.prototype.clear = function() { - var self = this; - self.req.clear(); - return self; -}; - -/* - Pipe URL response - @url {String} - @headers {Object} :: optional - return {Controller} -*/ -Controller.prototype.pipe = function(url, headers, callback) { - - var self = this; - - if (typeof(headers) === FUNCTION) { - var tmp = callback; - callback = headers; - headers = tmp; - } - - if (self.res.success || !self.isConnected) - return self; - - self.framework.responsePipe(self.req, self.res, url, headers, null, function() { - self.subscribe.success(); - if (callback) - callback(); - }); - - return self; -}; - -/* - Cryptography (encrypt) - @value {String} - @key {String} - @isUniqe {Boolean} :: optional, default true - return {String} -*/ -Controller.prototype.encrypt = function() { - var framework = this.framework; - return framework.encrypt.apply(framework, arguments); -}; - -/* - Cryptography (decrypt) - @value {String} - @key {String} - @jsonConvert {Boolean} :: optional (convert string to JSON) - return {String or Object} -*/ -Controller.prototype.decrypt = function() { - var framework = this.framework; - return framework.decrypt.apply(framework, arguments); -}; - -/* - Hash value - @type {String} :: sha1, sha256, sha512, md5 - @value {Object} - @salt {String or Boolean} :: custom salt {String} or secret as salt {undefined or Boolean} - return {String} -*/ -Controller.prototype.hash = function() { - var framework = this.framework; - return framework.hash.apply(framework, arguments); -}; - -Controller.prototype.validate = function(model, properties, prefix, name) { - - var self = this; - - var resource = function(key) { - return self.resource(name || 'default', (prefix || '') + key); - }; - - var error = new builders.ErrorBuilder(resource); - return utils.validate.call(self, model, properties, self.framework.onValidation, error); -}; - -/* - Set response header - @name {String} - @value {String} - return {Controller} -*/ -Controller.prototype.header = function(name, value) { - var self = this; - self.res.setHeader(name, value); - return self; -}; - -/* - Get host name - @path {String} :: optional - return {String} -*/ -Controller.prototype.host = function(path) { - var self = this; - return self.req.hostname(path); -}; - -Controller.prototype.hostname = function(path) { - var self = this; - return self.req.hostname(path); -}; - -/* - Cross-origin resource sharing - @allow {String Array} - @method {String Array} :: optional, default null - @header {String Array} :: optional, default null - @credentials {Boolean} :: optional, default false - return {Boolean} -*/ -Controller.prototype.cors = function(allow, method, header, credentials) { - - var self = this; - var origin = self.req.headers['origin']; - var isOPTIONS = self.req.method.toUpperCase() === 'OPTIONS'; - - if (typeof(origin) === UNDEFINED) - return true; - - if (typeof(allow) === UNDEFINED) - allow = '*'; - - if (typeof(method) === BOOLEAN) { - credentials = method; - method = null; - } - - if (typeof(header) === BOOLEAN) { - credentials = header; - header = null; - } - - if (!utils.isArray(allow)) - allow = [allow]; - - var isAllowed = false; - var isAll = false; - var value; - var headers = self.req.headers; - - if (header) { - - if (!utils.isArray(header)) - header = [header]; - - for (var i = 0; i < header.length; i++) { - if (headers[header[i].toLowerCase()]) { - isAllowed = true; - break; - } - } - - if (!isAllowed) - return false; - - isAllowed = false; - } - - if (method) { - - if (!utils.isArray(method)) - method = [method]; - - var current = headers['access-control-request-method'] || self.req.method; - - for (var i = 0; i < method.length; i++) { - - value = method[i].toUpperCase(); - method[i] = value; - - if (current.indexOf(value) !== -1) - isAllowed = true; - } - - if (!isAllowed) - return false; - - isAllowed = false; - } - - for (var i = 0; i < allow.length; i++) { - - value = allow[i]; - - if (value === '*' || origin.indexOf(value) !== -1) { - isAll = value === '*'; - isAllowed = true; - break; - } - - } - - if (!isAllowed) - return false; - - var tmp; - var name; - - self.res.setHeader('Access-Control-Allow-Origin', isAll ? '*' : origin); - - if (credentials) - self.res.setHeader('Access-Control-Allow-Credentials', 'true'); - - name = 'Access-Control-Allow-Methods'; - - if (method) { - self.res.setHeader(name, method.join(', ')); - } else if (isOPTIONS) { - tmp = headers['access-control-request-method']; - if (tmp) - self.res.setHeader(name, tmp); - } - - name = 'Access-Control-Allow-Headers'; - - if (header) { - self.res.setHeader(name, header.join(', ')); - } else if (isOPTIONS) { - tmp = headers['access-control-request-headers']; - if (tmp) - self.res.setHeader(name, tmp); - } - - return true; -}; - -/* - Error - @err {Error} - return {Framework} -*/ -Controller.prototype.error = function(err) { - var self = this; - self.framework.error(typeof(err) === STRING ? new Error(err) : err, self.name, self.uri); - self.subscribe.exception = err; - self.exception = err; - return self; -}; - -/* - Problem - @message {String} - return {Framework} -*/ -Controller.prototype.problem = function(message) { - var self = this; - self.framework.problem(message, self.name, self.uri, self.ip); - return self; -}; - -/* - Change - @message {String} - return {Framework} -*/ -Controller.prototype.change = function(message) { - var self = this; - self.framework.change(message, self.name, self.uri, self.ip); - return self; -}; - -/* - Add function to async waiting list - @name {String} - @waitingFor {String} :: name of async function - @fn {Function} - return {Controller} -*/ -Controller.prototype.wait = function(name, waitingFor, fn) { - var self = this; - self.async.wait(name, waitingFor, fn); - return self; -}; - -/* - Add function to async list - @name {String} - @fn {Function} - return {Controller} -*/ -Controller.prototype.await = function(name, fn) { - var self = this; - self.async.await(name, fn); - return self; -}; - -/* - Run async functions - @callback {Function} - return {Controller} -*/ -Controller.prototype.complete = function(callback) { - var self = this; - return self.async.complete(callback); -}; - -Controller.prototype.run = function(callback) { - var self = this; - return self.async.complete(callback); -}; - -/** - * Transfer to new route - * @param {String} url Relative URL. - * @param {String Array} flags Route flags (optional). - * @return {Boolean} - */ -Controller.prototype.transfer = function(url, flags) { - - var self = this; - var length = self.framework.routes.web.length; - var path = internal.routeSplit(url.trim()); - - var isSystem = url[0] === '#'; - var noFlag = flags === null || typeof(flags) === UNDEFINED || flags.length === 0; - var selected = null; - - for (var i = 0; i < length; i++) { - - var route = self.framework.routes.web[i]; - - if (route.isASTERIX) { - if (!internal.routeCompare(path, route.url, isSystem, true)) - continue; - } else { - if (!internal.routeCompare(path, route.url, isSystem)) - continue; - } - - if (noFlag) { - selected = route; - break; - } - - if (route.flags !== null && route.flags.length > 0) { - - var result = internal.routeCompareFlags(route.flags, flags, true); - if (result === -1) - req.isAuthorized = false; - - if (result < 1) - continue; - - } else { - - if (flags.indexOf('xss') !== -1) - continue; - } - - selected = route; - break; - } - - - if (!selected) - return false; - - self.cancel(); - self.req.path = []; - self.subscribe.success(); - self.subscribe.route = selected; - self.subscribe.execute(404); - - return true; - -}; - -/* - Cancel execute controller function - Note: you can cancel controller function execute in on('controller') or controller.request(); - - return {Controller} -*/ -Controller.prototype.cancel = function() { - var self = this; - - if (typeof(self._async) !== UNDEFINED) - self._async.cancel(); - - self.isCanceled = true; - return self; -}; - -/* - Log - @arguments {Object array} - return {Controller}; -*/ -Controller.prototype.log = function() { - var self = this; - self.framework.log.apply(self.framework, arguments); - return self; -}; - -/* - META Tags for views - @arguments {String array} - return {Controller}; -*/ -Controller.prototype.meta = function() { - var self = this; - self.repository[REPOSITORY_META_TITLE] = arguments[0] || ''; - self.repository[REPOSITORY_META_DESCRIPTION] = arguments[1] || ''; - self.repository[REPOSITORY_META_KEYWORDS] = arguments[2] || ''; - self.repository[REPOSITORY_META_IMAGE] = arguments[3] || ''; - return self; -}; - -Controller.prototype.$meta = function() { - var self = this; - - if (arguments.length !== 0) { - self.meta.apply(self, arguments); - return ''; - } - - var repository = self.repository; - return self.framework.onMeta.call(self, repository[REPOSITORY_META_TITLE], repository[REPOSITORY_META_DESCRIPTION], repository[REPOSITORY_META_KEYWORDS], repository[REPOSITORY_META_IMAGE]); -}; - -/* - Set Meta Title - @value {String} - return {Controller}; -*/ -Controller.prototype.title = function(value) { - var self = this; - self.$title(value); - return self; -}; - -/* - Set Meta Description - @value {String} - return {Controller}; -*/ -Controller.prototype.description = function(value) { - var self = this; - self.$description(value); - return self; -}; - -/* - Set Meta Keywords - @value {String} - return {Controller}; -*/ -Controller.prototype.keywords = function(value) { - var self = this; - self.$keywords(value); - return self; -}; - -Controller.prototype.$title = function(value) { - var self = this; - - if (!value) - return self.repository[REPOSITORY_META_TITLE] || ''; - - self.repository[REPOSITORY_META_TITLE] = value; - return ''; -}; - -Controller.prototype.$description = function(value) { - var self = this; - - if (!value) - return self.repository[REPOSITORY_META_DESCRIPTION] || ''; - - self.repository[REPOSITORY_META_DESCRIPTION] = value; - return ''; -}; - -Controller.prototype.$keywords = function(value) { - var self = this; - - if (!value) - return self.repository[REPOSITORY_META_KEYWORDS] || ''; - - self.repository[REPOSITORY_META_KEYWORDS] = value; - return ''; -}; - -/* - Sitemap generator - @name {String} - @url {String} - @index {Number} - return {Controller}; -*/ -Controller.prototype.sitemap = function(name, url, index) { - var self = this; - - if (typeof(name) === UNDEFINED) - return self.repository.sitemap || []; - - if (typeof(url) === UNDEFINED) - url = self.req.url; - - if (typeof(self.repository.sitemap) === UNDEFINED) - self.repository.sitemap = []; - - self.repository.sitemap.push({ - name: name, - url: url, - index: index || self.repository.sitemap.length - }); - - if (typeof(index) !== UNDEFINED && self.sitemap.length > 1) { - self.repository.sitemap.sort(function(a, b) { - if (a.index < b.index) - return -1; - if (a.index > b.index) - return 1; - return 0; - }); - } - - return self; -}; - -Controller.prototype.$sitemap = function(name, url, index) { - var self = this; - self.sitemap.apply(self, arguments); - return ''; -} - -/* - Module caller - @name {String} - return {Module}; -*/ -Controller.prototype.module = function(name) { - return this.framework.module(name); -}; - -/* - Layout setter - @name {String} :: layout filename - return {Controller}; -*/ -Controller.prototype.layout = function(name) { - var self = this; - self.layoutName = name; - return self; -}; - -/* - Layout setter - @name {String} :: layout filename - return {Controller}; -*/ -Controller.prototype.$layout = function(name) { - var self = this; - self.layoutName = name; - return ''; -}; - -/* - Get a model - @name {String} :: name of controller - return {Object}; -*/ -Controller.prototype.model = function(name) { - var self = this; - return self.framework.model(name); -}; - -/* - Controller models reader - @name {String} :: name of controller - return {Object}; -*/ -Controller.prototype.models = function(name) { - var self = this; - return (self.controllers[name || self.name] || {}).models; -}; - -/** - * Send e-mail - * @param {String or Array} address E-mail address. - * @param {String} subject E-mail subject. - * @param {String} view View name. - * @param {Object} model Optional. - * @param {Function(err)} callback Optional. - * @return {Controlller} - */ -Controller.prototype.mail = function(address, subject, view, model, callback) { - - if (typeof(model) === FUNCTION) { - callback = model; - model = null; - } - - var self = this; - var body = self.view(view, model, true); - - framework.onMail(address, subject, body, callback); - - return self; -}; - -/* - Controller functions reader - @name {String} :: name of controller - return {Object}; -*/ -Controller.prototype.functions = function(name) { - var self = this; - return (self.controllers[name || self.name] || {}).functions; -}; - -/* - 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) { - var self = this; - return self.framework.notModified(self.req, self.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) { - var self = this; - self.framework.setModified(self.req, self.res, value); - return self; -}; - -/* - Set Expires header - @date {Date} - - return {Controller}; -*/ -Controller.prototype.setExpires = function(date) { - var self = this; - - if (typeof(date) === UNDEFINED) - return self; - - self.res.setHeader('Expires', date.toUTCString()); - return self; -}; - -/* - Internal function for views - @name {String} :: filename - @model {Object} - return {String} -*/ -Controller.prototype.$view = function(name, model) { - return this.$viewToggle(true, name, model); -}; - -/* - Internal function for views - @visible {Boolean} - @name {String} :: filename - @model {Object} - return {String} -*/ -Controller.prototype.$viewToggle = function(visible, name, model) { - if (!visible) - return ''; - var self = this; - var layout = self.layoutName; - self.layoutName = ''; - var value = self.view(name, model, null, true); - self.layoutName = layout; - return value; -}; - -/* - Include: Angular.js CDN into the head - @version {String} - @name {String or String Array} :: optional, example: route or resource - return {String} -*/ -Controller.prototype.$ng = function(name) { - var self = this; - - var length = arguments.length; - if (length > 1) { - for (var i = 0; i < length; i++) - self.$ng(arguments[i]); - return ''; - } - - if (name instanceof Array) { - length = name.length; - for (var i = 0; i < length; i++) - self.$ng(name[i]); - return ''; - } - - var isCommon = name[0] === '~'; - - if (isCommon) - name = name.substring(1); - - if (typeof(name) === UNDEFINED) - name = 'angular'; - - if (name === 'core' || name === '' || name === 'base' || name === 'main') - name = 'angular'; - - if (name !== 'angular' && name.indexOf('angular-') === -1) - name = 'angular-' + name; - - var output = self.repository[REPOSITORY_ANGULAR] || ''; - var script = self.$script_create((isCommon ? '/common/' + name + '.min.js' : '//cdnjs.cloudflare.com/ajax/libs/angular.js/' + self.config['angular-version'] + '/' + name + '.min.js')); - - if (name === 'angular') - output = script + output; - else - output += script; - - self.repository[REPOSITORY_ANGULAR] = output; - return ''; -}; - - -Controller.prototype.$ngCommon = function(name) { - - var self = this; - var length = arguments.length; - - if (length > 1) { - for (var i = 0; i < length; i++) - self.$ngCommon(arguments[i]); - return ''; - } - - if (name instanceof Array) { - length = name.length; - for (var i = 0; i < length; i++) - self.$ngCommon(name[i]); - return ''; - } - - var output = self.repository[REPOSITORY_ANGULAR_COMMON] || ''; - - if (name.lastIndexOf(EXTENSION_JS) === -1) - name += EXTENSION_JS; - - var script = self.$script_create('/common/' + name); - output += script; - - self.repository[REPOSITORY_ANGULAR_COMMON] = output; - return ''; -}; - -Controller.prototype.$ngLocale = function(name) { - - var self = this; - var length = arguments.length; - - if (length > 2) { - for (var i = 1; i < length; i++) - self.$ngLocale(arguments[i]); - return ''; - } - - if (name instanceof Array) { - length = name.length; - for (var i = 0; i < length; i++) - self.$ngLocale(name[i]); - return ''; - } - - var output = self.repository[REPOSITORY_ANGULAR_LOCALE] || ''; - var isLocal = name[0] === '~'; - var extension = ''; - - if (isLocal) - name = name.substring(1); - - if (name.indexOf('angular-locale_') !== -1) - name = name.replace('angular-locale_', ''); - - if (name.lastIndexOf(EXTENSION_JS) === -1) - extension = EXTENSION_JS; - - output += self.$script_create(isLocal ? '/i18n/angular-locale_' + name + extension : '//cdnjs.cloudflare.com/ajax/libs/angular-i18n/' + self.config['angular-i18n-version'] + '/angular-locale_' + name + extension); - self.repository[REPOSITORY_ANGULAR_LOCALE] = output; - - return ''; -}; - -Controller.prototype.$script_create = function(url) { - return ''; -}; - -/* - Include: Controller into the head - @name {String or String Array} - return {String} -*/ -Controller.prototype.$ngController = function(name) { - - var self = this; - - var length = arguments.length; - if (length > 1) { - for (var i = 0; i < length; i++) - self.$ngController(arguments[i]); - return ''; - } - - if (name instanceof Array) { - length = name.length; - for (var i = 0; i < length; i++) - self.$ngController(name[i]); - return ''; - } - - if (name.lastIndexOf(EXTENSION_JS) === -1) - name += EXTENSION_JS; - - var output = self.repository[REPOSITORY_ANGULAR_CONTROLLER] || ''; - var isLocal = name[0] === '~'; - - if (isLocal) - name = name.substring(1); - - output += self.$script_create('/controllers/' + name); - self.repository[REPOSITORY_ANGULAR_CONTROLLER] = output; - - return ''; -}; - -/* - Include: Content from file into the body - @name {String} - return {String} -*/ -Controller.prototype.$ngTemplate = function(name, id) { - - var self = this; - - if (typeof(id) === UNDEFINED) - id = name; - - if (name.lastIndexOf('.html') === -1) - name += '.html'; - - if (name[0] === '~') - name = name.substring(1); - else if (name[1] !== '/') - name = '/templates/' + name; - - var key = 'ng-' + name; - var tmp = self.framework.temporary.views[key]; - - if (typeof(tmp) === UNDEFINED) { - var filename = utils.combine(self.config['directory-angular'], name); - - if (fs.existsSync(filename)) - tmp = fs.readFileSync(filename).toString('utf8'); - else - tmp = ''; - - if (!self.isDebug) - self.framework.temporary.views[key] = tmp; - } - - return ''; -}; + var tmp = item[type]; -/* - Include: Directive into the head - @name {String} - return {String} -*/ -Controller.prototype.$ngDirective = function(name) { - - var self = this; - - var length = arguments.length; - if (length > 1) { - for (var i = 0; i < length; i++) - self.$ngDirective(arguments[i]); - return ''; - } + if (isFn) + item[type] = a(item[type]); + else if (type === 'name') + item[type] = item.formatName ? item[type].format(a, b, c, d, e, f) : a; + else if (type === 'url') + item[type] = item.formatUrl ? item[type].format(a, b, c, d, e, f) : a; + else + item[type] = a; - if (name instanceof Array) { - length = name.length; - for (var i = 0; i < length; i++) - self.$ngDirective(name[i]); - return ''; - } + if (type === 'name' && self.repository[REPOSITORY_META_TITLE] === tmp) + self.repository[REPOSITORY_META_TITLE] = item[type]; - if (name.lastIndexOf(EXTENSION_JS) === -1) - name += EXTENSION_JS; + return sitemap; + } - var output = self.repository[REPOSITORY_ANGULAR_OTHER] || ''; - var isLocal = name[0] === '~'; - - if (isLocal) - name = name.substring(1); - - output += self.$script_create('/directives/' + name); - self.repository[REPOSITORY_ANGULAR_OTHER] = output; - return ''; + return sitemap; }; -/* - Include: CSS into the head - @name {String} - return {String} -*/ -Controller.prototype.$ngStyle = function(name) { - - var self = this; - var length = arguments.length; - - if (length > 1) { - for (var i = 0; i < length; i++) - self.$ngStyle(arguments[i]); - return ''; - } - - if (name instanceof Array) { - length = name.length; - for (var i = 0; i < length; i++) - self.$ngStyle(name[i]); - return ''; - } - - if (name.lastIndexOf('.css') === -1) - name += '.css'; +ControllerProto.sitemap_replace = function(name, title, url) { - self.head(name); - return ''; -}; + var self = this; + var sitemap = self.repository[REPOSITORY_SITEMAP]; -/* - Include: Service into the head - @name {String} - return {String} -*/ -Controller.prototype.$ngService = function(name) { + if (!sitemap) { + sitemap = self.sitemap(self.sitemapid || name); + if (!sitemap) + return EMPTYARRAY; + } - var self = this; + if (!sitemap.$cloned) { + sitemap = U.clone(sitemap); + sitemap.$cloned = true; + self.repository[REPOSITORY_SITEMAP] = sitemap; + } - var length = arguments.length; - if (length > 1) { - for (var i = 0; i < length; i++) - self.$ngService(arguments[i]); - return ''; - } + for (var i = 0, length = sitemap.length; i < length; i++) { + var item = sitemap[i]; + if (item.id !== name) + continue; - if (name instanceof Array) { - length = name.length; - for (var i = 0; i < length; i++) - self.$ngService(name[i]); - return ''; - } + var is = self.repository[REPOSITORY_META_TITLE] === item.name; - if (name.lastIndexOf(EXTENSION_JS) === -1) - name += EXTENSION_JS; + if (title) + item.name = typeof(title) === 'function' ? title(item.name) : item.formatName ? item.name.format(title) : title; - var output = self.repository[REPOSITORY_ANGULAR_OTHER] || ''; - var isLocal = name[0] === '~'; + if (url) + item.url = typeof(url) === 'function' ? url(item.url) : item.formatUrl ? item.url.format(url) : url; - if (isLocal) - name = name.substring(1); + if (is) + self.repository[REPOSITORY_META_TITLE] = item.name; - output += self.$script_create('/services/' + name); - self.repository[REPOSITORY_ANGULAR_OTHER] = output; + return sitemap; + } - return ''; + return sitemap; }; -/* - Include: Filter into the head - @name {String} - return {String} -*/ -Controller.prototype.$ngFilter = function(name) { - - var self = this; - - var length = arguments.length; - if (length > 1) { - for (var i = 0; i < length; i++) - self.$ngFilter(arguments[i]); - return ''; - } - - if (name instanceof Array) { - length = name.length; - for (var i = 0; i < length; i++) - self.$ngFilter(name[i]); - return ''; - } - - if (name.lastIndexOf(EXTENSION_JS) === -1) - name += EXTENSION_JS; - - var output = self.repository[REPOSITORY_ANGULAR_OTHER] || ''; - var isLocal = name[0] === '~'; - - if (isLocal) - name = name.substring(1); - - output += self.$script_create('/filters/' + name); - self.repository[REPOSITORY_ANGULAR_OTHER] = output; - - return ''; +// Arguments: parent, name, url +ControllerProto.$sitemap_add = function(parent, name, url) { + this.sitemap_add(parent, name, url); + return ''; }; -/* - Include: Resource into the head - @name {String} - return {String} -*/ -Controller.prototype.$ngResource = function(name) { +// Arguments: name, type, value, format +ControllerProto.$sitemap_change = function(a, b, c, d, e, f, g, h) { + this.sitemap_change(a, b, c, d, e, f, g, h); + return ''; +}; - var self = this; +// Arguments: name, title, url +ControllerProto.$sitemap_replace =function(a, b, c) { + this.sitemap_replace(a, b, c); + return ''; +}; - var length = arguments.length; - if (length > 1) { - for (var i = 0; i < length; i++) - self.$ngResource(arguments[i]); - return ''; - } +ControllerProto.sitemap = function(name) { + var self = this; + var sitemap; - if (name instanceof Array) { - length = name.length; - for (var i = 0; i < length; i++) - self.$ngResource(name[i]); - return ''; - } + if (!name) { + sitemap = self.repository[REPOSITORY_SITEMAP]; + if (!sitemap && (self.$sitemapid || self.route.sitemap)) + return self.sitemap(self.$sitemapid || self.route.sitemap); + return sitemap ? sitemap : self.repository.sitemap || EMPTYARRAY; + } - if (name.lastIndexOf(EXTENSION_JS) === -1) - name += EXTENSION_JS; + if (name instanceof Array) { + self.repository[REPOSITORY_SITEMAP] = name; + return self; + } - var output = self.repository[REPOSITORY_ANGULAR_OTHER] || ''; - var isLocal = name[0] === '~'; + self.$sitemapid = name; + sitemap = U.clone(F.sitemap(name, false, self.language)); + sitemap.$cloned = true; - if (isLocal) - name = name.substring(1); + self.repository[REPOSITORY_SITEMAP] = sitemap; - output += self.$script_create('/resources/' + name); - self.repository[REPOSITORY_ANGULAR_OTHER] = output; + if (!self.repository[REPOSITORY_META_TITLE]) { + sitemap = sitemap[sitemap.length - 1]; + if (sitemap) + self.repository[REPOSITORY_META_TITLE] = sitemap.name; + } - return ''; + return self.repository[REPOSITORY_SITEMAP]; }; -Controller.prototype.$ngInclude = function(name) { - var self = this; - - if (name.lastIndexOf(EXTENSION_JS) === -1) - name += EXTENSION_JS; - - return self.$script_create(name); +// Arguments: name +ControllerProto.$sitemap = function(name) { + var self = this; + self.sitemap(name); + return ''; }; -/* - Internal function for views - @name {String} :: filename - return {String} -*/ -Controller.prototype.$content = function(name) { - return this.$contentToggle(true, name); +ControllerProto.module = function(name) { + return F.module(name); }; -/* - Internal function for views - @visible {Boolean} - @name {String} :: filename - return {String} -*/ -Controller.prototype.$contentToggle = function(visible, name) { - - var self = this; - - if (!visible) - return ''; - - if (name[0] !== '~') - name = self._currentContent + name; - - return internal.generateContent(self, name) || ''; +ControllerProto.layout = function(name) { + var self = this; + self.layoutName = name; + return self; }; -Controller.prototype.$url = function(host) { - var self = this; - return host ? self.req.hostname(self.url) : self.url; +ControllerProto.theme = function(name) { + var self = this; + self.themeName = name; + return self; }; -/* - Internal function for views - @name {String} :: filename - @model {Object} :: must be an array - @nameEmpty {String} :: optional filename from contents - @repository {Object} :: optional - return {Controller}; -*/ -Controller.prototype.$template = function(name, model, nameEmpty, repository) { - var self = this; - return self.$templateToggle(true, name, model, nameEmpty, repository); +/** + * Layout setter for views + * @param {String} name Layout name + * @return {String} + */ +ControllerProto.$layout = function(name) { + var self = this; + self.layoutName = name; + return ''; }; -/* - Internal function for views - @bool {Boolean} - @name {String} :: filename - @model {Object} - @nameEmpty {String} :: optional filename from contents - @repository {Object} :: optional - return {Controller}; -*/ -Controller.prototype.$templateToggle = function(visible, name, model, nameEmpty, repository) { - var self = this; - - if (!visible) - return ''; - - return self.template(name, model, nameEmpty, repository); +ControllerProto.model = function(name) { + return F.model(name); }; -/* - Internal function for views - @name {String} :: filename - @model {Object} :: must be an array - @nameEmpty {String} :: optional filename from contents - @repository {Object} :: optional - return {Controller}; -*/ -Controller.prototype.$component = function(name) { - var self = this; - return self.component.apply(self, arguments); -}; +/** + * Send e-mail + * @param {String or Array} address E-mail address. + * @param {String} subject E-mail subject. + * @param {String} view View name. + * @param {Object} model Optional. + * @param {Function(err)} callback Optional. + * @return {MailMessage} + */ +ControllerProto.mail = function(address, subject, view, model, callback) { -Controller.prototype.$helper = function(name) { - var self = this; - return self.helper.apply(self, arguments); -}; + if (typeof(model) === 'function') { + callback = model; + model = null; + } -/* - Internal function for views - @bool {Boolean} - @name {String} :: filename - @model {Object} - @nameEmpty {String} :: optional filename from contents - @repository {Object} :: optional - return {Controller}; -*/ -Controller.prototype.$componentToggle = function(visible, name) { - var self = this; + var self = this; - if (!visible) - return ''; + if (typeof(self.language) === 'string') + subject = subject.indexOf('@(') === -1 ? F.translate(self.language, subject) : F.translator(self.language, subject); - var params = []; - var length = arguments.length; + // Backup layout + var layoutName = self.layoutName; + var body = self.view(view, model, true); - for (var i = 1; i < length; i++) - params.push(arguments[i]); + var message; - return self.component.apply(self, arguments); -}; + if (body instanceof Function) { + message = F.onMail(address, subject, ''); + message.manually(); + body(function(err, body) { + message.body = body; + message.send2(callback); + }); + } else { + message = F.onMail(address, subject, body, callback); + self.layoutName = layoutName; + } -/* - Internal function for views - @name {String} - return {String} -*/ -Controller.prototype.$checked = function(bool, charBeg, charEnd) { - var self = this; - return self.$isValue(bool, charBeg, charEnd, 'checked="checked"'); + return message; }; -/* - Internal function for views - @bool {Boolean} - @charBeg {String} - @charEnd {String} - return {String} -*/ -Controller.prototype.$disabled = function(bool, charBeg, charEnd) { - var self = this; - return self.$isValue(bool, charBeg, charEnd, 'disabled="disabled"'); +ControllerProto.$template = function(name, model, expire, key) { + OBSOLETE('@{template()}', 'The method will be removed in v4'); + return this.$viewToggle(true, name, model, expire, key); }; -/* - Internal function for views - @bool {Boolean} - @charBeg {String} - @charEnd {String} - return {String} -*/ -Controller.prototype.$selected = function(bool, charBeg, charEnd) { - var self = this; - return self.$isValue(bool, charBeg, charEnd, 'selected="selected"'); +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); }; -/** - * Fake function for assign value - * @private - * @param {Object} value Value to eval. - * return {String} Returns empty string. - */ -Controller.prototype.$set = function(value) { - return ''; -}; +ControllerProto.$view = function(name, model, expire, key) { -/* - Internal function for views - @bool {Boolean} - @charBeg {String} - @charEnd {String} - return {String} -*/ -Controller.prototype.$readonly = function(bool, charBeg, charEnd) { - var self = this; - return self.$isValue(bool, charBeg, charEnd, 'readonly="readonly"'); -}; + var self = this; + var cache; -/* - Internal function for views - @name {String} - @value {String} - return {String} -*/ -Controller.prototype.$header = function(name, value) { - this.header(name, value); - return ''; -}; + if (expire) { + cache = '$view.' + name + '.' + (key || ''); + var output = F.cache.read2(cache); + if (output) + return output.body; + } -/* - Internal function for views - @model {Object} - @name {String} - @attr {Object} :: optional - return {String} -*/ -Controller.prototype.$text = function(model, name, attr) { - return this.$input(model, 'text', name, attr); -}; + var value = self.view(name, model, null, true, true, cache); + if (!value) + return ''; -/* - Internal function for views - @model {Object} - @name {String} :: optional - @attr {Object} :: optional - return {String} -*/ -Controller.prototype.$password = function(model, name, attr) { - return this.$input(model, 'password', name, attr); + expire && F.cache.add(cache, { components: value instanceof Function, body: value instanceof Function ? '' : value }, expire, false); + return value; }; -/* - Internal function for views - @model {Object} - @name {String} - @attr {Object} :: optional - return {String} -*/ -Controller.prototype.$hidden = function(model, name, attr) { - return this.$input(model, 'hidden', name, attr); +ControllerProto.$viewCompile = function(body, model, key) { + OBSOLETE('@{viewCompile()}', 'Was renamed to @{view_compile()}.'); + return this.$view_compile(body, model, key); }; -/* - Internal function for views - @model {Object} - @name {String} - @attr {Object} :: optional - return {String} -*/ -Controller.prototype.$radio = function(model, name, value, attr) { - - if (typeof(attr) === STRING) - attr = { - label: attr - }; - - attr.value = value; - return this.$input(model, 'radio', name, attr); +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 || ''; }; -/* - Internal function for views - @model {Object} - @name {String} - @attr {Object} :: optional - return {String} -*/ -Controller.prototype.$checkbox = function(model, name, attr) { - - if (typeof(attr) === STRING) - attr = { - label: attr - }; - - return this.$input(model, 'checkbox', name, attr); +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) : ''; }; -/* - Internal function for views - @model {Object} - @name {String} - @attr {Object} :: optional - return {String} -*/ -Controller.prototype.$textarea = function(model, name, attr) { - - var builder = ''; + break; + case 'css': + val = ''; + break; + } - if (typeof(model) === UNDEFINED) - return builder + '>'; + output += val; + } - var value = (model[name] || attr.value) || ''; - return builder + '>' + value.toString().encode() + ''; + this.repository[key] = (this.repository[key] || '') + output; + return this; }; -/* - Internal function for views - @model {Object} - @type {String} - @name {String} - @attr {Object} :: optional - return {String} -*/ -Controller.prototype.$input = function(model, type, name, attr) { +/** + * Adds a content into the section + * @param {String} name A section name. + * @param {String} value A content. + * @param {Boolean} replace Optional, default `false` otherwise concats contents. + * @return {String/Controller} String is returned when the method contains only `name` argument + */ +ControllerProto.section = function(name, value, replace) { - var builder = ['' + builder + ' ' + attr.label + ''; + obj[key] = '\0'; - return builder; -}; + 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; + } -/* - Internal function for views - @arguments {String} - return {String} -*/ -Controller.prototype.$dns = function(value) { + str = str.replace('\0', querystring_encode(value, self.query[key], key)); - var builder = ''; - var self = this; - var length = arguments.length; + for (var i = 2; i < arguments.length; i++) { + var beg = str.indexOf(arguments[i] + '='); + if (beg === -1) + continue; + var end = str.indexOf('&', beg); + str = str.substring(0, beg) + str.substring(end === -1 ? str.length : end + 1); + } - for (var i = 0; i < length; i++) - builder += ''; + return str ? '?' + str : ''; + } - self.head(builder); - return ''; -}; + if (value) { + obj = U.copy(self.query); + U.extend(obj, value); + } -/* - Internal function for views - @arguments {String} - return {String} -*/ -Controller.prototype.$prefetch = function() { + if (value != null) + obj[key] = value; - var builder = ''; - var self = this; - var length = arguments.length; + obj = Qs.stringify(obj); - for (var i = 0; i < length; i++) - builder += ''; + if (value === undefined && type === 'string') + obj += (obj ? '&' : '') + key; - self.head(builder); - return ''; + return self.url + (obj ? '?' + obj : ''); }; -/* - Internal function for views - @arguments {String} - return {String} -*/ -Controller.prototype.$prerender = function(value) { - - var builder = ''; - var self = this; - var length = arguments.length; - - for (var i = 0; i < length; i++) - builder += ''; - - self.head(builder); - return ''; +ControllerProto.$checked = function(bool, charBeg, charEnd) { + return this.$isValue(bool, charBeg, charEnd, 'checked="checked"'); }; -/* - Internal function for views - @value {String} - return {String} -*/ -Controller.prototype.$next = function(value) { - var self = this; - self.head(''); - return ''; +ControllerProto.$disabled = function(bool, charBeg, charEnd) { + return this.$isValue(bool, charBeg, charEnd, 'disabled="disabled"'); }; -/* - Internal function for views - @arguments {String} - return {String} -*/ -Controller.prototype.$prev = function(value) { - var self = this; - self.head(''); - return ''; +ControllerProto.$selected = function(bool, charBeg, charEnd) { + return this.$isValue(bool, charBeg, charEnd, 'selected="selected"'); }; -/* - Internal function for views - @arguments {String} - return {String} -*/ -Controller.prototype.$canonical = function(value) { - var self = this; - self.head(''); - return ''; +/** + * Fake function for assign value + * @private + * @param {Object} value Value to eval. + * return {String} Returns empty string. + */ +// Argument: value +ControllerProto.$set = function() { + return ''; }; -Controller.prototype._prepareHost = function(value) { - var tmp = value.substring(0, 5); - - if (tmp !== 'http:' && tmp !== 'https://') { - if (tmp[0] !== '/' || tmp[1] !== '/') - value = this.host(value); - } - - return value; +ControllerProto.$readonly = function(bool, charBeg, charEnd) { + return this.$isValue(bool, charBeg, charEnd, 'readonly="readonly"'); }; -/* - Internal function for views - @arguments {String} - return {String} -*/ -Controller.prototype.head = function() { - - var self = this; - - var length = arguments.length; - var header = (self.repository[REPOSITORY_HEAD] || ''); - - if (length === 0) { - var angularBeg = (self.repository[REPOSITORY_ANGULAR] || '') + (self.repository[REPOSITORY_ANGULAR_COMMON] || '') + (self.repository[REPOSITORY_ANGULAR_LOCALE] || ''); - var angularEnd = (angularBeg.length > 0 ? self.$script_create('/app.js') : '') + (self.repository[REPOSITORY_ANGULAR_OTHER] || '') + (self.repository[REPOSITORY_ANGULAR_CONTROLLER] || ''); - return (self.config.author && self.config.author.length > 0 ? '' : '') + angularBeg + header + angularEnd; - } - - var output = ''; - for (var i = 0; i < length; i++) { - - var val = arguments[i]; - - if (header.length > 0 && header.indexOf(val) !== -1) - continue; - - if (val.indexOf('<') !== -1) { - output += val; - continue; - } - - var tmp = val.substring(0, 7); - var isRoute = (tmp[0] !== '/' && tmp[1] !== '/') && tmp !== 'http://' && tmp !== 'https:/'; - - if (val.lastIndexOf(EXTENSION_JS) !== -1) - output += ''; - else if (val.lastIndexOf('.css') !== -1) - output += ''; - } - - header += output; - self.repository[REPOSITORY_HEAD] = header; - return self; +ControllerProto.$header = function(name, value) { + this.header(name, value); + return ''; }; -Controller.prototype.$head = function() { - var self = this; - self.head.apply(self, arguments); - return ''; +ControllerProto.$text = function(model, name, attr) { + return this.$input(model, 'text', name, attr); }; -/* - Internal function for views - @arguments {String} - return {Controller} -*/ -Controller.prototype.place = function(name) { - - var self = this; - - var key = REPOSITORY_PLACE + '_' + name; - var length = arguments.length; - - if (length === 1) - return self.repository[key] || ''; - - var output = ''; - for (var i = 1; i < length; i++) { - - var val = arguments[i]; - - if (val.indexOf('<') !== -1) { - output += val; - continue; - } - - if (val.lastIndexOf(EXTENSION_JS) === -1) { - output += val; - continue; - } - - var tmp = val.substring(0, 7); - var isRoute = (tmp[0] !== '/' && tmp[1] !== '/') && tmp !== 'http://' && tmp !== 'https:/'; - output += ''; - } - - self.repository[key] = (self.repository[key] || '') + output; - return self; +ControllerProto.$password = function(model, name, attr) { + return this.$input(model, 'password', name, attr); }; -Controller.prototype.$place = function() { - var self = this; - if (arguments.length === 1) - return self.place.apply(self, arguments); - self.place.apply(self, arguments); - return ''; +ControllerProto.$hidden = function(model, name, attr) { + return this.$input(model, 'hidden', name, attr); }; -/* - Internal function for views - @bool {Boolean} - @charBeg {String} - @charEnd {String} - @value {String} - return {String} -*/ -Controller.prototype.$isValue = function(bool, charBeg, charEnd, value) { - if (!bool) - return ''; +ControllerProto.$radio = function(model, name, value, attr) { - charBeg = charBeg || ' '; - charEnd = charEnd || ''; + if (typeof(attr) === 'string') { + var label = attr; + attr = SINGLETON('!$radio'); + attr.label = label; + } - return charBeg + value + charEnd; + attr.value = value; + return this.$input(model, 'radio', name, attr); }; -/* - Internal function for views - @date {String or Date or Number} :: if {String} date format must has YYYY-MM-DD HH:MM:SS, {Number} represent Ticks (.getTime()) - return {String} :: empty string -*/ -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 = utils.parseInt(date[0] || ''); - var month = utils.parseInt(date[1] || '') - 1; - var day = utils.parseInt(date[2] || '') - 1; - - if (month < 0) - month = 0; +ControllerProto.$checkbox = function(model, name, attr) { - if (day < 0) - day = 0; + if (typeof(attr) === 'string') { + var label = attr; + attr = SINGLETON('!$checkbox'); + attr.label = label; + } - var hour = utils.parseInt(time[0] || ''); - var minute = utils.parseInt(time[1] || ''); - var second = utils.parseInt(time[2] || ''); - - date = new Date(year, month, day, hour, minute, second, 0); - } else if (utils.isDate(value)) - date = value; - - if (typeof(date) === UNDEFINED) - return ''; - - self.setModified(date); - return ''; + return this.$input(model, 'checkbox', name, attr); }; -/* - Internal function for views - @value {String} - return {String} :: empty string -*/ -Controller.prototype.$etag = function(value) { - this.setModified(value); - return ''; -}; +ControllerProto.$textarea = function(model, name, attr) { -/* - Internal function for views - @arr {Array} :: array of object or plain value array - @selected {Object} :: value for selecting item - @name {String} :: name of name property, default: name - @value {String} :: name of value property, default: value - return {String} -*/ -Controller.prototype.$options = function(arr, selected, name, value) { + var builder = '' + text.toString().encode() + ''; - } + if (value === undefined) + builder += ' value="' + (attr.value || '').toString().encode() + ATTR_END; + else + builder += ' value="' + (value || '').toString().encode() + ATTR_END; - return options; -}; + builder += ' />'; + return attr.label ? ('') : builder; +}; + +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; +}; -/* - Append '; + } + + self.repository[REPOSITORY_HEAD] = header; + return self; }; -/* - Append TAG - @name {String} :: filename - @width {Number} :: optional - @height {Number} :: optional - @alt {String} :: optional - @className {String} :: optional - return {String} -*/ -Controller.prototype.$image = function(name, width, height, alt, className) { - - var style = ''; - - if (typeof(width) === OBJECT) { - height = width.height; - alt = width.alt; - className = width.class; - style = width.style; - width = width.width; - } - - var builder = ' 0) - builder += ' height="' + height + ATTR_END; - - if (alt) - builder += ' alt="' + alt.encode() + ATTR_END; - - if (className) - builder += ' class="' + className + ATTR_END; - - if (style) - builder += ' style="' + style + ATTR_END; - - return builder + ' border="0" />'; +ControllerProto.$head = function() { + this.head.apply(this, arguments); + return ''; }; -/* - Append TAG - @filename {String} - @innerHTML {String} - @downloadName {String} - @className {String} :: optional - return {String} -*/ -Controller.prototype.$download = function(filename, innerHTML, downloadName, className) { - var builder = '' + (innerHTML || filename) + ''; +ControllerProto.$isValue = function(bool, charBeg, charEnd, value) { + if (!bool) + return ''; + charBeg = charBeg || ' '; + charEnd = charEnd || ''; + return charBeg + value + charEnd; }; -Controller.prototype.$json = function(obj, name, beautify) { +ControllerProto.$options = function(arr, selected, name, value, disabled) { - if (typeof(name) === BOOLEAN) { - var tmp = name; - name = beautify; - beautify = name; - } + var type = typeof(arr); + if (!arr) + return ''; - var value = beautify ? JSON.stringify(obj, null, 4) : JSON.stringify(obj); + var isObject = false; + var tmp = null; - if (!name) - return value; + if (!(arr instanceof Array) && type === 'object') { + isObject = true; + tmp = arr; + arr = Object.keys(arr); + } - return ''; -}; - -/* - Append favicon TAG - @name {String} :: filename - return {String} -*/ -Controller.prototype.$favicon = function(name) { - var self = this; - var contentType = 'image/x-icon'; + if (!(arr instanceof Array)) + arr = [arr]; - if (typeof(name) === UNDEFINED) - name = 'favicon.ico'; + selected = selected || ''; - if (name.lastIndexOf('.png') !== -1) - contentType = 'image/png'; - - if (name.lastIndexOf('.gif') !== -1) - contentType = 'image/gif'; - - name = self.framework.routeStatic('/' + name); - - return ''; -}; + var options = ''; -Controller.prototype._routeHelper = function(current, name, fn) { + if (!isObject) { + if (value == null) + value = value || name || 'value'; + if (name == null) + name = 'name'; + if (disabled == null) + disabled = 'disabled'; + } - var self = this; + var isSelected = false; + var length = 0; - if (current.length === 0) - return fn.call(self.framework, name); + length = arr.length; - if (current.substring(0, 2) === '//' || current.substring(0, 6) === 'http:/' || current.substring(0, 7) === 'https:/') - return fn.call(self.framework, current + name); + for (var i = 0; i < length; i++) { - if (current[0] === '~') - return fn.call(self.framework, utils.path(current.substring(1)) + name); + var o = arr[i]; + var type = typeof(o); + var text = ''; + var val = ''; + var sel = false; + var dis = false; - return fn.call(self.framework, utils.path(current) + name); -}; + if (isObject) { + if (name === true) { + val = tmp[o]; + text = o; + if (!value) + value = ''; + } else { + val = o; + text = tmp[o]; + if (!text) + text = ''; + } -/* - Static file routing - @name {String} :: filename - @tag {Boolean} :: optional, append tag? default: false - return {String} -*/ -Controller.prototype.routeJS = function(name, tag) { - var self = this; + } else if (type === 'object') { - if (typeof(name) === UNDEFINED) - name = 'default.js'; + text = (o[name] || ''); + val = (o[value] || ''); - var url = self._routeHelper(self._currentJS, name, self.framework.routeJS); - return tag ? '' : url; -}; + if (typeof(text) === 'function') + text = text(i); -/* - Static file routing - @name {String} :: filename - @tag {Boolean} :: optional, append tag? default: false - return {String} -*/ -Controller.prototype.routeCSS = function(name, tag) { - var self = this; + if (typeof(val) === 'function') + val = val(i, text); - if (typeof(name) === UNDEFINED) - name = 'default.css'; + dis = o[disabled]; - var url = self._routeHelper(self._currentCSS, name, self.framework.routeCSS); - return tag ? '' : url; -}; + if (typeof(disabled) === 'function') + dis = disabled(i, val, text); + else + dis = dis ? true : false; -/* - Static file routing - @name {String} :: filename - return {String} -*/ -Controller.prototype.routeImage = function(name) { - var self = this; - return self._routeHelper(self._currentImage, name, self.framework.routeImage); -}; + } else { + text = o; + val = o; + } -/* - Static file routing - @name {String} :: filename - return {String} -*/ -Controller.prototype.routeVideo = function(name) { - var self = this; - return self._routeHelper(self._currentVideo, name, self.framework.routeVideo); -}; + if (!isSelected) { + sel = val == selected; + isSelected = sel; + } -/* - Static file routing - @name {String} :: filename - return {String} -*/ -Controller.prototype.routeFont = function(name) { - var self = this; - return self.framework.routeFont(name); -}; + options += ''; + } -/* - Static file routing - @name {String} :: filename - return {String} -*/ -Controller.prototype.routeDownload = function(name) { - var self = this; - return self._routeHelper(self._currentDownload, name, self.framework.routeDownload); + return options; }; -/* - Static file routing - @name {String} :: filename - return {String} -*/ -Controller.prototype.routeStatic = function(name) { - var self = this; - return self.framework.routeStatic(name); +/** + * Append '; + } + 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] === '#') + ext = '.js'; + + switch (ext) { + case '.js': + builder += F.temporary.other[k] = self.public_js(filename, tag); + break; + case '.css': + builder += F.temporary.other[k] = self.public_css(filename, tag); + break; + case '.ico': + builder += F.temporary.other[k] = self.$favicon(filename); + break; + case '.jpg': + case '.gif': + case '.svg': + case '.png': + case '.jpeg': + case '.heif': + case '.webp': + case '.heic': + case '.apng': + builder += F.temporary.other[k] = self.public_image(filename); + break; + case '.mp4': + case '.avi': + case '.ogv': + case '.webm': + case '.mov': + case '.mpg': + case '.mpe': + case '.mpeg': + case '.m4v': + builder += F.temporary.other[k] = self.public_video(filename); + break; + default: + builder += F.temporary.other[k] = self.public(filename); + break; + } + } + + return builder; }; -/* - Internal - @path {String} :: add path to route path - return {String} -*/ -Controller.prototype.$currentDownload = function(path) { - this._currentDownload = path && path.length > 0 ? path : ''; - return ''; -}; +/** + * Append TAG + * @private + * @return {String} + */ +ControllerProto.$css = function() { -/* - Set current image path - @path {String} - return {Controller} -*/ -Controller.prototype.currentImage = function(path) { - var self = this; - self.$currentImage(path); - self._defaultImage = self._currentImage; - return self; -}; + var self = this; + var builder = ''; -/* - Set current download path - @path {String} - return {Controller} -*/ -Controller.prototype.currentDownload = function(path) { - var self = this; - self.$currentDownload(path); - self._defaultDownload = self._currentDownload; - return self; -}; + for (var i = 0; i < arguments.length; i++) + builder += self.public_css(arguments[i], true); -/* - Set current CSS path - @path {String} - return {Controller} -*/ -Controller.prototype.currentCSS = function(path) { - var self = this; - self.$currentCSS(path); - self._defaultCSS = self._currentCSS; - return self; + return builder; }; -/* - Set current JS path - @path {String} - return {Controller} -*/ -Controller.prototype.currentJS = function(path) { - var self = this; - self.$currentJS(path); - self._defaultJS = self._currentJS; - return self; -}; +ControllerProto.$image = function(name, width, height, alt, className) { -/* - Set current video path - @path {String} - return {Controller} -*/ -Controller.prototype.currentVideo = function(path) { - var self = this; - self.$currentVideo(path); - self._defaultVideo = self._currentVideo; - return self; -}; + var style = ''; -/* - Resource reader - @name {String} :: filename - @key {String} - return {String} -*/ -Controller.prototype.resource = function(name, key) { - var self = this; - return self.framework.resource(name, key); -}; + if (typeof(width) === 'object') { + height = width.height; + alt = width.alt; + className = width.class; + style = width.style; + width = width.width; + } -/* - Render template to string - @name {String} :: filename - @model {Object} - @nameEmpty {String} :: filename for empty Contents - @repository {Object} - @cb {Function} :: callback(string) - return {String} -*/ -Controller.prototype.template = function(name, model, nameEmpty, repository) { + var builder = ' 0) + builder += ' height="' + height + ATTR_END; - if (typeof(model) === UNDEFINED || model === null || model.length === 0) { + if (alt) + builder += ' alt="' + alt.encode() + ATTR_END; - if (typeof(nameEmpty) !== UNDEFINED && nameEmpty.length > 0) - return self.$content(nameEmpty); + if (className) + builder += ' class="' + className + ATTR_END; - return ''; - } + if (style) + builder += ' style="' + style + ATTR_END; - if (typeof(repository) === UNDEFINED) - repository = self.repository; + return builder + ' border="0" />'; +}; + +/** + * Create URL: DOWNLOAD (' + (innerHTML || filename) + ''; }; -/* - Render component to string - @name {String} - return {String} -*/ -Controller.prototype.component = function(name) { - var self = this; - var component = framework.component(name); +/** + * Serialize object into the JSON + * @private + * @param {Object} obj + * @param {String} id Optional. + * @param {Boolean} beautify Optional. + * @return {String} + */ +ControllerProto.$json = function(obj, id, beautify, replacer) { - if (component === null) - return ''; + if (typeof(id) === 'boolean') { + replacer = beautify; + beautify = id; + id = null; + } - var length = arguments.length; - var params = []; + if (typeof(beautify) === 'function') { + replacer = beautify; + beautify = false; + } - for (var i = 1; i < length; i++) - params.push(arguments[i]); + if (obj && obj.$$schema) + obj = obj.$clean(); - var output = component.render.apply(self, params); - return output; + var value = beautify ? JSON.stringify(obj, replacer, 4) : JSON.stringify(obj, replacer); + return id ? ('') : value; }; -/* - Render component to string - @name {String} - return {String} -*/ -Controller.prototype.helper = function(name) { - var self = this; - var helper = framework.helpers[name] || null; +/** + * 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 (helper === null) - return ''; + if (obj && obj.$$schema) + obj = obj.$clean(); - var length = arguments.length; - var params = []; + var data = {}; - for (var i = 1; i < length; i++) - params.push(arguments[i]); + for (var i = 2; i < arguments.length; i++) { + var key = arguments[i]; + data[key] = obj[key]; + } - return helper.apply(self, params); + return ''; }; -/* - Response JSON - @obj {Object} - @headers {Object} :: optional - return {Controller}; -*/ -Controller.prototype.json = function(obj, headers, beautify) { - var self = this; +/** + * Append FAVICON tag + * @private + * @param {String} name + * @return {String} + */ +ControllerProto.$favicon = function(name) { - if (self.res.success || !self.isConnected) - return self; + var contentType = 'image/x-icon'; - if (typeof(headers) === BOOLEAN) { - var tmp = headers; - beautify = headers; - headers = tmp; - } + if (!name) + name = 'favicon.ico'; - if (obj instanceof builders.ErrorBuilder) - obj = obj.json(beautify); - else { - if (beautify) - obj = JSON.stringify(obj || {}, null, 4); - else - obj = JSON.stringify(obj || {}); - } + var key = 'favicon#' + name; + if (F.temporary.other[key]) + return F.temporary.other[key]; - self.subscribe.success(); - self.framework.responseContent(self.req, self.res, self.status, obj, 'application/json', self.config['allow-gzip'], headers); - self.framework.stats.response.json++; + if (name.lastIndexOf('.png') !== -1) + contentType = 'image/png'; + else if (name.lastIndexOf('.gif') !== -1) + contentType = 'image/gif'; - if (self.precache) - self.precache(obj, 'application/json', headers); + return F.temporary.other[key] = ''; +}; + +/** + * Route static file helper + * @private + * @param {String} current + * @param {String} name + * @param {Function} fn + * @return {String} + */ +ControllerProto.$static = function(name, fn) { + return fn.call(framework, prepare_staticurl(name, false), this.themeName); +}; - return self; +ControllerProto.routeScript = function(name, tag, path) { + OBSOLETE('controller.routeScript()', 'Was renamed to "controller.public_js()"'); + return this.public_js(name, tag, path); }; -/* - Set custom response - return {Boolean} -*/ -Controller.prototype.custom = function() { +ControllerProto.public_js = function(name, tag, path) { - var self = this; - if (self.res.success || !self.isConnected) - return false; + if (name === undefined) + name = 'default.js'; - self.subscribe.success(); - self.res.success = true; - self.framework.stats.response.custom++; - self.framework._request_stats(false, false); - self.framework.emit('request-end', self.req, self.res); + var async = false; + var url; - return true; + // Checks "async " + if (tag && name[0] === 'a' && name[5] === ' ') { + async = true; + name = name.substring(6); + } + // Isomorphic + if (name[0] === '#') { + var tmp = F.isomorphic[name.substring(1)]; + if (tmp) + url = tmp.url; + else { + F.error('Isomorphic library {0} doesn\'t exist.'.format(name.substring(1))); + return ''; + } + } else { + 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); + } + + return tag ? ('') : url; }; -/* - Manul clear request data - @enable {Boolean} :: enable manual clear - controller.clear() - return {Controller} -*/ -Controller.prototype.noClear = function(enable) { - var self = this; - self.req._manual = typeof(enable) === UNDEFINED ? true : enable; - return self; +ControllerProto.routeStyle = function(name, tag, path) { + OBSOLETE('controller.routeStyle()', 'Was renamed to "controller.public_css()"'); + return this.public_css(name, tag, path); }; -/* - Response JSON ASYNC - @obj {Object} - @headers {Object} :: optional - return {Controller}; -*/ -Controller.prototype.jsonAsync = function(obj, headers, beautify) { - var self = this; +ControllerProto.public_css = function(name, tag, path) { + + var self = this; - var fn = function() { - self.json(obj, headers, beautify); - }; + if (name === undefined) + name = 'default.css'; - self.async.complete(fn); - return self; + 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; }; -/* - !!! pell-mell - Response custom content or Return content from Contents - @contentBody {String} - @contentType {String} :: optional - @headers {Object} :: optional - return {Controller or String}; :: return String when contentType is undefined -*/ -Controller.prototype.content = function(contentBody, contentType, headers) { +ControllerProto.routeImage = function(name) { + OBSOLETE('controller.routeImage()', 'Was renamed to "controller.public_image()"'); + return this.public_image(name); +}; - var self = this; - var type = typeof(contentType); +ControllerProto.public_image = function(name) { + return this.$static(name, F.public_image); +}; - if (type === UNDEFINED) { - self.content(self.$contentToggle(true, contentBody), CONTENTTYPE_TEXTHTML, headers); - return; - } +ControllerProto.routeVideo = function(name) { + OBSOLETE('controller.routeVideo()', 'Was renamed to "controller.public_video()"'); + return this.public_video(name); +}; - if (type === BOOLEAN) - return self.$contentToggle(true, contentBody); +ControllerProto.public_video = function(name) { + return this.$static(name, F.public_video); +}; - if (self.res.success || !self.isConnected) - return self; +ControllerProto.routeFont = function(name) { + OBSOLETE('controller.routeFont()', 'Was renamed to "controller.public_font()"'); + return this.public_font(name); +}; - self.subscribe.success(); - self.framework.responseContent(self.req, self.res, self.status, contentBody, contentType || CONTENTTYPE_TEXTPLAIN, self.config['allow-gzip'], headers); - return self; +ControllerProto.public_font = function(name) { + return F.public_font(name); }; -/* - Response plain text - @contentBody {String} - @headers {Object} :: optional - return {Controller}; -*/ -Controller.prototype.plain = function(contentBody, headers) { - var self = this; +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); +}; - if (self.res.success || !self.isConnected) - return self; +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; +}; - var type = typeof(contentBody); +/** + * Creates a string from the view + * @param {String} name A view name without `.html` extension. + * @param {Object} model A model, optional. + * @return {String} + */ +ControllerProto.template = function(name, model) { + OBSOLETE('controller.template()', 'This method will be removed in v4.'); + return this.view(name, model, true); +}; - if (type === UNDEFINED) - contentBody = ''; - else if (type === OBJECT) - contentBody = contentBody === null ? '' : JSON.stringify(contentBody, null, 4); - else - contentBody = contentBody === null ? '' : contentBody.toString(); +/** + * Renders a custom helper to a string + * @param {String} name A helper name. + * @return {String} + */ +ControllerProto.helper = function(name) { + var helper = F.helpers[name]; + if (!helper) + return ''; - self.subscribe.success(); - self.framework.responseContent(self.req, self.res, self.status, contentBody, CONTENTTYPE_TEXTPLAIN, self.config['allow-gzip'], headers); - self.framework.stats.response.plain++; + var params = []; + for (var i = 1; i < arguments.length; i++) + params.push(arguments[i]); - if (self.precache) - self.precache(contentBody, CONTENTTYPE_TEXTPLAIN, headers); + return helper.apply(this, params); +}; - return self; +/** + * Response JSON + * @param {Object} obj + * @param {Object} headers Custom headers, optional. + * @param {Boolean} beautify Beautify JSON. + * @param {Function(key, value)} replacer JSON replacer. + * @return {Controller} + */ +ControllerProto.json = function(obj, headers, beautify, replacer) { + + var self = this; + var res = self.res; + + if (typeof(headers) === 'boolean') { + replacer = beautify; + beautify = headers; + } + + res.options.code = self.status || 200; + res.options.type = CT_JSON; + res.options.headers = headers; + + // Checks the HEAD method + if (self.req.method === 'HEAD') { + res.options.body = EMPTYBUFFER; + res.options.type = CT_JSON; + res.$text(); + F.stats.response.json++; + 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); + + var json = obj.output(true); + + if (obj.contentType) + res.options.type = obj.contentType; + else + res.options.type = CT_JSON; + + if (obj.status !== 200) + res.options.code = obj.status; + + obj = json; + F.stats.response.errorBuilder++; + } else { + + if (obj && obj.$$schema) + obj = obj.$clean(); + + if (beautify) + obj = JSON.stringify(obj, replacer, 4); + else + obj = JSON.stringify(obj, 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; +}; + +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(); + } + }; }; -/* - Response empty content - @headers {Object} :: optional - return {Controller}; -*/ -Controller.prototype.empty = function(headers) { - var self = this; +/** + * Responds with JSONP + * @param {String} name A method name. + * @param {Object} obj Object to serialize. + * @param {Object} headers A custom headers. + * @param {Boolean} beautify Should be the JSON prettified? Optional, default `false` + * @param {Function} replacer Optional, the JSON replacer. + * @return {Controller} + */ +ControllerProto.jsonp = function(name, obj, headers, beautify, replacer) { - if (self.res.success || !self.isConnected) - return self; + var self = this; + var res = self.res; - var code = 204; + if (typeof(headers) === 'boolean') { + replacer = beautify; + beautify = headers; + } - if (typeof(headers) === NUMBER) { - code = headers; - headers = null; - } + res.options.code = self.status || 200; + res.options.headers = headers; + res.options.type = 'application/x-javascript'; - self.subscribe.success(); - self.framework.responseContent(self.req, self.res, code, '', CONTENTTYPE_TEXTPLAIN, false, headers); - self.framework.stats.response.empty++; + // Checks the HEAD method + if (self.req.method === 'HEAD') { + res.options.body = EMPTYBUFFER; + res.$text(); + F.stats.response.json++; + return self; + } - return self; -}; + !name && (name = 'callback'); -Controller.prototype.destroy = function() { - var self = this; + if (obj instanceof framework_builders.ErrorBuilder) { + self.req.$language && !obj.isResourceCustom && obj.setResource(self.req.$language); + obj = obj.json(beautify); + if (obj.status !== 200) + res.options.code = obj.status; + F.stats.response.errorBuilder++; + } else { - if (self.res.success || !self.isConnected) - return self; + if (obj && obj.$$schema) + obj = obj.$clean(); - self.subscribe.success(); - self.req.connection.destroy(); - self.framework.stats.response.destroy++; + if (beautify) + obj = JSON.stringify(obj, replacer, 4); + else + obj = JSON.stringify(obj, replacer); + } - return self; -}; + res.options.body = name + '(' + obj + ')'; + res.$text(); -/* - Response a file - @filename {String} - @downloadName {String} :: optional - @headers {Object} :: optional - return {Controller}; -*/ -Controller.prototype.file = function(filename, downloadName, headers) { - var self = this; + F.stats.response.json++; + self.precache && self.precache(name + '(' + obj + ')', res.options.type, headers); + return self; +}; - if (self.res.success || !self.isConnected) - return self; +/** + * Creates View or JSON callback + * @param {String} view Optional, undefined or null returns JSON. + * @return {Function} + */ +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 && (err && !err.stack) && !is) { + data = err; + err = null; + } + + if (err) { + if (is && !view) { + self.req.$language && !err.isResourceCustom && err.setResource(self.req.$language); + return self.content(err); + } + 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 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); + }; +}; + +ControllerProto.custom = function() { + if (this.res.success) + return false; + this.res.$custom(); + return true; +}; - if (filename[0] === '~') - filename = '.' + filename.substring(1); - else - filename = utils.combine(self.framework.config['directory-public'], filename); +/** + * Prevents cleaning uploaded files (need to call `controller.clear()` manually). + * @param {Boolean} enable Optional, default `true`. + * @return {Controller} + */ +ControllerProto.noClear = function(enable) { + OBSOLETE('controller.noClear()', 'You need to use controller.autoclear(false)'); + this.req._manual = enable === undefined ? true : enable; + return this; +}; - self.subscribe.success(); - self.framework.responseFile(self.req, self.res, filename, downloadName, headers); +ControllerProto.autoclear = function(enable) { + this.req._manual = enable === false; + return this; +}; - return self; +ControllerProto.html = function(body, headers) { + return this.content(body, 'text/html', headers); }; -/* - Response an image - @filename {String or Stream} - @fnProcess {Function} :: function(FrameworkImage) {} - @headers {Object} :: optional, additional headers - @useImageMagick {Boolean} :: optional, use ImageMagick (otherwise is used GraphicsMagick), default false - return {Framework} -*/ -Controller.prototype.image = function(filename, fnProcess, headers, useImageMagick) { - var self = this; +ControllerProto.content = function(body, type, headers) { - if (self.res.success || !self.isConnected) - return self; + var self = this; + var res = self.res; - if (typeof(filename) === STRING) { - if (filename[0] === '~') - filename = '.' + filename.substring(1); - else - filename = utils.combine(self.framework.config['directory-public'], filename); - } + res.options.headers = headers; + res.options.code = self.status || 200; - self.subscribe.success(); - self.framework.responseImage(self.req, self.res, filename, fnProcess, headers, useImageMagick); + if (self.$evalroutecallback) { + var err = body instanceof ErrorBuilder ? body : null; + self.$evalroutecallback(err, err ? null : body); + return self; + } - return self; -}; + if (body instanceof ErrorBuilder) { -/* - Response Async file - @filename {String} - @downloadName {String} :: optional - @headers {Object} :: optional - return {Controller}; -*/ -Controller.prototype.fileAsync = function(filename, downloadName, headers) { - var self = this; + if (self.language && !body.resourceName) + body.resourceName = self.language; - var fn = function() { - self.file(filename, downloadName, headers); - }; + var tmp = body.output(true); + if (body.contentType) + res.options.type = body.contentType; + else + res.options.type = CT_JSON; - self.async.complete(fn); - return self; -}; + if (body.status !== 200) + res.options.code = body.status; -/* - Response stream - @contentType {String} - @stream {ReadStream} - @downloadName {String} :: optional - @headers {Object} :: optional key/value - return {Controller} -*/ -Controller.prototype.stream = function(contentType, stream, downloadName, headers) { - var self = this; + body = tmp; + F.stats.response.errorBuilder++; + } else + res.options.type = type || CT_TEXT; - if (self.res.success || !self.isConnected) - return self; + res.options.body = body; + res.options.compress = body.length > 4096; + res.$text(); - self.subscribe.success(); - self.framework.responseStream(self.req, self.res, contentType, stream, downloadName, headers); - return self; + if (self.precache && (!self.status || self.status === 200)) { + self.layout(''); + self.precache(body, res.options.type, headers, true); + } + + return self; }; /** - * Throw 401 - Bad request. - * @param {String} problem Description of problem (optional) - * @return {FrameworkController} + * Responds with plain/text body + * @param {String} body A response body (object is serialized into the JSON automatically). + * @param {Boolean} headers A custom headers. + * @return {Controller} */ -Controller.prototype.throw400 = function(problem) { - return this.view400(problem); -}; +ControllerProto.plain = function(body, headers) { -/* - Response 400 - return {Controller}; -*/ -Controller.prototype.view400 = function(problem) { - var self = this; + var self = this; + var res = self.res; + + res.options.code = self.status || 200; + res.options.headers = headers; + res.options.type = CT_TEXT; - if (problem && problem.length > 0) - self.problem(problem); + // Checks the HEAD method + if (self.req.method === 'HEAD') { + res.options.body = EMPTYBUFFER; + res.$text(); + F.stats.response.plain++; + return self; + } - if (self.res.success || !self.isConnected) - return self; + var type = typeof(body); - self.req.path = []; - self.subscribe.success(); - self.subscribe.route = self.framework.lookup(self.req, '#400', []); - self.subscribe.exception = problem; - self.subscribe.execute(400); - return self; + if (body == null) + body = ''; + else if (type === 'object') { + if (body && body.$$schema) + body = body.$clean(); + body = body ? JSON.stringify(body, null, 4) : ''; + } else + body = body ? body.toString() : ''; + + res.options.body = body; + res.$text(); + F.stats.response.plain++; + self.precache && self.precache(body, res.options.type, headers); + return self; }; /** - * Throw 401 - Unauthorized. - * @param {String} problem Description of problem (optional) - * @return {FrameworkController} + * Creates an empty response + * @param {Object/Number} headers A custom headers or a custom HTTP status. + * @return {Controller} */ -Controller.prototype.throw401 = function(problem) { - return this.view401(problem); -}; +ControllerProto.empty = function(headers) { -/* - Response 401 - return {Controller}; -*/ -Controller.prototype.view401 = function(problem) { - var self = this; + var self = this; + var res = self.res; - if (problem && problem.length > 0) - self.problem(problem); + if (typeof(headers) === 'number') { + self.status = headers; + headers = null; + } - if (self.res.success || !self.isConnected) - return self; - - self.req.path = []; - self.subscribe.success(); - self.subscribe.route = self.framework.lookup(self.req, '#401', []); - self.subscribe.exception = problem; - self.subscribe.execute(401); - return self; + res.options.code = self.status || 200; + res.options.headers = headers; + res.options.body = EMPTYBUFFER; + res.options.type = CT_TEXT; + res.options.compress = false; + res.$text(); + F.stats.response.empty++; + return self; }; /** - * Throw 403 - Forbidden. - * @param {String} problem Description of problem (optional) - * @return {FrameworkController} + * Creates an empty response with 204 + * @param {Object/Number} headers A custom headers or a custom HTTP status. + * @return {Controller} */ -Controller.prototype.throw403 = function(problem) { - return this.view403(problem); +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; }; -/* - Response 403 - return {Controller}; -*/ -Controller.prototype.view403 = function(problem) { - var self = this; - - if (problem && problem.length > 0) - self.problem(problem); +/** + * Destroys a request (closes it) + * @param {String} problem Optional. + * @return {Controller} + */ +ControllerProto.destroy = function(problem) { + var self = this; - if (self.res.success || !self.isConnected) - return self; + problem && self.problem(problem); + if (self.res.success || self.res.headersSent || !self.isConnected) + return self; - self.req.path = []; - self.subscribe.success(); - self.subscribe.route = self.framework.lookup(self.req, '#403', []); - self.subscribe.exception = problem; - self.subscribe.execute(403); - return self; + self.req.$total_success(); + self.req.connection && self.req.connection.destroy(); + F.stats.response.destroy++; + return self; }; /** - * Throw 404 - Not found. - * @param {String} problem Description of problem (optional) - * @return {FrameworkController} + * Responds with a file + * @param {String} filename + * @param {String} download Optional, a download name. + * @param {Object} headers Optional, additional headers. + * @param {Function} done Optinoal, callback. + * @return {Controller} */ -Controller.prototype.throw404 = function(problem) { - return this.view404(problem); +ControllerProto.file = function(filename, download, headers, done) { + + if (filename[0] === '~') + filename = filename.substring(1); + else + filename = F.path.public_cache(filename); + + var res = this.res; + res.options.filename = filename; + 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; }; -/* - Response 404 - return {Controller}; -*/ -Controller.prototype.view404 = function(problem) { - var self = this; - if (problem && problem.length > 0) - self.problem(problem); +/** + * Responds with an image + * @param {String or Stream} filename + * @param {Function(image)} fnProcess + * @param {Object} headers Optional, additional headers. + * @param {Function} done Optional, callback. + * @return {Controller} + */ +ControllerProto.image = function(filename, make, headers, done) { - if (self.res.success || !self.isConnected) - return self; + var res = this.res; - self.req.path = []; - self.subscribe.success(); - self.subscribe.route = self.framework.lookup(self.req, '#404', []); - self.subscribe.exception = problem; - self.subscribe.execute(404); - return self; -}; + if (typeof(filename) === 'string') { + if (filename[0] === '~') + filename = filename.substring(1); + else + filename = F.path.public_cache(filename); -/* - Response 500 - @error {String} - return {Controller}; -*/ -Controller.prototype.view500 = function(error) { - var self = this; + res.options.filename = filename; + } else + res.options.stream = filename; - self.framework.error(typeof(error) === STRING ? new Error(error) : error, self.name, self.req.uri); + res.options.make = make; + headers && (res.options.headers = headers); + done && (res.options.callback = done); + res.$image(); + return this; +}; - if (self.res.success || !self.isConnected) - return self; +/** + * Responds with a stream + * @param {String} contentType + * @param {Stream} stream + * @param {String} download Optional, a download name. + * @param {Object} headers Optional, additional headers. + * @param {Function} done Optinoal, callback. + * @return {Controller} + */ +ControllerProto.stream = function(type, stream, download, headers, done, nocompress) { + var res = this.res; + res.options.type = type; + res.options.stream = stream; + res.options.download = download; + res.options.headers = headers; + res.options.done = done; + res.options.compress = nocompress ? false : true; + res.$stream(); + return this; +}; - self.req.path = []; - self.subscribe.exception = error; - self.subscribe.success(); - self.subscribe.route = self.framework.lookup(self.req, '#500', []); - self.subscribe.exception = error; - self.subscribe.execute(500); - return self; +/** + * Throw 400 - Bad request. + * @param {String} problem Description of problem (optional) + * @return {Controller} + */ +ControllerProto.throw400 = ControllerProto.view400 = function(problem) { + return controller_error_status(this, 400, problem); }; /** - * Throw 500 - Internal Server Error - * @param {Error} error - * @return {FrameworkController} + * Throw 401 - Unauthorized. + * @param {String} problem Description of problem (optional) + * @return {Controller} */ -Controller.prototype.throw500 = function(error) { - return this.view500(error); +ControllerProto.throw401 = ControllerProto.view401 = function(problem) { + return controller_error_status(this, 401, problem); }; /** - * Throw 501 - Not implemented - * @param {String} problem Description of the problem (optional) - * @return {FrameworkController} + * Throw 403 - Forbidden. + * @param {String} problem Description of problem (optional) + * @return {Controller} */ -Controller.prototype.view501 = function(problem) { - var self = this; +ControllerProto.throw403 = ControllerProto.view403 = function(problem) { + return controller_error_status(this, 403, problem); +}; - if (problem && problem.length > 0) - self.problem(problem); +/** + * Throw 404 - Not found. + * @param {String} problem Description of problem (optional) + * @return {Controller} + */ +ControllerProto.throw404 = ControllerProto.view404 = function(problem) { + return controller_error_status(this, 404, problem); +}; - if (self.res.success || !self.isConnected) - return self; +/** + * Throw 409 - Conflict. + * @param {String} problem Description of problem (optional) + * @return {Controller} + */ +ControllerProto.throw409 = ControllerProto.view409 = function(problem) { + return controller_error_status(this, 409, problem); +}; - self.req.path = []; - self.subscribe.success(); - self.subscribe.route = self.framework.lookup(self.req, '#501', []); - self.subscribe.exception = problem; - self.subscribe.execute(501); - return self; +/** + * Throw 500 - Internal Server Error. + * @param {Error} error + * @return {Controller} + */ +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); }; /** * Throw 501 - Not implemented * @param {String} problem Description of the problem (optional) - * @return {FrameworkController} + * @return {Controller} */ -Controller.prototype.throw501 = function(problem) { - return this.view501(problem); +ControllerProto.throw501 = ControllerProto.view501 = function(problem) { + return controller_error_status(this, 501, problem); }; -/* - Response redirect - @url {String} - @permanent {Boolean} :: optional default false - return {Controller}; -*/ -Controller.prototype.redirect = function(url, permanent) { - var self = this; +/** + * 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); +}; - if (self.res.success || !self.isConnected) - return self; +/** + * Creates a redirect + * @param {String} url + * @param {Boolean} permanent Is permanent? Default: `false` + * @return {Controller} + */ +ControllerProto.redirect = function(url, permanent) { + this.precache && this.precache(null, null, null); + var res = this.res; + res.options.url = url; + res.options.permanent = permanent; + res.$redirect(); + return this; +}; - self.subscribe.success(); - self.req.clear(true); - self.res.success = true; - self.res.writeHead(permanent ? 301 : 302, { - 'Location': url - }); - self.res.end(); - self.framework._request_stats(false, false); - self.framework.emit('request-end', self.req, self.res); - self.framework.stats.response.redirect++; +/** + * A binary response + * @param {Buffer} buffer + * @param {String} type + * @param {String} encoding Transformation type: `binary`, `utf8`, `ascii`. + * @param {String} download Optional, download name. + * @param {Object} headers Optional, additional headers. + * @return {Controller} + */ +ControllerProto.binary = function(buffer, type, encoding, download, headers) { - return self; -}; + var res = this.res; -/* - Response Async View - @name {String} - @model {Object} :: optional - @headers {Object} :: optional - return {Controller}; -*/ -Controller.prototype.redirectAsync = function(url, permanent) { - var self = this; + if (typeof(encoding) === 'object') { + var tmp = encoding; + encoding = download; + download = headers; + headers = tmp; + } - var fn = function() { - self.redirect(url, permanent); - }; + if (typeof(download) === 'object') { + headers = download; + download = headers; + } - self.async.complete(fn); - return self; + res.options.body = buffer; + res.options.type = type; + res.options.download = download; + res.options.headers = headers; + res.options.encoding = encoding; + res.$binary(); + return this; }; -/* - Binary response - @buffer {Buffer} - return {Framework} -*/ -Controller.prototype.binary = function(buffer) { - var self = this; +/** + * Basic access authentication (baa) + * @param {String} label + * @return {Object} + */ +ControllerProto.baa = function(label) { - if (self.res.success || !self.isConnected) - return self; + var self = this; + self.precache && self.precache(null, null, null); - self.subscribe.success(); - self.req.clear(true); - self.res.success = true; - self.res.write(buffer.toString('binary'), 'binary'); - self.res.end(); - self.framework._request_stats(false, false); - self.framework.emit('request-end', self.req, self.res); - self.framework.stats.response.binary++; + if (label === undefined) + return self.req.authorization(); - return self; -}; + var res = self.res; + var headers = SINGLETON('!controller.baa'); -/* - Basic access authentication (baa) - @name {String} :: optional, default Administration - return {Object} :: if null then user is not authenticated else return { name: {String}, password: {String} }; -*/ -Controller.prototype.baa = function(name) { + headers['WWW-Authenticate'] = 'Basic realm="' + (label || 'Administration') + '"'; - var self = this; - var authorization = self.req.headers['authorization'] || ''; + res.options.code = 401; + res.options.body = '401: NOT AUTHORIZED'; + res.options.compress = false; + res.options.headers = headers; + res.options.type = CT_TEXT; + res.$text(); + self.cancel(); + return null; +}; - if (authorization === '') { - self.res.setHeader('WWW-Authenticate', 'Basic realm="' + (name || 'Administration') + '"'); - self.view401(); - return null; - } +/** + * Sends server-sent event message + * @param {String/Object} data + * @param {String} eventname Optional, an event name. + * @param {String} id Optional, a custom ID. + * @param {Number} retry A reconnection timeout in milliseconds when is an unexpected problem. + * @return {Controller} + */ +ControllerProto.sse = function(data, eventname, id, retry) { - return self.req.authorization(); -}; + var self = this; + var res = self.res; -/* - Send data via [S]erver-[s]ent [e]vents - @data {String or Object} - @eventname {String} :: optional - @id {String} :: optional - @retry {Number} :: optional, reconnection in milliseconds - return {Controller}; -*/ -Controller.prototype.sse = function(data, eventname, id, retry) { + if (!self.isConnected) + return self; - var self = this; - var res = self.res; + if (!self.type && res.success) + throw new Error('Response was sent.'); - if (!self.isConnected) - return self; + if (self.type > 0 && self.type !== 1) + throw new Error('Response was used.'); - if (self.type === 0 && res.success) - throw new Error('Response was sent.'); + if (!self.type) { - if (self.type > 0 && self.type !== 1) - throw new Error('Response was used.'); + self.type = 1; - if (self.type === 0) { + if (retry === undefined) + retry = self.route.timeout; - self.type = 1; + self.req.$total_success(); + self.req.on('close', () => self.close()); + res.success = true; + res.writeHead(self.status || 200, HEADERS.sse); + } - if (typeof(retry) === UNDEFINED) - retry = self.subscribe.route.timeout; + if (typeof(data) === 'object') + data = JSON.stringify(data); + else + data = data.replace(/\n/g, '\\n').replace(/\r/g, '\\r'); - self.subscribe.success(); - self.req.on('close', self.close.bind(self)); - res.success = true; - var headers = { - 'Pragma': 'no-cache' - }; - headers[RESPONSE_HEADER_CACHECONTROL] = 'no-cache, no-store, max-age=0, must-revalidate'; - headers[RESPONSE_HEADER_CONTENTTYPE] = 'text/event-stream'; - res.writeHead(self.status, headers); - } + var newline = '\n'; + var builder = ''; - if (typeof(data) === OBJECT) - data = JSON.stringify(data); - else - data = data.replace(/\n/g, '\\n').replace(/\r/g, '\\r'); + if (eventname) + builder = 'event: ' + eventname + newline; - var newline = '\n'; - var builder = ''; + builder += 'data: ' + data + newline; - if (eventname && eventname.length > 0) - builder = 'event: ' + eventname + newline; + if (id) + builder += 'id: ' + id + newline; - builder += 'data: ' + data + newline; + if (retry > 0) + builder += 'retry: ' + retry + newline; - if (id && id.toString().length > 0) - builder += 'id: ' + id + newline; + builder += newline; + res.write(builder); + F.stats.response.sse++; + return self; +}; - if (retry && retry > 0) - builder += 'retry: ' + retry + newline; +/** + * Close a response + * @param {Boolean} end + * @return {Controller} + */ +ControllerProto.close = function(end) { + var self = this; - builder += newline; + if (end === undefined) + end = true; - res.write(builder); - self.framework.stats.response.sse++; + if (!self.isConnected) + return self; - return self; -}; + if (self.type) { + self.isConnected = false; + self.res.success = true; + F.reqstats(false, false); + 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); + return self; + } -/* - Send a file or stream via [m]ultipart/x-[m]ixed-[r]eplace - @filename {String} - @{stream} {Stream} :: optional, if undefined then framework reads by the filename file from disk - @cb {Function} :: callback if stream is sent - return {Controller} -*/ -Controller.prototype.mmr = function(filename, stream, cb) { + self.isConnected = false; - var self = this; - var res = self.res; + if (self.res.success) + return self; - if (!self.isConnected) - return self; + self.res.success = true; + F.reqstats(false, false); + 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; +}; - if (self.type === 0 && res.success) - throw new Error('Response was sent.'); +/** + * Creates a proxy between current request and new URL + * @param {String} url + * @param {Function(err, response, headers)} callback Optional. + * @param {Object} headers Optional, additional headers. + * @param {Number} timeout Optional, timeout (default: 10000) + * @return {EventEmitter} + */ +ControllerProto.proxy = ControllerProto.proxy2 = function(url, callback, headers, timeout) { + + if (typeof(callback) === 'object') { + timeout = headers; + headers = callback; + callback = undefined; + } + + var self = this; + var flags = []; + var req = self.req; + var type = req.headers['content-type']; + var h = {}; + + flags.push(req.method); + flags.push('dnscache'); + + if ((/\/json/i).test(type)) + flags.push('json'); + + var tmp; + + if (url.indexOf('?') === -1) { + tmp = Qs.stringify(self.query); + if (tmp) + url += '?' + tmp; + } + + var keys = Object.keys(req.headers); + for (var i = 0, length = keys.length; i < length; i++) { + switch (keys[i]) { + case 'x-forwarded-for': + case 'x-forwarded-protocol': + case 'x-nginx-proxy': + case 'connection': + case 'content-type': + case 'host': + case 'accept-encoding': + break; + default: + h[keys[i]] = req.headers[keys[i]]; + break; + } + } + + 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++) { + if (headers[keys[i]]) + h[keys[i]] = headers[keys[i]]; + } + } + + return U.request(url, flags, self.body, function(err, data, code, headers) { + + if (err) { + callback && callback(err); + self.invalid().push(err); + } 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); + } + + }, null, h, ENCODING, timeout || 10000); +}; - if (self.type > 0 && self.type !== 2) - throw new Error('Response was used.'); +/** + * Renders view to response + * @param {String} name View name without `.html` extension. + * @param {Object} model A model, optional default: `undefined`. + * @param {Object} headers A custom headers, optional. + * @param {Boolean} isPartial When is `true` the method returns rendered HTML as `String` + * @return {Controller/String} + */ +ControllerProto.view = function(name, model, headers, partial, noasync, cachekey) { + + var self = this; + + if (typeof(name) !== 'string') { + partial = headers; + headers = model; + model = name; + name = self.viewname; + } else if (partial === undefined && typeof(headers) === 'boolean') { + partial = headers; + headers = null; + } + + if (!partial && self.res && self.res.success) + return self; + + if (self.layoutName === undefined) + self.layoutName = CONF.default_layout; + if (self.themeName === undefined) + self.themeName = CONF.default_theme; - if (self.type === 0) { - self.type = 2; - self.boundary = '----totaljs' + utils.GUID(10); - self.subscribe.success(); - res.success = true; - self.req.on('close', self.close.bind(self)); - var headers = { - 'Pragma': 'no-cache' - }; - headers[RESPONSE_HEADER_CONTENTTYPE] = 'multipart/x-mixed-replace; boundary=' + self.boundary; - headers[RESPONSE_HEADER_CACHECONTROL] = 'no-cache, no-store, max-age=0, must-revalidate'; - res.writeHead(self.status, headers); - } + // theme root `~some_view` + // views root `~~some_view` + // package `@some_view` + // theme `=theme/view` + + var key = 'view#=' + this.themeName + '/' + self._currentView + '/' + name; + var filename = F.temporary.other[key]; + var isLayout = self.isLayout; + + self.isLayout = false; + + // A small cache + if (!filename) { + + // ~ --> routed into the root of views (if the controller uses a theme then is routed into the root views of the theme) + // ~~ --> routed into the root of views (if the controller contains theme) + // / --> routed into the views (skipped) + // @ --> routed into the packages + // . --> routed into the opened path + // = --> routed into the theme - var type = typeof(stream); + var c = name[0]; + var skip = c === '/' ? 1 : c === '~' && name[1] === '~' ? 4 : c === '~' ? 2 : c === '@' ? 3 : c === '.' ? 5 : c === '=' ? 6 : 0; + var isTheme = false; - if (type === FUNCTION) { - cb = stream; - stream = null; - } + if (REG_HTTPHTTPS.test(name)) + skip = 7; - res.write('--' + self.boundary + '\r\n' + RESPONSE_HEADER_CONTENTTYPE + ': ' + utils.getContentType(path.extname(filename)) + '\r\n\r\n'); + filename = name; - if (typeof(stream) !== UNDEFINED && stream !== null) { + if (self.themeName && skip < 3) { + filename = '.' + F.path.themes(self.themeName + '/views/' + (isLayout || skip ? '' : self._currentView.substring(1)) + (skip ? name.substring(1) : name)).replace(REG_SANITIZE_BACKSLASH, '/'); + isTheme = true; + } - stream.on('end', function() { - self = null; - if (cb) - cb(); - }); + if (skip === 4) { + filename = filename.substring(1); + name = name.substring(1); + skip = 2; + } - stream.pipe(res, { - end: false - }); - self.framework.stats.response.mmr++; - return self; - } + if (!isTheme && !isLayout && !skip && self._currentView) + filename = self._currentView + name; - stream = fs.createReadStream(filename); + if (!isTheme && (skip === 2 || skip === 3)) + filename = name.substring(1); - stream.on('end', function() { - self = null; - if (cb) - cb(); - }); + if (skip === 3) + filename = '.' + F.path.package(filename); - stream.pipe(res, { - end: false - }); - self.framework.stats.response.mmr++; + if (skip === 6) { + c = U.parseTheme(filename); + name = name.substring(name.indexOf('/') + 1); + filename = '.' + F.path.themes(c + '/views/' + name).replace(REG_SANITIZE_BACKSLASH, '/'); + } - return self; -}; + if (skip === 7) { -/* - Close a response - @end {Boolean} :: end response? - default true - return {Controller} -*/ -Controller.prototype.close = function(end) { - var self = this; + if (F.temporary.other[key] === 0) { + setTimeout(function() { + self.view(name, model, headers, partial); + }, 100, self); + return; + } - if (typeof(end) === UNDEFINED) - end = true; + filename = F.path.temp('view' + name.hash() + '.html'); + F.temporary.other[key] = 0; - if (!self.isConnected) - return self; + var done = { callback: NOOP }; - if (self.type === 0) { + F.download(name, filename, function(err) { + if (err) { + F.temporary.other[key] = undefined; + if (done.callback === NOOP) + F.throw500(err); + else + done.callback(err); + } else { + F.temporary.other[key] = '.' + filename.substring(0, filename.length - 5); + done.callback(null, self.view(name, model, headers, partial)); + } + }); - self.isConnected = false; + return function(cb) { + done.callback = cb; + }; + } + } - if (!self.res.success) { + return self.$viewrender(filename, framework_internal.viewEngine(name, filename, self), model, headers, partial, isLayout, noasync, cachekey); +}; - self.res.success = true; +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); +}; - if (end) - self.res.end(); +ControllerProto.view_compile = function(body, model, headers, partial, key) { - self.framework._request_stats(false, false); - self.framework.emit('request-end', self.req, self.res); - } + if (headers === true) { + key = partial; + partial = true; + headers = undefined; + } else if (typeof(headers) === 'string') { + key = headers; + headers = undefined; + } else if (typeof(partial) === 'string') { + key = partial; + partial = undefined; + } - return self; - } + return this.$viewrender('[dynamic view]', framework_internal.viewEngineCompile(body, this.language, this, key), model, headers, partial); +}; - if (self.type === 2) - self.res.write('\r\n\r\n--' + self.boundary + '--'); +ControllerProto.$viewrender = function(filename, generator, model, headers, partial, isLayout, noasync, cachekey) { - self.isConnected = false; - self.res.success = true; + var self = this; + var err; - if (end) - self.res.end(); + if (!generator) { - self.framework._request_stats(false, false); - self.framework.emit('request-end', self.req, self.res); - self.type = 0; + err = new Error('View "' + filename + '" not found.'); - return self; -}; + if (partial) { + F.error(err, self.name, self.uri); + return self.outputPartial; + } -/* - Send proxy request - @url {String} - @obj {Object} - @fnCallback {Function} :: optional - @timeout {Number} :: optional - return {Controller} -*/ -Controller.prototype.proxy = function(url, obj, fnCallback, timeout) { + if (isLayout) { + self.res.throw500(err); + return self; + } - var self = this; - var headers = { - 'X-Proxy': 'total.js' - }; + self.view500(err); + return self; + } - headers[RESPONSE_HEADER_CONTENTTYPE] = 'application/json'; + var value = ''; + self.$model = model; - var tmp; + if (isLayout) + self._currentView = self._defaultView || ''; - if (typeof(fnCallback) === NUMBER) { - tmp = timeout; - timeout = fnCallback; - fnCallback = tmp; - } + var helpers = F.helpers; - if (typeof(obj) === FUNCTION) { - tmp = fnCallback; - fnCallback = obj; - obj = tmp; - } + try { - utils.request(url, ['post', 'json'], obj, function(error, data, code, headers) { + 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; + } - if (!fnCallback) - return; + 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); - if ((headers['content-type'] || '').indexOf('application/json') !== -1) - data = JSON.parse(data); + } catch (ex) { - fnCallback.call(self, error, data, code, headers); + err = new Error('View "' + filename + '": ' + ex.message); - }, null, headers, 'utf8', timeout || 10000); + if (!partial) { + self.view500(err); + return self; + } - return self; -}; + self.error(err); -/* - Return database - @name {String} - return {NoSQL}; -*/ -Controller.prototype.database = function() { - var self = this.framework; - return self.database.apply(self, arguments); -}; + if (self.partial) + self.outputPartial = ''; + else + self.output = ''; -/* - Response view - @name {String} - @model {Object} :: optional - @headers {Object} :: optional - @isPartial {Boolean} :: optional - return {Controller or String}; string is returned when isPartial == true -*/ -Controller.prototype.view = function(name, model, headers, isPartial) { - var self = this; + isLayout = false; + return value; + } - if (typeof(isPartial) === UNDEFINED && typeof(headers) === BOOLEAN) { - isPartial = headers; - headers = null; - } + // noasync = true --> rendered inline view in view - if (self.res.success && !isPartial) - return self; + if (self.$viewasync && self.$viewasync.length) { - var skip = name[0] === '~'; - var filename = name; - var isLayout = self.isLayout; + var can = ((isLayout || !self.layoutName) && noasync !== true) || !!cachekey; + if (can) { + var done = {}; + var obj = {}; - self.isLayout = false; + 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; - if (!self.isLayout && !skip) - filename = self._currentView + name; + self.$viewasync.waitFor(function(item, next) { - if (skip) - filename = name.substring(1); + 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(); + } - var generator = internal.generateView(self, name, filename); - if (generator === null) { + 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) { + + self.outputPartial = ''; + self.output = ''; + isLayout = false; + + if (partial) + return value; + + self.req.$total_success(); + + 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; + 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; + return value; + } + + return self; +}; - if (isPartial) - return self.outputPartial; +/** + * 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, 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} + */ +ControllerProto.memorize = function(key, expires, disabled, fnTo, fnFrom) { + + var self = this; + + if (disabled === true) { + fnTo.call(self); + return self; + } + + self.themeName && (key += '#' + self.themeName); + + var output = F.cache.read2(key); + if (!output) + return self.$memorize_prepare(key, expires, disabled, fnTo, fnFrom); + + if (typeof(disabled) === 'function') { + var tmp = fnTo; + fnTo = disabled; + fnFrom = tmp; + } + + self.layoutName = output.layout; + self.themeName = output.theme; + + var res = self.res; + + res.options.code = self.status || 200; + res.options.type = output.type; + res.options.headers = output.headers; + res.options.body = output.content; + + if (output.type !== CT_HTML) { + fnFrom && fnFrom.call(self); + res.$text(); + return; + } + + switch (output.type) { + case CT_TEXT: + F.stats.response.plain++; + return self; + case CT_JSON: + F.stats.response.json++; + return self; + case CT_HTML: + F.stats.response.view++; + break; + } + + var length = output.repository.length; + for (var i = 0; i < length; i++) { + var key = output.repository[i].key; + if (self.repository[key] === undefined) + self.repository[key] = output.repository[i].value; + } + + fnFrom && fnFrom.call(self); + + if (self.layoutName) { + self.output = Buffer.from(output.content); + self.isLayout = true; + self.view(self.layoutName, null); + } else { + self.req.$total_success(); + res.$text(); + } + + return self; +}; + +ControllerProto.$memorize_prepare = function(key, expires, disabled, fnTo, fnFrom) { + + var self = this; + var pk = '$memorize' + key; + + if (F.temporary.processing[pk]) { + setTimeout(function() { + !self.req.$total_canceled && self.memorize(key, expires, disabled, fnTo, fnFrom); + }, 500); + return self; + } + + self.precache = function(value, contentType, headers, isView) { + + if (!value && !contentType && !headers) { + delete F.temporary.processing[pk]; + self.precache = null; + return; + } + + var options = { content: value, type: contentType || CT_TEXT, layout: self.layoutName, theme: self.themeName }; + if (headers) + options.headers = headers; + + if (isView) { + options.repository = []; + for (var name in self.repository) { + var value = self.repository[name]; + value !== undefined && options.repository.push({ key: name, value: value }); + } + } + + F.cache.add(key, options, expires, false); + self.precache = null; + delete F.temporary.processing[pk]; + }; + + if (typeof(disabled) === 'function') + fnTo = disabled; + + F.temporary.processing[pk] = true; + fnTo.call(self); + return self; +}; - var err = 'View "' + filename + '" not found.'; +// ********************************************************************************* +// ================================================================================= +// F.WebSocket +// ================================================================================= +// ********************************************************************************* - if (isLayout) { - self.subscribe.success(); - self.framework.response500(self.req, self.res, err); - return; - } +const NEWLINE = '\r\n'; +const SOCKET_RESPONSE = 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\n\r\n'; +const SOCKET_RESPONSE_COMPRESS = 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nSec-WebSocket-Extensions: permessage-deflate\r\n\r\n'; +const SOCKET_RESPONSE_PROTOCOL = 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nSec-WebSocket-Protocol: {1}\r\n\r\n'; +const SOCKET_RESPONSE_PROTOCOL_COMPRESS = 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nSec-WebSocket-Protocol: {1}\r\nSec-WebSocket-Extensions: permessage-deflate\r\n\r\n'; +const SOCKET_HASH = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; +const SOCKET_ALLOW_VERSION = [13]; + +function WebSocket(path, name, id) { + this._keys = []; + this.id = id; + this.online = 0; + this.connections = {}; + this.name = name; + this.isController = true; + this.url = U.path(path); + this.route = null; + this.$events = {}; + + // on('open', function(client) {}); + // on('close', function(client) {}); + // on('message', function(client, message) {}); + // on('error', function(error, client) {}); + // Events.EventEmitter.call(this); +} - self.view500(err); - return; - } +WebSocket.prototype = { - var value = ''; - self.$model = model; + get repository() { + if (this.$repository) + return this.$repository; + else + return this.$repository ? this.$repository : (this.$repository = {}); + }, + + get global() { + OBSOLETE('controller.global', 'Use: G'); + return F.global; + }, + + get config() { + OBSOLETE('controller.config', 'Use: CONF'); + return CONF; + }, + + get cache() { + OBSOLETE('controller.cache', 'Use: F.cache or CACHE()'); + return F.cache; + }, + + get isDebug() { + OBSOLETE('controller.isDebug', 'Use: DEBUG'); + return DEBUG; + }, + + get path() { + OBSOLETE('controller.path', 'Use: PATH'); + return F.path; + }, + + get isSecure() { + OBSOLETE('controller.isSecure', 'Use: controller.secured'); + return this.req.isSecure; + }, + + get secured() { + return this.req.secured; + }, + + get params() { + if (this.$params) + return this.$params; + var split = framework_internal.routeSplit(this.url, true); + var names = this.route.paramnames; + if (names) { + var obj = {}; + for (var i = 0; i < names.length; i++) + obj[names[i]] = split[this.route.param[i]]; + this.$params = obj; + return obj; + } else { + this.$params = EMPTYOBJECT; + return EMPTYOBJECT; + } + }, + + get keys() { + return this._keys; + } +}; + + +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; + 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; +}; + +WebSocketProto.on = function(name, fn) { + if (this.$events[name]) + this.$events[name].push(fn); + else + this.$events[name] = [fn]; + return this; +}; + +WebSocketProto.once = function(name, fn) { + fn.$once = true; + return this.on(name, fn); +}; + +WebSocketProto.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; +}; + +WebSocketProto.removeAllListeners = function(name) { + if (name === true) + this.$events = EMPTYOBJECT; + else if (name) + this.$events[name] = undefined; + else + this.$events = {}; + return this; +}; - var sitemap = function() { - return self.sitemap.apply(self, arguments); - }; +/** + * Sends a message + * @param {String} message + * @param {String Array or Function(id, client)} id (optional) + * @param {String Array or Function(id, client)} blacklist (optional) + * @param {Function(key, value)} replacer for JSON (optional) + * @return {WebSocket} + */ +WebSocketProto.send = function(message, id, blacklist, replacer) { - if (isLayout) { - self._currentCSS = self._defaultCSS || ''; - self._currentJS = self._defaultJS || ''; - self._currentDownload = self._defaultDownload || ''; - self._currentVideo = self._defaultVideo || ''; - self._currentImage = self._defaultImage || ''; - self._currentView = self._defaultView || ''; - self._currentTemplate = self._defaultTemplate || ''; - self._currentContent = self._defaultContent || ''; - } + var self = this; + var keys = self._keys; - var helpers = self.framework.helpers; + if (!keys || !keys.length || message === undefined) + return self; - try { - value = generator.call(self, self, self.repository, model, self.session, self.get, self.post, self.url, self.framework.global, helpers, self.user, self.config, self.framework.functions, 0, sitemap, isPartial ? self.outputPartial : self.output); - } catch (ex) { + var data; + var raw = false; - var err = new Error('View: ' + name + ' - ' + ex.toString()); + for (var i = 0, length = keys.length; i < length; i++) { - if (!isPartial) { - self.view500(err); - return; - } + var conn = self.connections[keys[i]]; - self.error(err); + 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, message)) + continue; + } else + throw new Error('Invalid "id" argument.'); + } - if (self.isPartial) { - value = self.outputPartial; - self.outputPartial = ''; - } else { - value = self.output; - self.output = ''; - } + if (blacklist) { + if (blacklist instanceof Array) { + if (websocket_valid_array(conn.id, blacklist)) + continue; + } else if (blacklist instanceof Function) { + if (websocket_valid_fn(conn.id, conn, blacklist, message)) + continue; + } else + throw new Error('Invalid "blacklist" argument.'); + } - isLayout = false; - return value; - } + if (data === undefined) { + if (conn.type === 3) { + raw = true; + data = JSON.stringify(message, replacer); + } else + data = message; + } - if (!isLayout && self.precache && self.status === 200) - self.precache(value, CONTENTTYPE_TEXTHTML, headers, true); + conn.send(data, raw); + F.stats.response.websocket++; + } - if (isLayout || utils.isNullOrEmpty(self.layoutName)) { + return self; +}; - self.outputPartial = ''; - self.output = ''; - isLayout = false; +WebSocketProto.send2 = function(message, comparer, replacer, params) { - if (isPartial) - return value; + var self = this; + var keys = self._keys; + if (!keys || !keys.length || message === undefined) + return self; - self.subscribe.success(); + if (!params && replacer != null && typeof(replacer) !== 'function') { + params = replacer; + replacer = null; + } - if (!self.isConnected) - return; + var data; + var raw = false; - self.framework.responseContent(self.req, self.res, self.status, value, CONTENTTYPE_TEXTHTML, self.config['allow-gzip'], headers); - self.framework.stats.response.view++; + for (var i = 0, length = keys.length; i < length; i++) { - return self; - } + var conn = self.connections[keys[i]]; - if (isPartial) - self.outputPartial = value; - else - self.output = value; + if (data === undefined) { + if (conn.type === 3) { + raw = true; + data = JSON.stringify(message, replacer); + } else + data = message; + } - self.isLayout = true; - value = self.view(self.layoutName, self.$model, headers, isPartial); + if (comparer && !comparer(conn, message, params)) + continue; - if (isPartial) { - self.outputPartial = ''; - self.isLayout = false; - return value; - } + conn.send(data, raw); + F.stats.response.websocket++; + } - return self; + return self; }; -/* - Memorize a view (without layout) into the cache - @key {String} :: cache key - @expire {Date} :: expiration - @disabled {Boolean} :: disabled for debug mode - @fnTo {Function} :: if cache not exist - @fnFrom {Function} :: optional, if cache is exist - return {Controller} -*/ -Controller.prototype.memorize = function(key, expire, disabled, fnTo, fnFrom) { - - var self = this; - var output = self.cache.read(key); - - if (output === null) { - - if (disabled === true) { - fnTo(); - return self; - } - - self.precache = function(value, contentType, headers, isView) { - - var options = { - content: value, - type: contentType - }; - - if (headers) - options.headers = headers; - - if (isView) { - var keys = Object.keys(self.repository); - var length = keys.length; - options.repository = []; - for (var i = 0; i < length; i++) { - var name = keys[i]; - if (name[0] === '$' || name === 'sitemap') { - var value = self.repository[name]; - if (value) - options.repository.push({ - key: name, - value: value - }); - } - } - } - - self.cache.add(key, options, expire); - self.precache = null; - }; - - if (typeof(disabled) === FUNCTION) - fnTo = disabled; - - fnTo(); - return self; - } - - if (typeof(disabled) === FUNCTION) { - var tmp = fnTo; - fnTo = disabled; - fnFrom = tmp; - } - - if (fnFrom) - fnFrom(); - - if (output.type !== CONTENTTYPE_TEXTHTML) - self.framework.responseContent(self.req, self.res, self.status, output.content, output.type, self.config['allow-gzip'], output.headers); - - switch (output.type) { - case CONTENTTYPE_TEXTPLAIN: - self.framework.stats.response.plain++; - return self; - case 'application/json': - self.framework.stats.response.json++; - return self; - case CONTENTTYPE_TEXTHTML: - self.framework.stats.response.view++; - break; - } - - var length = output.repository.length; - for (var i = 0; i < length; i++) - self.repository[output.repository[i].key] = output.repository[i].value; - - if (utils.isNullOrEmpty(self.layoutName)) { - - self.subscribe.success(); - - if (!self.isConnected) - return self; - - self.framework.responseContent(self.req, self.res, self.status, output.content, output.type, self.config['allow-gzip'], output.headers); - return self; - } - - self.output = output.content; - self.isLayout = true; - self.view(self.layoutName, null); - - return self; -}; +function websocket_valid_array(id, arr) { + return arr.indexOf(id) !== -1; +} -/* - Response Async View - @name {String} - @model {Object} :: optional - @headers {Object} :: optional - return {Controller}; -*/ -Controller.prototype.viewAsync = function(name, model, headers) { - var self = this; +function websocket_valid_fn(id, client, fn, msg) { + return fn && fn(id, client, msg) ? true : false; +} + +/** + * Sends a ping message + * @return {WebSocket} + */ +WebSocketProto.ping = function() { - var fn = function() { - self.view(name, model, headers); - }; + var keys = this._keys; + if (!keys) + return this; - self.async.complete(fn); - return self; -}; + var length = keys.length; + if (length) { + this.$ping = true; + F.stats.other.websocketPing++; + for (var i = 0; i < length; i++) + this.connections[keys[i]].ping(); + } -// ********************************************************************************* -// ================================================================================= -// Framework.WebSocket -// ================================================================================= -// ********************************************************************************* + return this; +}; -var NEWLINE = '\r\n'; -var SOCKET_RESPONSE = 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nX-Powered-By: {0}\r\nSec-WebSocket-Accept: {1}\r\n\r\n'; -var SOCKET_RESPONSE_ERROR = 'HTTP/1.1 403 Forbidden\r\nConnection: close\r\nX-WebSocket-Reject-Reason: 403 Forbidden\r\n\r\n'; -var SOCKET_HASH = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; -var SOCKET_ALLOW_VERSION = [13]; +/** + * Closes a connection + * @param {String Array} id Client id, optional, default `null`. + * @param {String} message A message for the browser. + * @param {Number} code Optional default 1000. + * @return {Websocket} + */ +WebSocketProto.close = function(id, message, code) { -/* - WebSocket - @framework {total.js} - @path {String} - @name {String} :: Controller name - return {WebSocket} -*/ -function WebSocket(framework, path, name, id) { - this._keys = []; - this.id = id; - this.online = 0; - this.connections = {}; - this.framework = framework; - this.repository = {}; - this.name = name; - this.url = utils.path(path); - - // on('open', function(client) {}); - // on('close', function(client) {}); - // on('message', function(client, message) {}); - // on('error', function(error, client) {}); - events.EventEmitter.call(this); -} + var keys = this._keys; -WebSocket.prototype = { + if (!keys) + return this; - get global() { - return this.framework.global; - }, + if (typeof(id) === 'string') { + code = message; + message = id; + id = null; + } - get config() { - return this.framework.config; - }, + var length = keys.length; + if (!length) + return this; - get cache() { - return this.framework.cache; - }, + if (!id || !id.length) { + for (var i = 0; i < length; i++) { + var _id = keys[i]; + this.connections[_id].close(message, code); + this.$remove(_id); + } + this.$refresh(); + return this; + } - get isDebug() { - return this.framework.config.debug; - }, + var is = id instanceof Array; + var fn = typeof(id) === 'function' ? id : null; - get path() { - return this.framework.path; - }, + for (var i = 0; i < length; i++) { - get fs() { - return this.framework.fs; - }, + var _id = keys[i]; + if (is && id.indexOf(_id) === -1) + continue; - get isSecure() { - return this.req.isSecure; - }, + var conn = this.connections[_id]; + if (fn && !fn.call(this, _id, conn)) + continue; - get async() { + conn.close(message, code); + this.$remove(_id); + } - var self = this; + this.$refresh(); + return this; +}; - if (typeof(self._async) === UNDEFINED) - self._async = new utils.Async(self); +/** + * Error caller + * @param {Error/String} err + * @return {WebSocket/Function} + */ +WebSocketProto.error = function(err) { + var result = F.error(typeof(err) === 'string' ? new Error(err) : err, this.name, this.path); + return err ? this : result; +}; - return self._async; - } -} +/** + * Creates a problem + * @param {String} message + * @return {WebSocket} + */ +WebSocketProto.wtf = WebSocketProto.problem = function(message) { + F.problem(message, this.name, this.uri); + return this; +}; -WebSocket.prototype.__proto__ = Object.create(events.EventEmitter.prototype, { - constructor: { - value: WebSocket, - enumberable: false - } -}); +/** + * Creates a change + * @param {String} message + * @return {WebSocket} + */ +WebSocketProto.change = function(message) { + F.change(message, this.name, this.uri, this.ip); + return this; +}; -/* - Send message - @message {String or Object} - @id {String Array} - @blacklist {String Array} - return {WebSocket} -*/ -WebSocket.prototype.send = function(message, id, blacklist) { +/** + * The method executes a provided function once per client. + * @param {Function(connection, index)} fn + * @return {WebSocket} + */ +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 arr ? arr : self; +}; - var self = this; - var keys = self._keys; - var length = keys.length; +/** + * Finds a connection + * @param {String} id + * @return {WebSocketClient} + */ +WebSocketProto.find = function(id) { + var self = this; - if (length === 0) - return self; + if (!self._keys) + return self; + var length = self._keys.length; + var isFn = typeof(id) === 'function'; - var fn = typeof(blacklist) === FUNCTION ? blacklist : null; - var is = blacklist instanceof Array; + for (var i = 0; i < length; i++) { + var connection = self.connections[self._keys[i]]; + if (isFn) { + if (id(connection, connection.id)) + return connection; + } else if (connection.id === id) + return connection; + } + return null; +}; - if (typeof(id) === UNDEFINED || id === null || id.length === 0) { +/** + * Destroys a WebSocket controller + * @param {String} problem Optional. + * @return {WebSocket} + */ +WebSocketProto.destroy = function(problem) { + var self = this; - for (var i = 0; i < length; i++) { + problem && self.problem(problem); + if (!self.connections && !self._keys) + return self; - var _id = keys[i]; + self.close(); + self.$events.destroy && self.emit('destroy'); - if (is && blacklist.indexOf(_id) !== -1) - continue; + setTimeout(function() { - var conn = self.connections[_id]; + 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(); + } + } - if (fn !== null && !fn.call(self, _id, conn)) - continue; + self.connections = null; + self._keys = null; + self.route = null; + self.buffer = null; + delete F.connections[self.id]; + self.removeAllListeners(); - conn.send(message); - self.framework.stats.response.websocket++; - } + }, 1000); - self.emit('send', message, null, []); - return self; - } + return self; +}; - fn = typeof(id) === FUNCTION ? id : null; - is = id instanceof Array; +/** + * Enables auto-destroy websocket controller when any user is not online + * @param {Function} callback + * @return {WebSocket] + */ +WebSocketProto.autodestroy = function(callback) { + var self = this; + var key = 'websocket:' + self.id; + self.on('open', () => clearTimeout2(key)); + self.on('close', function() { + !self.online && setTimeout2(key, function() { + callback && callback.call(self); + self.destroy(); + }, 5000); + }); + return self; +}; - for (var i = 0; i < length; i++) { +/** + * Internal function + * @return {WebSocket} + */ +WebSocketProto.$refresh = function() { + if (this.connections) { + this._keys = Object.keys(this.connections); + this.online = this._keys.length; + } else + this.online = 0; + return this; +}; - var _id = keys[i]; +/** + * Internal function + * @param {String} id + * @return {WebSocket} + */ +WebSocketProto.$remove = function(id) { + if (this.connections) + delete this.connections[id]; + return this; +}; - if (is && id.indexOf(_id) === -1) - continue; +/** + * Internal function + * @param {WebSocketClient} client + * @return {WebSocket} + */ +WebSocketProto.$add = function(client) { + this.connections[client._id] = client; + return this; +}; - var conn = self.connections[_id]; +/** + * A resource header + * @param {String} name A resource name. + * @param {String} key A resource key. + * @return {String} + */ +WebSocketProto.resource = function(name, key) { + return F.resource(name, key); +}; - if (fn !== null && !fn.call(self, _id, conn) === -1) - continue; +WebSocketProto.log = function() { + F.log.apply(framework, arguments); + return this; +}; - conn.send(message); - self.framework.stats.response.websocket++; - } +WebSocketProto.logger = function() { + F.logger.apply(framework, arguments); + return this; +}; - self.emit('send', message, id, blacklist); - return self; +WebSocketProto.check = function() { + this.$ping && this.all(websocketcheck_ping); + return this; }; -/* - Close connection - @id {String Array} :: optional, default null - @message {String} :: optional - @code {Number} :: optional, default 1000 - return {WebSocket} -*/ -WebSocket.prototype.close = function(id, message, code) { +function websocketcheck_ping(client) { + if (!client.$ping) { + client.close(); + F.stats.other.websocketCleaner++; + } +} - var self = this; - var keys = self._keys; +/** + * WebSocket controller + * @param {Request} req + * @param {Socket} socket + */ +function WebSocketClient(req, socket) { + this.$ping = true; + this.container; + this._id; + this.id = ''; + this.socket = socket; + this.req = req; + + // this.isClosed = false; + this.errors = 0; + this.length = 0; + this.current = {}; + + // 1 = raw - not implemented + // 2 = plain + // 3 = JSON + + this.type = 2; + // this._isClosed = false; +} - if (typeof(id) === STRING) { - code = message; - message = id; - id = null; - } +WebSocketClient.prototype = { - if (keys === null) - return self; + get protocol() { + return (this.req.headers['sec-websocket-protocol'] || '').replace(REG_EMPTY, '').split(','); + }, - var length = keys.length; + get ip() { + return this.req.ip; + }, - if (length === 0) - return self; + get get() { + return this.req.query; + }, - if (typeof(id) === UNDEFINED || id === null || id.length === 0) { - for (var i = 0; i < length; i++) { - var _id = keys[i]; - self.connections[_id].close(message, code); - self._remove(_id); - } - self._refresh(); - return self; - } + get query() { + return this.req.query; + }, - var is = id instanceof Array; - var fn = typeof(id) === FUNCTION ? id : null; + get headers() { + return this.req.headers; + }, - for (var i = 0; i < length; i++) { + get uri() { + return this.req.uri; + }, - var _id = keys[i]; + get config() { + OBSOLETE('controller.config', 'Use: CONF'); + return this.container.config; + }, - if (is && id.indexOf(_id) === -1) - continue; + get global() { + OBSOLETE('controller.global', 'Use: G'); + return this.container.global; + }, - var conn = self.connections[_id]; + get sessionid() { + return this.req.sessionid; + }, - if (fn !== null && !fn.call(self, _id, conn)) - continue; + get session() { + return this.req.session; + }, - conn.close(message, code); - self._remove(_id); - } + set session(value) { + this.req.session = value; + }, - self._refresh(); - return self; -}; + get user() { + return this.req.user; + }, -/* - Error - @err {Error} - return {Framework} -*/ -WebSocket.prototype.error = function(err) { - var self = this; - self.framework.error(typeof(err) === STRING ? new Error(err) : err, self.name, self.path); - return self; -}; + set user(value) { + this.req.user = value; + }, -/* - Problem - @message {String} - return {Framework} -*/ -WebSocket.prototype.problem = function(message) { - var self = this; - self.framework.problem(message, self.name, self.uri); - return self; + get mobile() { + return this.req.mobile; + } }; -/* - Change - @message {String} - return {Framework} -*/ -WebSocket.prototype.change = function(message) { - var self = this; - self.framework.change(message, self.name, self.uri, self.ip); - return self; +const WebSocketClientProto = WebSocketClient.prototype; + +WebSocketClientProto.isWebSocket = true; + +WebSocketClientProto.cookie = function(name) { + return this.req.cookie(name); }; +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; + allow = allow || EMPTYARRAY; + + var self = this; + + if (SOCKET_ALLOW_VERSION.indexOf(U.parseInt(self.req.headers['sec-websocket-version'])) === -1) + return false; + + self.length = length; + + var origin = self.req.headers.origin || ''; + var length = allow.length; + + if (length && allow.indexOf('*') === -1) { + var is = false; + for (var i = 0; i < length; i++) { + if (origin.indexOf(allow[i]) !== -1) { + is = true; + break; + } + } + if (!is) + return false; + } + + length = protocols.length; + if (length) { + for (var i = 0; i < length; i++) { + if (self.protocol.indexOf(protocols[i]) === -1) + return false; + } + } + + 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(Buffer.from(header, 'binary')); + self.ready = true; + + if (compress) { + self.inflatepending = []; + self.inflatelock = false; + self.inflate = Zlib.createInflateRaw(WEBSOCKET_COMPRESS_OPTIONS); + self.inflate.$websocket = self; + self.inflate.on('error', function() { + if (!self.$uerror) { + self.$uerror = true; + self.close('Invalid data', 1003); + } + }); + self.inflate.on('data', websocket_inflate); + + self.deflatepending = []; + self.deflatelock = false; + self.deflate = Zlib.createDeflateRaw(WEBSOCKET_COMPRESS_OPTIONS); + self.deflate.$websocket = self; + self.deflate.on('error', function() { + if (!self.$uerror) { + self.$uerror = true; + self.close('Invalid data', 1003); + } + }); + self.deflate.on('data', websocket_deflate); + } + + self._id = Date.now() + U.GUID(5); + self.id = self._id; + return true; +}; + +function websocket_inflate(data) { + var ws = this.$websocket; + if (ws && ws.inflatechunks) { + ws.inflatechunks.push(data); + ws.inflatechunkslength += data.length; + } +} + +function websocket_deflate(data) { + var ws = this.$websocket; + if (ws && ws.deflatechunks) { + ws.deflatechunks.push(data); + ws.deflatechunkslength += data.length; + } +} -/* - All connections (forEach) - @fn {Function} :: function(client, index) {} - return {WebSocketClient}; -*/ -WebSocket.prototype.all = function(fn) { +/** + * Add a container to client + * @param {WebSocket} container + * @return {WebSocketClient} + */ +WebSocketClientProto.upgrade = function(container) { + var self = this; + self.req.on('error', websocket_onerror); + self.container = container; + self.socket.$websocket = this; + self.socket.on('data', websocket_ondata); + self.socket.on('error', websocket_onerror); + self.socket.on('close', websocket_close); + self.socket.on('end', websocket_close); + self.container.$add(self); + self.container.$refresh(); + 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; +}; + +function websocket_ondata(chunk) { + this.$websocket.$ondata(chunk); +} - var self = this; - var length = self._keys.length; +function websocket_onerror(e) { + this.destroy && this.destroy(); + this.$websocket.$onerror(e); +} - for (var i = 0; i < length; i++) { - var id = self._keys[i]; - if (fn(self.connections[id], i)) - break; - } +function websocket_close() { + this.destroy && this.destroy(); + this.$websocket.$onclose(); +} - return self; -}; +WebSocketClientProto.$ondata = function(data) { + + var self = this; + + if (self.isClosed) + return; + + var current = self.current; + + if (data) { + if (current.buffer) { + CONCAT[0] = current.buffer; + CONCAT[1] = data; + current.buffer = Buffer.concat(CONCAT); + } else + current.buffer = data; + } + + 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 (decompress) { + current.final && self.parseInflate(); + } 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 && self.$decode(); + } + + break; + + case 0x02: + + // binary + if (decompress) { + current.final && self.parseInflate(); + } 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 && self.$decode(); + } + + break; + + case 0x08: + // 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 + self.socket.write(U.getWebSocketFrame(0, 'PONG', 0x0A)); + current.buffer = null; + current.inflatedata = null; + self.$ping = true; + break; + + case 0x0a: + // pong + self.$ping = true; + current.buffer = null; + current.inflatedata = null; + break; + } + + if (current.buffer) { + current.buffer = current.buffer.slice(current.length, current.buffer.length); + current.buffer.length && self.$ondata(); + } +}; + +function buffer_concat(buffers, length) { + var buffer = Buffer.alloc(length); + var offset = 0; + for (var i = 0, n = buffers.length; i < n; i++) { + buffers[i].copy(buffer, offset); + offset += buffers[i].length; + } + return buffer; +} -/* - Find a connection - @id {String or Function} :: function(client, id) {} - return {WebSocketClient} -*/ -WebSocket.prototype.find = function(id) { - var self = this; - var length = self._keys.length; - var isFn = typeof(id) === FUNCTION; +// MIT +// Written by Jozef Gula +// Optimized by Peter Sirka +WebSocketClientProto.$parse = function() { - for (var i = 0; i < length; i++) { - var connection = self.connections[self._keys[i]]; + var self = this; + var current = self.current; - if (!isFn) { - if (connection.id === id) - return connection; - continue; - } + // check end message - if (id(connection, connection.id)) - return connection; - } + // 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; - return null; -}; + // WebSocket - Opcode + current.type = current.buffer[0] & 0x0f; + current.compressed = (current.buffer[0] & 0x40) === 0x40; -/* - Destroy a websocket -*/ -WebSocket.prototype.destroy = function() { - var self = this; + // is final message? + current.final = ((current.buffer[0] & 0x80) >> 7) === 0x01; - if (self.connections === null && self._keys === null) - return self; + // does frame contain mask? + current.isMask = ((current.buffer[1] & 0xfe) >> 7) === 0x01; - self.close(); - self.connections = null; - self._keys = null; - delete self.framework.connections[self.id]; - self.emit('destroy'); - return self; -}; + // data length + var length = U.getMessageLength(current.buffer, F.isLE); + // index for data -/* - Send proxy request - @url {String} - @obj {Object} - @fnCallback {Function} :: optional - return {Controller} -*/ -WebSocket.prototype.proxy = function(url, obj, fnCallback) { + // Solving a problem with The value "-1" is invalid for option "size" + if (length <= 0) + return; - var self = this; - var headers = { - 'X-Proxy': 'total.js' - }; - headers[RESPONSE_HEADER_CONTENTTYPE] = 'application/json'; + var index = current.buffer[1] & 0x7f; + index = ((index === 126) ? 4 : (index === 127 ? 10 : 2)) + (current.isMask ? 4 : 0); - if (typeof(obj) === FUNCTION) { - var tmp = fnCallback; - fnCallback = obj; - obj = tmp; - } + // total message length (data + header) + var mlength = index + length; - utils.request(url, 'POST', obj, function(error, data, code, headers) { + if (mlength > this.length) { + this.close('Frame is too large', 1009); + return; + } - if (!fnCallback) - return; + // Check length of data + if (current.buffer.length < mlength) + return; - if ((headers['content-type'] || '').indexOf('application/json') !== -1) - data = JSON.parse(data); + current.length = mlength; - fnCallback.call(self, error, data, code, headers); + // Not Ping & Pong + if (current.type !== 0x09 && current.type !== 0x0A) { - }, headers); + // does frame contain mask? + if (current.isMask) { + current.mask = Buffer.alloc(4); + current.buffer.copy(current.mask, 0, index - 4, index); + } - return self; -}; + if (current.compressed && this.inflate) { -/* - Internal function - return {WebSocket} -*/ -WebSocket.prototype._refresh = function() { - var self = this; - self._keys = Object.keys(self.connections); - self.online = self._keys.length; - return self; -}; + var buf = Buffer.alloc(length); + current.buffer.copy(buf, 0, index, mlength); -/* - Internal function - @id {String} - return {WebSocket} -*/ -WebSocket.prototype._remove = function(id) { - var self = this; - delete self.connections[id]; - return self; -}; + // does frame contain mask? + if (current.isMask) { + for (var i = 0; i < length; i++) + buf[i] = buf[i] ^ current.mask[i % 4]; + } -/* - Internal function - @client {WebSocketClient} - return {WebSocket} -*/ -WebSocket.prototype._add = function(client) { - var self = this; - self.connections[client._id] = client; - return self; -}; + // Does the buffer continue? + buf.$continue = current.final === false; + this.inflatepending.push(buf); + } else { + current.data = Buffer.alloc(length); + current.buffer.copy(current.data, 0, index, mlength); + } + } -/* - Module caller - @name {String} - return {Module}; -*/ -WebSocket.prototype.module = function(name) { - return this.framework.module(name); + return true; }; -/* - Get a model - @name {String} :: name of model - return {Object}; -*/ -WebSocket.prototype.model = function(name) { - return this.framework.model(name); +WebSocketClientProto.$readbody = function() { + var current = this.current; + var length = current.data.length; + 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; }; -/* - Get a model - @name {String} :: name of model - return {Object}; -*/ -WebSocket.prototype.component = function(name) { +WebSocketClientProto.$decode = function() { - var self = this; - var component = framework.component(name); + var data = this.current.body; + F.stats.performance.message++; - if (component === null) - return ''; + // Buffer + if (this.typebuffer) { + this.container.emit('message', this, data); + return; + } - var length = arguments.length; - var params = []; + switch (this.type) { - for (var i = 1; i < length; i++) - params.push(arguments[i]); + case 1: // BINARY + // this.container.emit('message', this, new Uint8Array(data).buffer); + this.container.emit('message', this, data); + break; - var output = component.render.apply(self, params); - return output; -}; + case 3: // JSON -/* - Render component to string - @name {String} - return {String} -*/ -WebSocket.prototype.helper = function(name) { - var self = this; - var helper = framework.helpers[name] || null; + if (data instanceof Buffer) + data = data.toString(ENCODING); - if (helper === null) - return ''; + if (this.container.encodedecode === true) + data = $decodeURIComponent(data); - var length = arguments.length; - var params = []; + if (data.isJSON()) { + var tmp = F.onParseJSON(data, this.req); + if (tmp !== undefined) + this.container.emit('message', this, tmp); + } + break; - for (var i = 1; i < length; i++) - params.push(arguments[i]); + default: // TEXT + if (data instanceof Buffer) + data = data.toString(ENCODING); + this.container.emit('message', this, this.container.encodedecode === true ? $decodeURIComponent(data) : data); + break; + } - return helper.apply(self, params); + this.current.body = null; }; -/* - Controller functions reader - @name {String} :: name of controller - return {Object}; -*/ -WebSocket.prototype.functions = function(name) { - return (this.framework.controllers[name] || {}).functions; -}; +WebSocketClientProto.parseInflate = function() { + var self = this; -/* - Return database - @name {String} - return {Database}; -*/ -WebSocket.prototype.database = function(name) { - return this.framework.database(name); -}; + if (self.inflatelock) + return; -/* - Resource reader - @name {String} :: filename - @key {String} - return {String}; -*/ -WebSocket.prototype.resource = function(name, key) { - return this.framework.resource(name, key); -}; + var buf = self.inflatepending.shift(); + if (buf) { + self.inflatechunks = []; + self.inflatechunkslength = 0; + self.inflatelock = true; + self.inflate.write(buf); + !buf.$continue && self.inflate.write(Buffer.from(WEBSOCKET_COMPRESS)); + self.inflate.flush(function() { -/* - Log - @arguments {Object array} - return {WebSocket}; -*/ -WebSocket.prototype.log = function() { - var self = this; - self.framework.log.apply(self.framework, arguments); - return self; -}; + if (!self.inflatechunks) + return; -/* - Validation / alias for validate - return {ErrorBuilder} -*/ -WebSocket.prototype.validation = function(model, properties, prefix, name) { - return this.validate(model, properties, prefix, name); -}; + var data = buffer_concat(self.inflatechunks, self.inflatechunkslength); -/* - Validation object - @model {Object} :: object to validate - @properties {String array} : what properties? - @prefix {String} :: prefix for resource = prefix + model name - @name {String} :: name of resource - return {ErrorBuilder} -*/ -WebSocket.prototype.validate = function(model, properties, prefix, name) { + self.inflatechunks = null; + self.inflatelock = false; - var self = this; + if (data.length > self.length) { + self.close('Frame is too large', 1009); + return; + } - var resource = function(key) { - return self.resource(name || 'default', (prefix || '') + key); - }; + if (self.current.body) { + CONCAT[0] = self.current.body; + CONCAT[1] = data; + self.current.body = Buffer.concat(CONCAT); + } else + self.current.body = data; - var error = new builders.ErrorBuilder(resource); - return utils.validate.call(self, model, properties, self.framework.onValidation, error); + !buf.$continue && self.$decode(); + self.parseInflate(); + }); + } }; -/* - Add function to async wait list - @name {String} - @waitingFor {String} :: name of async function - @fn {Function} - return {WebSocket} -*/ -WebSocket.prototype.wait = function(name, waitingFor, fn) { - var self = this; - self.async.wait(name, waitingFor, fn); - return self; -}; +WebSocketClientProto.$onerror = function(err) { -/* - Run async functions - @callback {Function} - return {WebSocket} -*/ -WebSocket.prototype.complete = function(callback) { - var self = this; - return self.complete(callback); -}; + if (this.isClosed) + return; -/* - Add function to async list - @name {String} - @fn {Function} - return {WebSocket} -*/ -WebSocket.prototype.await = function(name, fn) { - var self = this; - self.async.await(name, fn); - return self; + if (REG_WEBSOCKET_ERROR.test(err.stack)) { + this.isClosed = true; + this.$onclose(); + } else + this.container.$events.error && this.container.emit('error', err, this); }; -/* - WebSocketClient - @req {Request} - @socket {Socket} - @head {Buffer} -*/ -function WebSocketClient(req, socket, head) { - - this.handlers = { - ondata: this._ondata.bind(this), - onerror: this._onerror.bind(this), - onclose: this._onclose.bind(this) - }; - - this.container = null; - this._id = null; - this.id = ''; - this.socket = socket; - this.req = req; - this.isClosed = false; - this.errors = 0; - this.buffer = new Buffer(0); - this.length = 0; - this.cookie = req.cookie.bind(req); - - // 1 = raw - not implemented - // 2 = plain - // 3 = JSON - - this.type = 2; - this._isClosed = false; -} - -WebSocketClient.prototype = { - - get protocol() { - return (req.headers['sec-websocket-protocol'] || '').replace(/\s/g, '').split(','); - }, +WebSocketClientProto.$onclose = function() { - get ip() { - return this.req.ip; - }, + if (this._isClosed) + return; - get get() { - return this.req.data.get; - }, + F.stats.performance.online--; + this.isClosed = true; + this._isClosed = true; - get uri() { - return this.req.uri; - }, + if (this.inflate) { + this.inflate.removeAllListeners(); + this.inflate = null; + this.inflatechunks = null; + } - get config() { - return this.container.config; - }, + if (this.deflate) { + this.deflate.removeAllListeners(); + this.deflate = null; + this.deflatechunks = null; + } - get global() { - return this.container.global; - }, + this.container.$remove(this._id); + this.container.$refresh(); + this.container.$events.close && this.container.emit('close', this, this.closecode, this.closemessage); + this.socket.removeAllListeners(); + F.$events['websocket-end'] && EMIT('websocket-end', this.container, this); + F.$events.websocket_end && EMIT('websocket_end', this.container, this); +}; - get session() { - return this.req.session; - }, +/** + * Sends a message + * @param {String/Object} message + * @param {Boolean} raw The message won't be converted e.g. to JSON. + * @return {WebSocketClient} + */ +WebSocketClientProto.send = function(message, raw, replacer) { + + var self = this; + + if (self.isClosed) + return self; + + 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 (self.deflate) { + self.deflatepending.push(Buffer.from(data)); + self.sendDeflate(); + } else + self.socket.write(U.getWebSocketFrame(0, data, 0x01)); + } else if (message) { + if (self.deflate) { + self.deflatepending.push(Buffer.from(message)); + self.sendDeflate(); + } else + self.socket.write(U.getWebSocketFrame(0, new Int8Array(message), 0x02)); + } + + return self; +}; + +WebSocketClientProto.sendDeflate = function() { + var self = this; + + if (self.deflatelock) + return; + + var buf = self.deflatepending.shift(); + if (buf) { + self.deflatechunks = []; + self.deflatechunkslength = 0; + self.deflatelock = true; + self.deflate.write(buf); + self.deflate.flush(function() { + 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(); + } + }); + } +}; - set session(value) { - this.req.session = value; - }, +/** + * Ping message + * @return {WebSocketClient} + */ +WebSocketClientProto.ping = function() { + if (!this.isClosed) { + this.socket.write(U.getWebSocketFrame(0, 'PING', 0x09)); + this.$ping = false; + } + return this; +}; - get user() { - return this.req.user; - }, +/** + * Close connection + * @param {String} message Message. + * @param {Number} code WebSocket code. + * @return {WebSocketClient} + */ +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 self; +}; - set user(value) { - this.req.user = value; - } +/** + * Create a signature for the WebSocket + * @param {Request} req + * @return {String} + */ +WebSocketClientProto.$websocket_key = function(req) { + var sha1 = Crypto.createHash('sha1'); + sha1.update((req.headers['sec-websocket-key'] || '') + SOCKET_HASH); + return sha1.digest('base64'); }; -WebSocketClient.prototype.__proto__ = Object.create(events.EventEmitter.prototype, { - constructor: { - value: WebSocketClient, - enumberable: false - } -}); +// ********************************************************************************* +// ================================================================================= +// Prototypes +// ================================================================================= +// ********************************************************************************* -/* - Internal function - @allow {String Array} :: allow origin - @protocols {String Array} :: allow protocols - @flags {String Array} :: flags - return {Boolean} -*/ -WebSocketClient.prototype.prepare = function(flags, protocols, allow, length, version) { +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; +} - var self = this; +function req_authorizetotal(isAuthorized, user, $) { - flags = flags || []; - protocols = protocols || []; - allow = allow || []; + // @isAuthorized "null" for callbacks(err, user) + // @isAuthorized "true" + // @isAuthorized "object" is as user but "user" must be "undefined" - self.length = length; + var req = $.req; + var roles = req.flagslength !== req.flags.length; - var origin = self.req.headers['origin'] || ''; + if (roles) { + req.$flags += req.flags.slice(req.flagslength).join(''); + req.$roles = true; + } - if (allow.length > 0) { + req.flagslength = undefined; - if (allow.indexOf('*') === -1) { - for (var i = 0; i < allow.length; i++) { - if (origin.indexOf(allow[i]) === -1) - return false; - } - } + 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; + } - } else { + req.isAuthorized = isAuthorized; + req.$total_authorize(isAuthorized, user, roles); +} - if (origin.indexOf(self.req.headers.host) === -1) - return false; - } +function extend_request(PROTO) { + + PROTOREQ = PROTO; + + Object.defineProperty(PROTO, 'ip', { + get: function() { + if (this._ip) + return this._ip; + + // x-forwarded-for: client, proxy1, proxy2, ... + var proxy = this.headers['x-forwarded-for']; + if (proxy) + this._ip = proxy.split(',', 1)[0] || this.connection.remoteAddress; + else if (!this._ip) + this._ip = this.connection.remoteAddress; + + return this._ip; + } + }); + + Object.defineProperty(PROTO, 'query', { + get: function() { + !this._querydata && F.$onParseQueryUrl(this); + return this._querydata; + }, + set: function(value) { + this._querydata = value; + } + }); + + Object.defineProperty(PROTO, 'subdomain', { + get: function() { + if (this._subdomain) + return this._subdomain; + 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; + } + }); + + Object.defineProperty(PROTO, 'host', { + get: function() { + return this.headers['host']; + } + }); + + Object.defineProperty(PROTO, 'split', { + get: function() { + return this.$path ? this.$path : this.$path = framework_internal.routeSplit(this.uri.pathname, true); + } + }); + + Object.defineProperty(PROTO, 'secured', { + get: function() { + return this.uri.protocol === 'https:' || this.uri.protocol === 'wss:'; + } + }); + + Object.defineProperty(PROTO, 'language', { + get: function() { + if (!this.$language) + this.$language = (((this.headers['accept-language'] || '').split(';')[0] || '').split(',')[0] || '').toLowerCase(); + return this.$language; + }, + set: function(value) { + this.$language = value; + } + }); + + 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) + this.$mobile = REG_MOBILE.test(this.headers['user-agent']); + return this.$mobile; + } + }); + + Object.defineProperty(PROTO, 'robot', { + get: function() { + if (this.$robot === undefined) + this.$robot = REG_ROBOT.test(this.headers['user-agent']); + return this.$robot; + } + }); + + /** + * Signature request (user-agent + ip + referer + current URL + custom key) + * @param {String} key Custom key. + * @return {Request} + */ + PROTO.signature = function(key) { + return F.encrypt((this.headers['user-agent'] || '') + '#' + this.ip + '#' + this.url + '#' + (key || ''), 'request-signature', false); + }; + + PROTO.localize = function() { + F.onLocale && (this.$language = F.onLocale(this, this.res, this.isStaticFile)); + return this.$language; + }; + + /** + * Disable HTTP cache for current request + * @return {Request} + */ + PROTO.noCache = PROTO.nocache = function() { + this.res && this.res.noCache(); + return this; + }; + + PROTO.useragent = function(structured) { + var key = structured ? '$ua2' : '$ua'; + return this[key] ? this[key] : this[key] = (this.headers['user-agent'] || '').parseUA(structured); + }; + + /** + * Read a cookie from current request + * @param {String} name Cookie name. + * @return {String} Cookie value (default: '') + */ + PROTO.cookie = function(name) { + + if (this.cookies) + return $decodeURIComponent(this.cookies[name] || ''); + + var cookie = this.headers['cookie']; + if (!cookie) + return ''; + + this.cookies = {}; + + var arr = cookie.split(';'); + + for (var i = 0, length = arr.length; i < length; i++) { + var line = arr[i].trim(); + var index = line.indexOf('='); + if (index !== -1) + this.cookies[line.substring(0, index)] = line.substring(index + 1); + } + + return $decodeURIComponent(this.cookies[name] || ''); + }; + + /** + * Read authorization header + * @return {Object} + */ + PROTO.authorization = function() { + + var authorization = this.headers['authorization']; + if (!authorization) + return HEADERS.authorization; + + var result = { user: '', password: '', empty: true }; + + try { + 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; + } catch (e) {} + + return result; + }; + + /** + * Authorization for custom delegates + * @param {Function(err, userprofile, isAuthorized)} callback + * @return {Request} + */ + PROTO.authorize = function(callback) { + + var req = this; + + if (!F.onAuthorize) { + callback(null, null, false); + return req; + } + + if (F.onAuthorize.$newversion) { + req.authorizecallback = callback; + F.onAuthorize(req, req.res, req.flags, req_authorizecallback); + return req; + } + + 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 = true; + } + + req.isAuthorized = isAuthorized; + callback(null, user, isAuthorized); + }); + + return this; + }; + + /** + * Clear all uplaoded files + * @private + * @param {Boolean} isAuto + * @return {Request} + */ + PROTO.clear = function(isAuto) { + + var self = this; + var files = self.files; + + if (!files || (isAuto && self._manual)) + return self; + + self.body = null; + self.query = null; + self.cookies = null; + + var length = files.length; + if (!length) + return self; + + var arr = []; + for (var i = 0; i < length; i++) + files[i].rem && arr.push(files[i].path); + + F.unlink(arr); + self.files = null; + return self; + }; + + /** + * Get host name from URL + * @param {String} path Additional path. + * @return {String} + */ + PROTO.hostname = function(path) { + + var self = this; + var uri = self.uri; + + if (path && path[0] !== '/') + path = '/' + path; + + 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; + if (this.controller) { + this.controller.res.controller = null; + this.controller = null; + } + }; + + PROTO.$total_file = function() { + var h = this.method[0]; + if (h === 'G' || h === 'H') + this.$total_endfile(); + else + this.on('end', this.$total_endfile); + }; + + PROTO.$total_multipart = function(header) { + F.stats.request.upload++; + this.$total_route = F.lookup(this, this.uri.pathname, this.flags, 0); + this.$total_header = header; + if (this.$total_route) { + F.path.verify('temp'); + framework_internal.parseMULTIPART(this, header, this.$total_route, CONF.directory_temp); + } else + this.$total_status(404); + }; + + PROTO.$total_urlencoded = function() { + this.$total_route = F.lookup(this, this.uri.pathname, this.flags, 0); + if (this.$total_route) { + this.buffer_has = true; + this.buffer_exceeded = false; + this.on('data', this.$total_parsebody); + this.$total_end(); + } else + this.$total_status(404); + }; + + PROTO.$total_status = function(status) { + + if (status == null) + F.stats.request.blocked++; + else + F.stats.request['error' + status]++; + + F.reqstats(false, false); + this.res.writeHead(status); + this.res.end(U.httpStatus(status)); + F.$events['request-end'] && EMIT('request-end', this, this.res); + F.$events.request_end && EMIT('request_end', this, this.res); + this.clear(true); + }; + + PROTO.$total_end = function() { + var h = this.method[0]; + if (h === 'G' || h === 'H' || h === 'O') { + if (this.$total_route && this.$total_route.schema) + this.$total_schema = true; + this.buffer_data = null; + this.$total_prepare(); + } else + this.on('end', this.$total_end2); + }; + + PROTO.$total_execute = function(status, isError) { + + var route = this.$total_route; + var res = this.res; + + if (isError || !route) { + 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) { + if (status === 400 && this.$total_exception instanceof framework_builders.ErrorBuilder) { + F.stats.response.errorBuilder++; + this.$language && this.$total_exception.setResource(this.$language); + res.options.body = this.$total_exception.output(true); + res.options.code = this.$total_exception.status; + res.options.type = this.$total_exception.contentType; + res.$text(); + } else { + + 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(); + } + return; + } + + var name = route.controller; + + if (route.isMOBILE_VARY) + this.$mobile = true; + + if (route.currentViewDirectory === undefined) + route.currentViewDirectory = name && name[0] !== '#' && name !== 'default' && name !== 'unknown' ? '/' + name + '/' : ''; + + var controller = new Controller(name, this, res, route.currentViewDirectory); + + controller.isTransfer = this.$total_transfer; + controller.exception = this.$total_exception; + this.controller = controller; + + if (!this.$total_canceled && route.timeout) { + this.$total_timeout && clearTimeout(this.$total_timeout); + this.$total_timeout = setTimeout(subscribe_timeout, route.timeout, this); + } + + route.isDELAY && res.writeContinue(); + + if (this.$total_schema) + this.body.$$controller = controller; + + if (route.middleware) + async_middleware(0, this, res, route.middleware, subscribe_timeout_middleware, route.options, controller); + else + this.$total_execute2(); + }; + + PROTO.$total_execute2 = function() { + + var name = this.$total_route.controller; + var controller = this.controller; + + try { + + if (F.onTheme) + controller.themeName = F.onTheme(controller); + + if (controller.isCanceled) + return; + + var ctrlname = '@' + name; + 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 (!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) { + 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); + } + + } catch (err) { + F.error(err, name, this.uri); + this.$total_exception = err; + this.$total_route = F.lookup(this, '#500', EMPTYARRAY, 0); + this.$total_execute(500, true); + } + }; + + PROTO.$total_parsebody = function(chunk) { + + if (this.buffer_exceeded) + return; + + if (!this.buffer_exceeded) { + CONCAT[0] = this.buffer_data; + CONCAT[1] = chunk; + this.buffer_data = Buffer.concat(CONCAT); + } + + if ((this.buffer_data.length / 1024) < this.$total_route.length) + return; + + this.buffer_exceeded = true; + this.buffer_data = Buffer.alloc(0); + }; + + PROTO.$total_cancel = function() { + F.stats.response.timeout++; + clearTimeout(this.$total_timeout); + if (!this.controller) + return; + this.controller.isTimeout = true; + this.controller.isCanceled = true; + this.$total_route = F.lookup(this, '#408', EMPTYARRAY, 0); + this.$total_execute(408, true); + }; + + PROTO.$total_validate = function(route, next, code) { + + var self = this; + self.$total_schema = false; + + if (!self.$total_route.schema) + return next(self, code); + + 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; + } else { + F.stats.request.schema++; + self.body = body; + self.$total_schema = true; + next(self, code); + } + }); + }; + + PROTO.$total_authorize = function(isLogged, user, roles) { + + var membertype = isLogged ? 1 : 2; + var code = this.buffer_exceeded ? 431 : 401; + + this.$flags += membertype; + user && (this.user = user); + + if (this.$total_route && this.$total_route.isUNIQUE && !roles && (!this.$total_route.MEMBER || this.$total_route.MEMBER === membertype)) { + if (code === 401 && this.$total_schema) + this.$total_validate(this.$total_route, subscribe_validate_callback, code); + else + this.$total_execute(code, true); + } else { + var route = F.lookup(this, this.buffer_exceeded ? '#431' : this.uri.pathname, this.flags, this.buffer_exceeded ? 0 : membertype); + var status = this.$isAuthorized ? 404 : 401; + var code = this.buffer_exceeded ? 431 : status; + !route && (route = F.lookup(this, '#' + status, EMPTYARRAY, 0)); + + this.$total_route = route; + + if (this.$total_route && this.$total_schema) + this.$total_validate(this.$total_route, subscribe_validate_callback, code); + else + this.$total_execute(code); + } + }; + + PROTO.$total_end2 = function() { + + var route = this.$total_route; + + if (this.buffer_exceeded) { + route = F.lookup(this, '#431', EMPTYARRAY, 0); + this.buffer_data = null; + if (route) { + this.$total_route = route; + this.$total_execute(431, true); + } else + this.res.throw431(); + return; + } + + if (this.buffer_data && (!route || !route.isBINARY)) + this.buffer_data = this.buffer_data.toString(ENCODING); + + if (!this.buffer_data) { + if (route && route.schema) + this.$total_schema = true; + this.buffer_data = null; + this.$total_prepare(); + return; + } + + if (route.isXML) { + + if (this.$type !== 2) { + this.$total_400('Invalid "Content-Type".'); + this.buffer_data = null; + return; + } + + try { + F.$onParseXML(this); + this.buffer_data = null; + this.$total_prepare(); + } catch (err) { + F.error(err, null, this.uri); + this.$total_500(err); + } + + return; + } + + if (route.isRAW) { + this.body = this.buffer_data; + this.buffer_data = null; + this.$total_prepare(); + return; + } + + if (!this.$type) { + this.buffer_data = null; + this.$total_400('Invalid "Content-Type".'); + return; + } + + if (this.$type === 1) { + try { + F.$onParseJSON(this); + this.buffer_data = null; + } catch (e) { + this.$total_400('Invalid JSON data.'); + return; + } + } 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; + this.$total_prepare(); + }; + + PROTO.$total_endfile = function() { + + var req = this; + var res = this.res; + + if (!F._length_files) + return res.continue(); + + for (var i = 0; i < F.routes.files.length; i++) { + + var file = F.routes.files[i]; + // try { + + if (file.extensions && !file.extensions[req.extension]) + continue; + + if (file.url) { + var skip = false; + var length = file.url.length; + + if (!file.wildcard && !file.fixedfile && length !== req.path.length - 1) + continue; + + for (var j = 0; j < length; j++) { + if (file.url[j] === req.path[j]) + continue; + skip = true; + break; + } + + if (skip) + continue; + + } else if (file.onValidate && !file.onValidate(req, res, true)) + continue; + + if (file.middleware) + req.$total_endfilemiddleware(file); + else + file.execute(req, res, false); + + return; + } + + res.continue(); + }; + + PROTO.$total_endfilemiddleware = function(file) { + this.$total_filemiddleware = file; + async_middleware(0, this, this.res, file.middleware, total_endmiddleware, file.options); + }; + + PROTO.$total_400 = function(problem) { + this.$total_route = F.lookup(this, '#400', EMPTYARRAY, 0); + this.$total_exception = problem; + 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; + this.$total_execute(500, true); + }; + + 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) { + + 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(''); + 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 = true; + } + + req.isAuthorized = isAuthorized; + 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); + if (!req.$total_route) + 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, 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')); + }); + }; +} - if (protocols.length > 0) { - for (var i = 0; i < protocols.length; i++) { - if (self.protocol.indexOf(protocols[i]) === -1) - return false; - } - } +function total_endmiddleware(req) { - if (SOCKET_ALLOW_VERSION.indexOf(utils.parseInt(self.req.headers['sec-websocket-version'])) === -1) - return false; + if (req.total_middleware) + req.total_middleware = null; - self.socket.write(new Buffer(SOCKET_RESPONSE.format('total.js v' + version, self._request_accept_key(self.req)), 'binary')); + try { + 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(); + } +} - self._id = (self.ip || '').replace(/\./g, '') + utils.GUID(20); - self.id = self._id; +function extend_response(PROTO) { + + PROTORES = PROTO; + + /** + * Add a cookie into the response + * @param {String} name + * @param {Object} value + * @param {Date/String} expires + * @param {Object} options Additional options. + * @return {Response} + */ + PROTO.cookie = function(name, value, expires, options) { + + var self = this; + + if (self.headersSent || self.success) + return; + + var cookiename = name + '='; + var builder = [cookiename + value]; + var type = typeof(expires); + + if (expires && !U.isDate(expires) && type === 'object') { + options = expires; + expires = options.expires || options.expire || null; + } + + if (type === 'string') + expires = expires.parseDateExpiration(); + + if (!options) + options = {}; + + options.path = options.path || '/'; + 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'); + + 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 + 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; + }; + + /** + * Disable HTTP cache for current response + * @return {Response} + */ + PROTO.noCache = PROTO.nocache = function() { + var self = this; + + if (self.$nocache) + return self; + + if (self.req) { + delete self.req.headers['if-none-match']; + delete self.req.headers['if-modified-since']; + } + + if (self.getHeader(HEADER_CACHE)) { + self.removeHeader(HEADER_CACHE); + self.removeHeader('Expires'); + self.removeHeader('Etag'); + self.removeHeader('Last-Modified'); + self.setHeader(HEADER_CACHE, 'private, no-cache, no-store, max-age=0'); + self.setHeader('Expires', -1); + } + + self.$nocache = true; + return self; + }; + + // For express middleware + PROTO.status = function(code) { + this.options.code = code; + return this; + }; + + // For express middleware + PROTO.send = function(code, body, type) { + + if (this.headersSent) + return this; + + this.controller && this.req.$total_success(); + + if (code instanceof Buffer) { + // express.js static file + if (!body && !type) { + this.end(code); + return this; + } + } + + var res = this; + var req = this.req; + var contentType = type; + var isHEAD = req.method === 'HEAD'; + + if (body === undefined) { + body = code; + code = res.$statuscode || 200; + } + + switch (typeof(body)) { + case 'string': + if (!contentType) + contentType = 'text/html'; + break; + + case 'number': + if (!contentType) + contentType = 'text/plain'; + body = U.httpStatus(body); + break; + + case 'boolean': + case 'object': + if (!isHEAD) { + if (body instanceof framework_builders.ErrorBuilder) { + var json = body.output(true); + if (body.status !== 200) + res.options.code = body.status; + if (body.contentType) + contentType = body.contentType; + else + contentType = CT_JSON; + body = json; + F.stats.response.errorBuilder++; + } else + body = JSON.stringify(body); + !contentType && (contentType = CT_JSON); + } + break; + } + + var accept = req.headers['accept-encoding'] || ''; + var headers = {}; + + headers[HEADER_CACHE] = 'private, no-cache, no-store, max-age=0'; + headers['Vary'] = 'Accept-Encoding' + (req.$mobile ? ', User-Agent' : ''); + + if ((/text|application/).test(contentType)) + contentType += '; charset=utf-8'; + + headers[HEADER_TYPE] = contentType; + res.$custom(); + + if (!accept && isGZIP(req)) + accept = 'gzip'; + + var compress = CONF.allow_gzip && accept.indexOf('gzip') !== -1; + if (isHEAD) { + compress && (headers['Content-Encoding'] = 'gzip'); + res.writeHead(200, headers); + res.end(); + return res; + } + + if (!compress) { + res.writeHead(code, headers); + res.end(body, ENCODING); + return res; + } + + var buffer = Buffer.from(body); + Zlib.gzip(buffer, function(err, data) { + + if (err) { + res.writeHead(code, headers); + res.end(body, ENCODING); + } else { + headers['Content-Encoding'] = 'gzip'; + res.writeHead(code, headers); + res.end(data, ENCODING); + } + }); + + return res; + }; + + /** + * Response a custom content + * @param {Number} code + * @param {String} body + * @param {String} type + * @param {Boolean} compress Disallows GZIP compression. Optional, default: true. + * @param {Object} headers Optional, additional headers. + * @return {Response} + */ + PROTO.content = function(code, body, type, compress, headers) { + + if (typeof(compress) === 'object') { + var tmp = headers; + headers = compress; + compress = tmp; + } + + var res = this; + res.options.code = code; + 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; + }; + + /** + * Response redirect + * @param {String} url + * @param {Boolean} permanent Optional, default: false. + * @return {Framework} + */ + PROTO.redirect = function(url, permanent) { + this.options.url = url; + permanent && (this.options.permanent = permanent); + this.$redirect(); + return this; + }; + + /** + * Responds with a file + * @param {String} filename + * @param {String} download Optional, a download name. + * @param {Object} headers Optional, additional headers. + * @param {Function} done Optional, callback. + * @return {Framework} + */ + PROTO.file = function(filename, download, headers, callback) { + this.options.filename = filename; + headers && (this.options.headers = headers); + callback && (this.options.callback = callback); + download && (this.options.download = download); + 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 + * @param {Stream} stream + * @param {String} download Optional, a download name. + * @param {Object} headers Optional, additional headers. + * @param {Function} done Optional, callback. + * @return {Framework} + */ + PROTO.stream = function(type, stream, download, headers, callback, nocompress) { + var res = this; + res.options.type = type; + res.options.stream = stream; + download && (res.options.download = download); + headers && (res.options.headers = headers); + callback && (res.options.callback = callback); + res.options.compress = nocompress ? false : true; + res.$stream(); + return res; + }; + + 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; + download && (this.options.download = download); + headers && (this.options.headers = headers); + this.$binary(); + return this; + }; + + PROTO.proxy = function(url, headers, timeout, callback) { + + OBSOLETE('res.proxy()', 'You need to use controller.proxy()'); + + var res = this; + + if (res.success || res.headersSent) + return res; + + callback && (res.options.callback = callback); + headers && (res.options.headers = headers); + timeout && (res.options.timeout = timeout); + + U.resolve(url, function(err, uri) { + + var headers = {}; + + headers[HEADER_CACHE] = 'private, no-cache, no-store, max-age=0'; + res.options.headers && U.extend_headers2(headers, res.options.headers); + + 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 = CONF.allow_gzip && (res.req.headers['accept-encoding'] || '').lastIndexOf('gzip') !== -1; + + var client = connection.get(options, function(response) { + + if (res.success || res.headersSent) + return; + + var contentType = response.headers['content-type']; + var isGZIP = (response.headers['content-encoding'] || '').lastIndexOf('gzip') !== -1; + var compress = !isGZIP && gzip && (contentType.indexOf('text/') !== -1 || contentType.lastIndexOf('javascript') !== -1 || contentType.lastIndexOf('json') !== -1); + var attachment = response.headers['content-disposition'] || ''; + + attachment && res.setHeader('Content-Disposition', attachment); + res.setHeader(HEADER_TYPE, contentType); + res.setHeader('Vary', 'Accept-Encoding' + (res.req.$mobile ? ', User-Agent' : '')); + + res.on('error', function() { + response.close(); + response_end(res); + }); + + if (compress) { + res.setHeader('Content-Encoding', 'gzip'); + response.pipe(Zlib.createGzip(GZIPSTREAM)).pipe(res); + return; + } + + if (isGZIP && !gzip) + response.pipe(Zlib.createGunzip()).pipe(res); + else + response.pipe(res); + }); + + timeout && client.setTimeout(timeout, function() { + res.throw408(); + }); + + client.on('close', function() { + if (!res.success) { + F.stats.response.pipe++; + response_end(res); + } + }); + }); + + return res; + }; + + /** + * Responds with an image + * @param {String or Stream} filename + * @param {String} make + * @param {Object} headers Optional, additional headers. + * @param {Function} callback Optional. + * @return {Framework} + */ + 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); + + if (typeof(filename) === 'object') + res.options.stream = filename; + else + res.options.filename = filename; + + res.$image(); + return res; + }; + + PROTO.image_nocache = function(filename, make, headers, callback) { + this.options.cache = false; + return this.image(filename, make, headers, callback); + }; + + /** + * Response JSON + * @param {Object} obj + * @return {Response} + */ + 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; + var req = res.req; + + callback && (res.options.callback = callback); + + if (res.success || res.headersSent) + return res; + + 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]) { + if (!F.routes.filesfallback || !F.routes.filesfallback(req, res)) + res.throw404(); + return res; + } + + var canresize = false; + var filename = null; + var name = req.uri.pathname; + + 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 (!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 === CONF.static_url_components + req.extension) { + res.noCompress = true; + 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; + res.$file(); + return res; + } + + if (!resizer.ishttp) { + res.options.cache = resizer.cache; + res.options.make = resizer.fn; + res.options.filename = filename; + res.$image(); + return res; + } + + if (F.temporary.processing[req.uri.pathname]) { + setTimeout($continue_timeout, 500, res); + return res; + } + + var tmp = F.path.temp(req.$key); + if (F.temporary.path[req.$key]) { + res.options.filename = req.uri.pathname; + res.$file(); + return res; + } + + F.temporary.processing[req.uri.pathname] = true; + + U.download(name, FLAGS_DOWNLOAD, function(err, response) { + var writer = Fs.createWriteStream(tmp); + response.pipe(writer); + CLEANUP(writer, function() { + + delete F.temporary.processing[req.uri.pathname]; + var contentType = response.headers['content-type']; + + if (response.statusCode !== 200 || !contentType || !contentType.startsWith('image/')) { + if (!F.routes.filesfallback || !F.routes.filesfallback(req, res)) + res.throw404(); + return; + } + + res.options.cache = resizer.cache; + res.options.filename = tmp; + res.options.maker = resizer.fn; + res.$image(); + }); + }); + + return res; + }; + + PROTO.$file = function() { + + // res.options.filename + // res.options.code + // res.options.callback + // res.options.headers + // res.options.download + + var res = this; + var options = res.options; + + if (res.headersSent) + return res; + + 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)); + + // "$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 && options.filename[0] === '@') + options.filename = F.path.package(options.filename.substring(1)); + + var name = F.temporary.path[req.$key]; + var index; + + if (!req.extension) { + req.$key && (req.extension = U.getExtension(req.$key)); + if (!req.extension && name) { + req.extension = U.getExtension(name); + index = req.extension.lastIndexOf(';'); + index !== -1 && (req.extension = req.extension.substring(0, index)); + } + !req.extension && options.filename && (req.extension = U.getExtension(options.filename)); + } + + if (name && RELEASE && !res.$nocache && req.headers['if-modified-since'] === name[2]) { + $file_notmodified(res, name); + return res; + } + + if (name === undefined) { + + if (F.temporary.processing[req.$key]) { + if (req.processing > CONF.default_request_timeout) { + res.throw408(); + } else { + req.processing += 500; + setTimeout($file_processing, 500, res); + } + return res; + } + + // waiting + F.temporary.processing[req.$key] = true; + compile_check(res); + return res; + } + + var contentType = U.getContentType(req.extension); + var accept = req.headers['accept-encoding'] || ''; + var headers; + + !accept && isGZIP(req) && (accept = 'gzip'); + + 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' && !RESPONSENOCACHE[req.extension]; + + if (canCache) { + if (compress) + headers = range ? HEADERS.file_release_compress_range : HEADERS.file_release_compress; + else + headers = range ? HEADERS.file_release_range : HEADERS.file_release; + } else { + if (compress) + headers = range ? HEADERS.file_debug_compress_range : HEADERS.file_debug_compress; + else + headers = range ? HEADERS.file_debug_range : HEADERS.file_debug; + } + + if (req.$mobile) + headers.Vary = 'Accept-Encoding, User-Agent'; + else + headers.Vary = 'Accept-Encoding'; + + headers[HEADER_TYPE] = contentType; + if (REG_TEXTAPPLICATION.test(contentType)) + headers[HEADER_TYPE] += '; charset=utf-8'; + + if (canCache && !res.getHeader('Expires')) { + headers.Expires = DATE_EXPIRES; + } else if (headers.Expires && RELEASE) + delete headers.Expires; + + if (res.options.headers) + headers = U.extend_headers(headers, res.options.headers); + + if (res.options.download) { + var encoded = encodeURIComponent(res.options.download); + headers['Content-Disposition'] = 'attachment; ' + (REG_UTF8.test(res.options.download) ? 'filename*=utf-8\'\'' + encoded : ('filename="' + encoded + '"')); + } else if (headers['Content-Disposition']) + delete headers['Content-Disposition']; + + if (res.getHeader('Last-Modified')) + delete headers['Last-Modified']; + else if (!res.options.lastmodified) + headers['Last-Modified'] = name[2]; + + headers.Etag = ETAG + CONF.etag_version; + + if (range) { + $file_range(name[0], range, headers, res); + return res; + } + + // (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); + res.end(); + response_end(res); + } else if (compress) { + + if (name[4]) + headers[HEADER_LENGTH] = name[4]; + else + delete headers[HEADER_LENGTH]; + + res.writeHead(res.options.code || 200, headers); + fsStreamRead(name[3], undefined, $file_nocompress, res); + } else { + res.writeHead(res.options.code || 200, headers); + fsStreamRead(name[0], undefined, $file_nocompress, res); + } + }; + + PROTO.$redirect = function() { + + // res.options.permanent + // res.options.url + + var res = this; + + if (res.headersSent) + return res; + + HEADERS.redirect.Location = res.options.url; + res.writeHead(res.options.permanent ? 301 : 302, HEADERS.redirect); + res.end(); + response_end(res); + F.stats.response.redirect++; + return res; + }; + + PROTO.$binary = function() { + + // res.options.callback + // res.options.code + // res.options.encoding + // res.options.download + // res.options.type + // res.options.body + // res.options.headers + + var res = this; + + if (res.headersSent) + return res; + + var req = res.req; + var options = res.options; + + /* + if (options.type.lastIndexOf('/') === -1) + options.type = U.getContentType(options.type); + */ + + var accept = req.headers['accept-encoding'] || ''; + !accept && isGZIP(req) && (accept = 'gzip'); + + 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' : ''); + + if (options.download) + headers['Content-Disposition'] = 'attachment; filename=' + encodeURIComponent(options.download); + else if (headers['Content-Disposition']) + delete headers['Content-Disposition']; + + headers[HEADER_TYPE] = options.type; + + if (options.headers) + headers = U.extend_headers(headers, options.headers); + + F.stats.response.binary++; + + if (req.method === 'HEAD') { + res.writeHead(options.code || 200, headers); + res.end(); + response_end(res); + } else if (compress) { + res.writeHead(options.code || 200, headers); + Zlib.gzip(!options.encoding || options.encoding === 'binary' ? options.body : options.body.toString(options.encoding), (err, buffer) => res.end(buffer)); + response_end(res); + } else { + res.writeHead(options.code || 200, headers); + res.end(!options.encoding || options.encoding === 'binary' ? options.body : options.body.toString(options.encoding)); + response_end(res); + } + + return res; + }; + + PROTO.$stream = function() { + + // res.options.filename + // res.options.options + // res.options.callback + // res.options.code + // res.options.stream + // res.options.type + // res.options.compress + + var res = this; + var req = res.req; + var options = res.options; + + if (res.headersSent) + return res; + + /* + if (options.type.lastIndexOf('/') === -1) + options.type = U.getContentType(options.type); + */ + + var accept = req.headers['accept-encoding'] || ''; + !accept && isGZIP(req) && (accept = 'gzip'); + + var compress = (options.compress === undefined || options.compress) && CONF.allow_gzip && COMPRESSION[options.type] && accept.indexOf('gzip') !== -1; + var headers; + + if (RELEASE) { + if (compress) + headers = HEADERS.stream_release_compress; + else + headers = HEADERS.stream_release; + } else { + if (compress) + headers = HEADERS.stream_debug_compress; + else + headers = HEADERS.stream_debug; + } + + headers.Vary = 'Accept-Encoding' + (req.$mobile ? ', User-Agent' : ''); + + if (RELEASE) { + headers.Expires = DATE_EXPIRES; + headers['Last-Modified'] = 'Mon, 01 Jan 2001 08:00:00 GMT'; + } + + if (options.download) + headers['Content-Disposition'] = 'attachment; filename=' + encodeURIComponent(options.download); + else if (headers['Content-Disposition']) + delete headers['Content-Disposition']; + + headers[HEADER_TYPE] = options.type; + + if (options.headers) + headers = U.extend_headers(headers, options.headers); + + F.stats.response.stream++; + + 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; + } + + if (compress) { + res.writeHead(options.code || 200, headers); + res.on('error', () => options.stream.close()); + options.stream.pipe(Zlib.createGzip(GZIPSTREAM)).pipe(res); + framework_internal.onFinished(res, () => framework_internal.destroyStream(options.stream)); + response_end(res); + } else { + res.writeHead(options.code || 200, headers); + framework_internal.onFinished(res, () => framework_internal.destroyStream(options.stream)); + options.stream.pipe(res); + response_end(res); + } + + return res; + }; + + PROTO.$image = function() { + + // res.options.filename + // res.options.stream + // res.options.options + // res.options.callback + // res.options.code + // res.options.cache + // res.options.headers + // res.options.make = function(image, res) + // res.options.persistent + + var res = this; + var options = res.options; + + if (options.cache === false) + return $image_nocache(res); + + var req = this.req; + if (!req.$key) + req.$key = createTemporaryKey(req); + + var key = req.$key; + + if (F.temporary.notfound[key]) { + DEBUG && (F.temporary.notfound[key] = undefined); + if (!F.routes.filesfallback || !F.routes.filesfallback(req, res)) + res.throw404(); + return res; + } + + var name = F.temporary.path[key]; + + if (options.filename && options.filename[0] === '@') + options.filename = F.path.package(options.filename.substring(1)); + + if (name !== undefined) { + res.$file(); + return res; + } + + if (F.temporary.processing[key]) { + if (req.processing > CONF.default_request_timeout) { + res.throw408(); + } else { + req.processing += 500; + setTimeout($image_processing, 500, res); + } + return res; + } + + var plus = F.id ? 'i-' + F.id + '_' : ''; + + 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) + fsFileExists(options.name, $image_stream, res); + else + fsFileExists(options.filename, $image_filename, res); + + return res; + }; + + PROTO.$custom = function() { + F.stats.response.custom++; + response_end(this); + return this; + }; + + PROTO.$text = function() { + + // res.options.type + // res.options.body + // res.options.code + // res.options.headers + // res.options.callback + // res.options.compress + // res.options.encoding + + var res = this; + var req = res.req; + var options = res.options; + + 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 = CONF.allow_gzip && (options.compress === undefined || options.compress) ? accept.indexOf('gzip') !== -1 : false; + var headers; + + if (req.$mobile) + headers = gzip ? HEADERS.content_mobile_release : HEADERS.content_mobile; + else + headers = gzip ? HEADERS.content_compress : HEADERS.content; + + if (REG_TEXTAPPLICATION.test(options.type)) + options.type += '; charset=utf-8'; + + headers[HEADER_TYPE] = options.type; + + if (options.headers) + headers = U.extend_headers(headers, options.headers); + + if (req.method === 'HEAD') { + res.writeHead(options.code || 200, headers); + res.end(); + } else { + if (gzip) { + res.writeHead(options.code || 200, headers); + 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); + } + } + + response_end(res); + return res; + }; + + PROTO.throw400 = function(problem) { + this.options.code = 400; + problem && (this.options.problem = problem); + return this.$throw(); + }; + + PROTO.throw401 = function(problem) { + this.options.code = 401; + problem && (this.options.problem = problem); + return this.$throw(); + }; + + PROTO.throw403 = function(problem) { + this.options.code = 403; + problem && (this.options.problem = problem); + return this.$throw(); + }; + + PROTO.throw404 = function(problem) { + this.options.code = 404; + problem && (this.options.problem = problem); + return this.$throw(); + }; + + PROTO.throw408 = function(problem) { + this.options.code = 408; + problem && (this.options.problem = problem); + return this.$throw(); + }; + + PROTO.throw409 = function(problem) { + this.options.code = 409; + problem && (this.options.problem = problem); + return this.$throw(); + }; + + PROTO.throw431 = function(problem) { + this.options.code = 431; + problem && (this.options.problem = problem); + return this.$throw(); + }; + + PROTO.throw500 = function(error) { + error && F.error(error, null, this.req.uri); + this.options.code = 500; + this.options.body = U.httpStatus(500) + error ? prepare_error(error) : ''; + return this.$throw(); + }; + + PROTO.throw501 = function(problem) { + this.options.code = 501; + problem && (this.options.problem = problem); + return this.$throw(); + }; + + PROTO.$throw = function() { + + // res.options.code + // res.options.body + // res.options.problem + + var res = this; + + if (res.success || res.headersSent) + 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); + + if (req.method === 'HEAD') { + res.writeHead(res.options.code || 501, res.options.headers || HEADERS.responseCode); + res.end(); + 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); + } + + F.$events[key] && EMIT(key, req, res, res.options.problem); + return res; + }; +} - return true; -}; +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); + } +} -/* - Internal function - @container {WebSocket} - return {WebSocketClient} -*/ -WebSocketClient.prototype.upgrade = function(container) { +function $continue_timeout(res) { + res.continue(); +} - var self = this; - self.container = container; +function $file_processing(res) { + res.$file(); +} - //self.socket.setTimeout(0); - //self.socket.setNoDelay(true); - //self.socket.setKeepAlive(true, 0); +function $file_notmodified(res, name) { + var req = res.req; + var headers = HEADERS.file_lastmodified; + + if (res.getHeader('Last-Modified')) + delete headers['Last-Modified']; + else + headers['Last-Modified'] = name instanceof Array ? name[2] : name; + + if (res.getHeader('Expires')) + delete headers.Expires; + else + headers.Expires = DATE_EXPIRES; + + if (res.getHeader('ETag')) + delete headers.Etag; + else + headers.Etag = ETAG + CONF.etag_version; + + headers[HEADER_TYPE] = U.getContentType(req.extension); + res.writeHead(304, headers); + res.end(); + F.stats.response.notModified++; + response_end(res); +} - self.socket.on('data', self.handlers.ondata); - self.socket.on('error', self.handlers.onerror); - self.socket.on('close', self.handlers.onclose); - self.socket.on('end', self.handlers.onclose); +function $file_nocompress(stream, next, res) { - self.container._add(self); - self.container._refresh(); + stream.pipe(res); - self.container.framework.emit('websocket-begin', self.container, self); - self.container.emit('open', self); + framework_internal.onFinished(res, function() { + next(); + framework_internal.destroyStream(stream); + }); - return self; -}; + response_end(res); +} -/* - MIT - Written by Jozef Gula - --------------------- - Internal handler - @data {Buffer} -*/ -WebSocketClient.prototype._ondata = function(data) { +function $file_range(name, range, headers, res) { - var self = this; + var arr = range.replace(REG_RANGE, '').split('-'); + var beg = +arr[0] || 0; + var end = +arr[1] || 0; + var total = F.temporary.range[name]; - if (data.length + self.buffer.length > self.length) { - self.errors++; - self.container.emit('error', new Error('Maximum request length exceeded.'), self); - return; - } + if (!total) { + total = Fs.statSync(name).size; + RELEASE && (F.temporary.range[name] = total); + } - switch (data[0] & 0x0f) { - case 0x01: + if (end === 0) + end = total - 1; - // text message or JSON message - if (self.type !== 1) - self.parse(data); + if (beg > end) { + beg = 0; + end = total - 1; + } - break; - case 0x02: + if (end > total) + end = total - 1; - // binary message - if (self.type === 1) - self.parse(data); + var length = (end - beg) + 1; - break; - case 0x08: - // close - self.close(); - break; - case 0x09: - // ping - self.socket.write(self._state('pong')); - break; - case 0x0a: - // pong - break; - } -}; + headers[HEADER_LENGTH] = length; + headers['Content-Range'] = 'bytes ' + beg + '-' + end + '/' + total; -// MIT -// Written by Jozef Gula -WebSocketClient.prototype.parse = function(data) { + var req = res; + F.stats.response.streaming++; - var self = this; + if (req.method === 'HEAD') { + res.writeHead(206, headers); + res.end(); + response_end(res); + return F; + } - if (data != null) - self.buffer = Buffer.concat([self.buffer, data]); + res.writeHead(206, headers); + RANGE.start = beg; + RANGE.end = end; + fsStreamRead(name, RANGE, $file_range_callback, res); + return F; +} - var bLength = self.buffer[1]; +function $file_range_callback(stream, next, res) { + framework_internal.onFinished(res, function() { + framework_internal.destroyStream(stream); + next(); + }); + stream.pipe(res); + response_end(res); +} - if (((bLength & 0x80) >> 7) !== 1) - return self; +function $image_nocache(res) { + + var options = res.options; + + // STREAM + if (options.stream) { + var image = framework_image.load(options.stream); + options.make.call(image, image, res); + options.type = U.getContentType(image.outputType); + options.stream = image; + F.stats.response.image++; + res.$stream(); + return F; + } + + // FILENAME + fsFileExists(options.filename, function(e) { + + if (e) { + F.path.verify('temp'); + var image = framework_image.load(options.filename); + options.make.call(image, image, res); + F.stats.response.image++; + options.type = U.getContentType(image.outputType); + options.stream = image; + res.$stream(); + } else { + options.headers = null; + if (!F.routes.filesfallback || !F.routes.filesfallback(res.req, res)) + res.throw404(); + } + }); +} - var length = utils.getMessageLength(self.buffer, self.container.framework.isLE); - var index = (self.buffer[1] & 0x7f); +function $image_processing(res) { + res.$image(); +} - index = (index == 126) ? 4 : (index == 127 ? 10 : 2); +function $image_stream(exists, size, isFile, stats, res) { + + var req = res.req; + var options = res.options; + + if (exists) { + delete F.temporary.processing[req.$key]; + F.temporary.path[req.$key] = [options.name, stats.size, stats.mtime.toUTCString()]; + res.options.filename = options.name; + + 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; + } + + F.path.verify('temp'); + + var image = framework_image.load(options.stream); + options.make.call(image, image, res); + req.extension = U.getExtension(options.name); + + if (req.extension !== image.outputType) { + var index = options.name.lastIndexOf('.' + req.extension); + if (index !== -1) + options.name = options.name.substring(0, index) + '.' + image.outputType; + else + options.name += '.' + image.outputType; + } + + 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; + res.throw500(err); + DEBUG && (F.temporary.notfound[req.$key] = undefined); + } else { + var stats = Fs.statSync(options.name); + F.temporary.path[req.$key] = [options.name, stats.size, stats.mtime.toUTCString()]; + options.filename = options.name; + res.$file(); + } + }); +} - if ((index + length + 4) > (self.buffer.length)) - return self; +function $image_filename(exists, size, isFile, stats, res) { + + var req = res.req; + var options = res.options; + + if (!exists) { + delete F.temporary.processing[req.$key]; + F.temporary.notfound[req.$key] = true; + 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); + options.make.call(image, image, res); + req.extension = U.getExtension(options.name); + + if (req.extension !== image.outputType) { + var index = options.name.lastIndexOf('.' + req.extension); + if (index === -1) + options.name += '.' + image.outputType; + else + options.name = options.name.substring(0, index) + '.' + image.outputType; + } + + F.stats.response.image++; + + image.save(options.name, function(err) { + + delete F.temporary.processing[req.$key]; + + if (err) { + F.temporary.notfound[req.$key] = true; + res.throw500(err); + DEBUG && (F.temporary.notfound[req.$key] = undefined); + } else { + var stats = Fs.statSync(options.name); + F.temporary.path[req.$key] = [options.name, stats.size, stats.mtime.toUTCString()]; + res.options.filename = options.name; + res.$file(); + } + }); +} - var mask = new Buffer(4); - self.buffer.copy(mask, 0, index, index + 4); +function response_end(res) { - // TEXT - if (self.type !== 1) { - var output = ''; - for (var i = 0; i < length; i++) - output += String.fromCharCode(self.buffer[index + 4 + i] ^ mask[i % 4]); + F.reqstats(false, res.req.isStaticFile); + res.success = true; - // JSON - if (self.type === 3) { - try { - self.container.emit('message', self, JSON.parse(self.container.config['default-websocket-encodedecode'] === true ? decodeURIComponent(output) : output)); - } catch (ex) { - self.errors++; - self.container.emit('error', new Error('JSON parser: ' + ex.toString()), self); - } - } else - self.container.emit('message', self, self.container.config['default-websocket-encodedecode'] === true ? decodeURIComponent(output) : output); + if (CONF.allow_reqlimit && F.temporary.ddos[res.req.ip]) + F.temporary.ddos[res.req.ip]--; - } else { - var binary = new Buffer(length); - for (var i = 0; i < length; i++) - binary.write(self.buffer[index + 4 + i] ^ mask[i % 4]); - self.container.emit('message', self, binary); - } + if (!res.req.isStaticFile) { + F.$events['request-end'] && EMIT('request-end', res.req, res); + F.$events.request_end && EMIT('request_end', res.req, res); + } - self.buffer = self.buffer.slice(index + length + 4, self.buffer.length); - if (self.buffer.length >= 2) - self.parse(null); + res.req.clear(true); + res.controller && res.req.$total_success(); - return self; -}; + if (res.options.callback) { + res.options.callback(); + res.options.callback = null; + } -/* - Internal handler -*/ -WebSocketClient.prototype._onerror = function(error) { - var self = this; - if (error.stack.indexOf('ECONNRESET') !== -1 || error.stack.indexOf('socket is closed') !== -1 || error.stack.indexOf('EPIPE') !== -1) - return; - self.container.emit('error', error, self); -}; + if (res.options.done) { + res.options.done(); + res.options.done = null; + } -/* - Internal handler -*/ -WebSocketClient.prototype._onclose = function() { - var self = this; + // res.options = EMPTYOBJECT; + res.controller = null; +} - if (self._isClosed) - return; +// Handle errors of decodeURIComponent +function $decodeURIComponent(value) { + try + { + return decodeURIComponent(value); + } catch (e) { + return value; + } +} - self._isClosed = true; - self.container._remove(self._id); - self.container._refresh(); - self.container.emit('close', self); - self.container.framework.emit('websocket-end', self.container, self); -}; +global.Controller = Controller; +global.WebSocketClient = WebSocketClient; -/* - Send message - @message {String or Object} - return {WebSocketClient} -*/ -WebSocketClient.prototype.send = function(message) { +process.on('unhandledRejection', function(e) { + F.error(e, '', null); +}); - var self = this; +process.on('uncaughtException', function(e) { - if (self.isClosed) - return; + var err = e.toString(); - if (self.type !== 1) { + 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(1); + return; + } else if (CONF.allow_filter_errors && REG_SKIPERROR.test(err)) + return; - var data = self.type === 3 ? JSON.stringify(message) : (message || '').toString(); - if (self.container.config['default-websocket-encodedecode'] === true && data.length > 0) - data = encodeURIComponent(data); + F.error(e, '', null); +}); - self.socket.write(utils.getWebSocketFrame(0, data, 0x01)); +function fsFileRead(filename, callback, a, b, c) { + 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); + }); + }); +} - } else { +function fsFileExists(filename, callback, a, b, c) { + 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); + }); + }); +} - if (message !== null) - self.socket.write(utils.getWebSocketFrame(0, message, 0x02)); +function fsStreamRead(filename, options, callback, res) { - } + if (!callback) { + callback = options; + options = undefined; + } - return self; -}; + var opt; -/* - Close connection - return {WebSocketClient} -*/ -WebSocketClient.prototype.close = function(message, code) { - var self = this; + if (options) { - if (self.isClosed) - return self; + opt = HEADERS.fsStreamReadRange; + opt.start = options.start; + opt.end = options.end; - self.isClosed = true; - self.socket.end(utils.getWebSocketFrame(code || 1000, message || '', 0x08)); + if (opt.start > opt.end) + delete opt.end; - return self; -}; + } else + opt = HEADERS.fsStreamRead; -/* - Send state - return {Buffer} -*/ -WebSocketClient.prototype._state = function(type) { - var value = new Buffer(6); - switch (type) { - case 'close': - value[0] = 0x08; - value[0] |= 0x80; - value[1] = 0x80; - break; - case 'ping': - value[0] = 0x09; - value[0] |= 0x80; - value[1] = 0x80; - break; - case 'pong': - value[0] = 0x0A; - value[0] |= 0x80; - value[1] = 0x80; - break; - } - var iMask = Math.floor(Math.random() * 255); - value[2] = iMask >> 8; - value[3] = iMask; - iMask = Math.floor(Math.random() * 255); - value[4] = iMask >> 8; - value[5] = iMask; - return value; -}; - -WebSocketClient.prototype._request_accept_key = function(req) { - var sha1 = crypto.createHash('sha1'); - sha1.update((req.headers['sec-websocket-key'] || '') + SOCKET_HASH); - return sha1.digest('base64'); -}; + 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, res); + }, filename); +} -// ********************************************************************************* -// ================================================================================= -// Prototypes -// ================================================================================= -// ********************************************************************************* +/** + * Prepare URL address to temporary key (for caching) + * @param {ServerRequest or String} req + * @return {String} + */ +function createTemporaryKey(req) { + return (req.uri ? req.uri.pathname : req).replace(REG_TEMPORARY, '_').substring(1); +} -/* - Write cookie - @name {String} - @value {String} - @expires {Date} :: optional - @options {Object} :: options.path, options.domain, options.secure, options.httpOnly, options.expires - return {ServerResponse} -*/ -http.ServerResponse.prototype.cookie = function(name, value, expires, options) { +F.createTemporaryKey = createTemporaryKey; - var builder = [name + '=' + encodeURIComponent(value)]; +function MiddlewareOptions() {} - if (expires && !utils.isDate(expires) && typeof(expires) === 'object') { - options = expires; - expires = options.expires || options.expire || null; - } +MiddlewareOptions.prototype = { - if (!options) - options = {}; + get user() { + return this.req.user; + }, - options.path = options.path || '/'; + get session() { + return this.req.session; + }, - if (expires) - builder.push('Expires=' + expires.toUTCString()); + get language() { + return this.req.$language; + }, - if (options.domain) - builder.push('Domain=' + options.domain); + get ip() { + return this.req.ip; + }, - if (options.path) - builder.push('Path=' + options.path); + get headers() { + return this.req.headers; + }, - if (options.secure) - builder.push('Secure'); + get ua() { + return this.req ? this.req.ua : null; + }, - if (options.httpOnly || options.httponly || options.HttpOnly) - builder.push('HttpOnly'); + get sessionid() { + return this.req.sessionid; + }, - var self = this; + get id() { + return this.controller ? this.controller.id : null; + }, - var arr = self.getHeader('set-cookie') || []; + get params() { + return this.controller ? this.controller.params : null; + }, - arr.push(builder.join('; ')); - self.setHeader('Set-Cookie', arr); + get files() { + return this.req.files; + }, - return self; -}; + get body() { + return this.req.body; + }, -/** - * Disable HTTP cache for current response - * @return {Response} - */ -http.ServerResponse.prototype.noCache = function() { - var self = this; - self.removeHeader('Etag'); - self.removeHeader('Last-Modified'); - return self; + get query() { + return this.req.query; + } }; -var _tmp = http.IncomingMessage.prototype; +const MiddlewareOptionsProto = MiddlewareOptions.prototype; -http.IncomingMessage.prototype = { +MiddlewareOptionsProto.callback = function() { + this.next(); + return this; +}; - get ip() { - var self = this; - var proxy = self.headers['x-forwarded-for']; - // x-forwarded-for: client, proxy1, proxy2, ... - if (typeof(proxy) !== UNDEFINED) - return proxy.split(',', 1)[0] || self.connection.removiewddress; - return self.connection.remoteAddress; - }, +MiddlewareOptionsProto.cancel = function() { + this.next(false); + return this; +}; - get subdomain() { +function forcestop() { + F.stop(); +} - var self = this; +process.on('SIGTERM', forcestop); +process.on('SIGINT', forcestop); +process.on('exit', forcestop); - if (self._subdomain) - return self._subdomain; +function process_ping() { + process.connected && process.send('total:ping'); +} - var subdomain = self.uri.host.toLowerCase().replace(/^www\./i, '').split('.'); - if (subdomain.length > 2) - self._subdomain = subdomain.slice(0, subdomain.length - 2); // example: [subdomain].domain.com - else - self._subdomain = null; +process.on('message', function(msg, h) { + if (msg === 'total:debug') { + U.wait(() => F.isLoaded, function() { + F.isLoaded = undefined; + F.console(); + }, 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) { + 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); +}); - return self._subdomain; - }, +function prepare_error(e) { + if (!e) + return ''; + else if (e instanceof ErrorBuilder) + return e.plain(); + else if (DEBUG) + return e.stack ? e.stack : e.toString(); +} - get host() { - return this.headers['host']; - }, +function prepare_filename(name) { + return name[0] === '@' ? (F.isWindows ? U.combine(CONF.directory_temp, name.substring(1)) : F.path.package(name.substring(1))) : U.combine('/', name); +} - get isSecure() { - return this.uri.protocol === 'https' || this.uri.protocol === 'wss'; - }, +function prepare_staticurl(url, isDirectory) { + if (!url) + return url; + if (url[0] === '~') { + if (isDirectory) + return U.path(url.substring(1)); + } else if (url.substring(0, 2) === '//' || url.substring(0, 6) === 'http:/' || url.substring(0, 7) === 'https:/') + return url; + return url; +} - get language() { - return ((this.headers['accept-language'].split(';')[0] || '').split(',')[0] || '').toLowerCase(); - } +function prepare_isomorphic(name, value) { + return 'if(window["isomorphic"]===undefined)window.isomorphic=window.I={};isomorphic["' + name.replace(/\.js$/i, '') + '"]=(function(framework,F,U,utils,Utils,is_client,is_server){var module={},exports=module.exports={};' + value + ';return exports;})(null,null,null,null,null,true,false)'; } -http.IncomingMessage.prototype.__proto__ = _tmp; +function isGZIP(req) { + var ua = req.headers['user-agent']; + return ua && ua.lastIndexOf('Firefox') !== -1; +} -/** - * Signature request (user-agent + ip + referer + current URL) - * @return {Request} - */ -http.IncomingMessage.prototype.signature = function() { - var self = this; - return framework.encrypt((self.headers['user-agent'] || '') + '#' + self.ip + '#' + self.url, 'request-signature', false); -}; +function prepare_viewname(value) { + // Cleans theme name + return value.substring(value.indexOf('/', 2) + 1); +} -/** - * Disable HTTP cache for current request - * @return {Request} - */ -http.IncomingMessage.prototype.noCache = function() { - var self = this; - delete self.headers['if-none-match']; - delete self.headers['if-modified-since']; - return self; -}; +function existsSync(filename, file) { + try { + var val = Fs.statSync(filename); + return val ? (file ? val.isFile() : true) : false; + } catch (e) { + return false; + } +} -/** - * Read a cookie from current request - * @param {String} name Cookie name. - * @return {String} Cookie value (default: '') - */ -http.IncomingMessage.prototype.cookie = function(name) { +function getLoggerMiddleware(name) { + return 'MIDDLEWARE("' + name + '")'; +} - var self = this; +function async_middleware(index, req, res, middleware, callback, options, controller) { + + if (res.success || res.headersSent || res.finished) { + req.$total_route && req.$total_success(); + callback = null; + return; + } + + var name = middleware[index++]; + if (!name) + return callback && callback(req, res); + + var item = F.routes.middleware[name]; + if (!item) { + F.error('Middleware not found: ' + name, null, req.uri); + return async_middleware(index, req, res, middleware, callback, options, controller); + } + + 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); + }; + } + + opt.index = index; + output = item(opt); + + } 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 (res.headersSent || res.finished) { + req.$total_route && req.$total_success(); + callback = null; + return; + } else if (output !== false) + return; + + req.$total_route && req.$total_success(); + callback = null; +} - if (typeof(self.cookies) !== UNDEFINED) - return decodeURIComponent(self.cookies[name] || ''); +global.setTimeout2 = function(name, fn, timeout, limit, param) { + var key = ':' + name; + var internal = F.temporary.internal; + + if (limit > 0) { + + 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; + } + + 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, key); + } + + if (internal[key]) { + clearTimeout(internal[key]); + internal[key] = undefined; + } + + return internal[key] = setTimeout(fn, timeout, param); +}; + +global.clearTimeout2 = function(name) { + var key = ':' + name; + + if (F.temporary.internal[key]) { + clearTimeout(F.temporary.internal[key]); + F.temporary.internal[key] = undefined; + F.temporary.internal[key + ':limit'] && (F.temporary.internal[key + ':limit'] = undefined); + return true; + } + + return false; +}; + +function parseComponent(body, filename) { + + 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; + + var data = body.substring(beg, end); + body = body.substring(0, beg) + body.substring(end + 7); + + // Creates directory + var p = F.path.temp() + '~' + comname; + try { + Fs.mkdirSync(p); + } catch (e) {} + + var tmp = data.indexOf('>'); + beg = data.lastIndexOf('name="', tmp); + var name = data.substring(beg + 6, data.indexOf('"', beg + 7)); + var encoding; + + beg = data.lastIndexOf('encoding="', tmp); + if (beg !== -1) + encoding = data.substring(beg + 10, data.indexOf('"', beg + 11)); + + data = data.substring(tmp + 1); + F.$bundling && Fs.writeFile(U.join(p, name), data.trim(), encoding || 'base64', NOOP); + response.files[name] = 1; + } + + while (true) { + beg = body.indexOf('@{part'); + if (beg === -1) + break; + end = body.indexOf('@{end}', beg); + if (end === -1) + break; + var tmp = body.substring(beg, end); + var tmpend = tmp.indexOf('}', 4); + response.parts[tmp.substring(tmp.indexOf(' '), tmpend).trim()] = body.substring(beg + tmpend + 1, end).trim(); + body = body.substring(0, beg).trim() + body.substring(end + 8).trim(); + end += 5; + } + + while (true) { + beg = body.indexOf('', beg); + if (end === -1) + break; + response.install += (response.install ? '\n' : '') + body.substring(beg, end).replace(/<(\/)?script.*?>/g, ''); + body = body.substring(0, beg).trim() + body.substring(end + 9).trim(); + } + + while (true) { + beg = body.indexOf('', beg); + if (end === -1) + break; + response.css += (response.css ? '\n' : '') + body.substring(beg, end).replace(/<(\/)?style.*?>/g, ''); + body = body.substring(0, beg).trim() + body.substring(end + 8).trim(); + } + + while (true) { + beg = body.indexOf('', beg); + if (end === -1) + break; + response.js += (response.js ? '\n' : '') + body.substring(beg, end).replace(/<(\/)?script.*?>/g, ''); + body = body.substring(0, beg).trim() + body.substring(end + 9).trim(); + } + + if (response.js) + response.js = framework_internal.compile_javascript(response.js, filename); + + if (response.css) + response.css = framework_internal.compile_css(response.css, filename); + + response.body = body; + return response; +} - self.cookies = {}; +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]); +} - var cookie = self.headers['cookie'] || ''; - if (cookie.length === 0) - return ''; +// Default action for workflow routing +function controller_json_workflow(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) { + 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)); +} - var arr = cookie.split(';'); - var length = arr.length; +// 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)); +} - for (var i = 0; i < length; i++) { - var c = arr[i].trim().split('='); - self.cookies[c.shift()] = c.join('='); - } +function ilogger(body) { + F.path.verify('logs'); + U.queue('F.ilogger', 5, (next) => Fs.appendFile(U.combine(CONF.directory_logs, 'logger.log'), body, next)); +} - return decodeURIComponent(self.cookies[name] || ''); -}; +F.ilogger = function(name, req, ts) { -/* - Read authorization header - return {Object} -*/ -http.IncomingMessage.prototype.authorization = function() { + if (req && req instanceof Controller) + req = req.req; - var self = this; - var authorization = self.headers['authorization'] || ''; + var isc = CONF.logger === 'console'; + var divider = ''; - if (authorization === '') - return { - name: '', - password: '' - }; + for (var i = 0; i < (isc ? 64 : 220); i++) + divider += '-'; - var arr = new Buffer(authorization.replace('Basic ', '').trim(), 'base64').toString('utf8').split(':'); - return { - name: arr[0] || '', - password: arr[1] || '' - }; -}; + var msg; -/* - Clear all uploaded files - @isAuto {Booelan} :: system, internal, optional default false - return {ServerRequest} -*/ -http.IncomingMessage.prototype.clear = function(isAuto) { + if (req && !name && req.$logger && req.$logger.length) { - var self = this; + msg = req.method + ' ' + req.url; - if (!self.data) - return self; + req.$logger.unshift(msg); + req.$logger.push(divider); - var files = self.data.files; + if (isc) + console.log(req.$logger.join('\n')); + else { + req.$logger.push(''); + ilogger(req.$logger.join('\n')); + } - if (isAuto && self._manual) - return self; + req.$logger = null; + return; + } - if (!files) - return self; + if (!name) + return; - var length = files.length; + var dt = new Date(); - if (length === 0) - return self; + 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); - var arr = []; - for (var i = 0; i < length; i++) - arr.push(files[i].path); + 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'); + } +}; - framework.unlink(arr); - self.data.files = null; +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); +} - return self; -}; +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); + } + }); +} -/* - Return hostname with protocol and port - @path {String} :: optional - return {String} -*/ -http.IncomingMessage.prototype.hostname = function(path) { +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 self = this; - var uri = self.uri; +var lastusagedate; - if (typeof(path) !== UNDEFINED) { - if (path[0] !== '/') - path = '/' + path; - } +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; +} - return uri.protocol + '//' + uri.hostname + (uri.port !== null && typeof(uri.port) !== UNDEFINED && uri.port !== 80 ? ':' + uri.port : '') + (path || ''); -}; +function measure_usage() { + lastusagedate = Date.now(); + setTimeout(measure_usage_response, 50); +} -global.framework = module.exports = new Framework(); \ No newline at end of file +// Because of controller prototypes +// It's used in VIEW() and VIEWCOMPILE() +const EMPTYCONTROLLER = new Controller('', null, null, ''); +EMPTYCONTROLLER.isConnected = false; +EMPTYCONTROLLER.req = {}; +EMPTYCONTROLLER.req.url = ''; +EMPTYCONTROLLER.req.uri = EMPTYOBJECT; +EMPTYCONTROLLER.req.query = EMPTYOBJECT; +EMPTYCONTROLLER.req.body = EMPTYOBJECT; +EMPTYCONTROLLER.req.files = EMPTYARRAY; +global.EMPTYCONTROLLER = EMPTYCONTROLLER; diff --git a/internal.js b/internal.js index 612b658f1..91ff43b8d 100755 --- a/internal.js +++ b/internal.js @@ -1,667 +1,828 @@ +// 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 FrameworkInternal - * @author Peter Širka - * @copyright Peter Širka 2012-2014 - * @version 1.5.0 + * @version 3.4.3 */ 'use strict'; -var crypto = require('crypto'); -var fs = require('fs'); -var utils = require('./utils'); - -var ENCODING = 'utf8'; -var UNDEFINED = 'undefined'; -var FUNCTION = 'function'; - -var REG_1 = /[\n\r\t]+/g; -var REG_2 = /\s{2,}/g; - -/* - Internal function / Parse data from Request - @req {ServerRequest} - @contentType {String} - @maximumSize {Number} - @tmpDirectory {String} - @onXSS {Function} - @callback {Function} -*/ -exports.parseMULTIPART = function(req, contentType, maximumSize, tmpDirectory, onXSS, callback) { +const Crypto = require('crypto'); +const Fs = require('fs'); +const ReadStream = Fs.ReadStream; +const Stream = require('stream'); +const ENCODING = 'utf8'; +const EMPTYARRAY = []; +const EMPTYOBJECT = {}; +const CONCAT = [null, null]; + +if (!global.framework_utils) + global.framework_utils = require('./utils'); + +Object.freeze(EMPTYOBJECT); +Object.freeze(EMPTYARRAY); + +const REG_1 = /[\n\r\t]+/g; +const REG_2 = /\s{2,}/g; +const REG_4 = /\n\s{2,}./g; +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+](\r)\n\s{1,}$/; +const REG_HELPERS = /helpers\.[a-z0-9A-Z_$]+\(.*?\)+/g; +const REG_SITEMAP = /\s+(sitemap_navigation\(|sitemap\()+/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; +const REG_CSS_5 = /\s?;\s{1,}/g; +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-_]+(\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 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; + +global.$STRING = function(value) { + return value != null ? value.toString() : ''; +}; - var parser = new MultipartParser(); - var boundary = contentType.split(';')[1]; - var isFile = false; - var size = 0; - var stream = null; - var tmp = { - name: '', - value: '', - contentType: '', - fileName: '', - fileNameTmp: '', - fileSize: 0, - isFile: false, - step: 0, - width: 0, - height: 0 - }; - var ip = req.ip.replace(/\./g, ''); - var close = 0; - var isXSS = false; - var rm = null; - - boundary = boundary.substring(boundary.indexOf('=') + 1); - - req.buffer_exceeded = false; - req.buffer_has = true; - - parser.initWithBoundary(boundary); - - parser.onPartBegin = function() { - tmp.value = ''; - tmp.fileSize = 0; - tmp.step = 0; - tmp.isFile = false; - }; - - parser.onHeaderValue = function(buffer, start, end) { - - if (req.buffer_exceeded) - return; - - if (isXSS) - return; - - var arr = buffer.slice(start, end).toString(ENCODING).split(';'); - - if (tmp.step === 1) { - tmp.contentType = arr[0]; - tmp.step = 2; - return; - } - - if (tmp.step !== 0) - return; - - tmp.name = arr[1].substring(arr[1].indexOf('=') + 2); - tmp.name = tmp.name.substring(0, tmp.name.length - 1); - tmp.step = 1; - - if (arr.length !== 3) - return; - - tmp.fileName = arr[2].substring(arr[2].indexOf('=') + 2); - tmp.fileName = tmp.fileName.substring(0, tmp.fileName.length - 1); - - tmp.isFile = true; - tmp.fileNameTmp = utils.combine(tmpDirectory, ip + '-' + new Date().getTime() + '-' + utils.random(100000) + '.upload'); - - stream = fs.createWriteStream(tmp.fileNameTmp, { - flags: 'w' - }); - - stream.once('close', function() { - close--; - }); - - stream.once('error', function() { - close--; - }); - - close++; - }; - - parser.onPartData = function(buffer, start, end) { - - if (req.buffer_exceeded) - return; - - if (isXSS) - return; - - var data = buffer.slice(start, end); - var length = data.length; - - size += length; - - if (size >= maximumSize) { - req.buffer_exceeded = true; - - if (rm === null) - rm = [tmp.fileNameTmp]; - else - rm.push(tmp.fileNameTmp); - - return; - } +global.$VIEWCACHE = []; +global.$VIEWASYNC = 0; + +exports.parseMULTIPART = function(req, contentType, route, tmpDirectory) { + + var beg = contentType.indexOf('boundary='); + if (beg === -1) { + F.reqstats(false, false); + F.stats.request.error400++; + req.res.writeHead(400); + req.res.end(); + return; + } - if (!tmp.isFile) { - tmp.value += data.toString(ENCODING); - return; - } + var end = contentType.length; - if (tmp.fileSize === 0) { - var wh = null; - switch (tmp.contentType) { - case 'image/jpeg': - wh = require('./image').measureJPG(data); - break; - case 'image/gif': - wh = require('./image').measureGIF(data); - break; - case 'image/png': - wh = require('./image').measurePNG(data); - break; - } + for (var i = (beg + 10); i < end; i++) { + if (contentType[i] === ';' || contentType[i] === ' ') { + end = i; + break; + } + } - if (wh) { - tmp.width = wh.width; - tmp.height = wh.height; - } - } + var boundary = contentType.substring(beg + 9, end); - stream.write(data); - tmp.fileSize += length; - }; + // For unexpected closing + req.once('close', () => !req.$upload && req.clear()); - parser.onPartEnd = function() { + var parser = new MultipartParser(); + var size = 0; + var maximumSize = route.length; + var close = 0; + var stream; + var tmp; + var rm; + var fn_close = function() { + close--; + }; + + // Replaces the EMPTYARRAY and EMPTYOBJECT in index.js + req.files = []; + req.body = {}; + + var path = framework_utils.combine(tmpDirectory, (F.id ? 'i-' + F.id + '_' : '') + 'uploadedfile-'); + + req.buffer_exceeded = false; + req.buffer_has = true; + req.buffer_parser = parser; + parser.initWithBoundary(boundary); - if (stream !== null) { - stream.end(); - stream = null; - } + parser.onPartBegin = function() { + + if (req.buffer_exceeded) + return; + + // Temporary data + tmp = new HttpFile(); + tmp.$data = Buffer.alloc(0); + 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; + + // UNKNOWN ERROR, maybe attack + if (header.indexOf('form-data; ') === -1) { + req.buffer_exceeded = true; + !tmp.$is && destroyStream(stream); + return; + } + + header = parse_multipart_header(header); + tmp.$step = 1; + tmp.$is = header[1] !== null; + tmp.name = header[0]; + + if (!tmp.$is) { + destroyStream(stream); + return; + } + + tmp.filename = header[1]; + + // IE9 sends absolute filename + var index = tmp.filename.lastIndexOf('\\'); + + // For Unix like senders + if (index === -1) + index = tmp.filename.lastIndexOf('/'); + + if (index !== -1) + tmp.filename = tmp.filename.substring(index + 1); + + tmp.path = path + (INDEXFILE++) + '.bin'; + }; + + parser.onPartData = function(buffer, start, end) { + + if (req.buffer_exceeded) + return; + + var data = buffer.slice(start, end); + var length = data.length; + + size += length; + + if (size >= maximumSize) { + req.buffer_exceeded = true; + if (rm) + rm.push(tmp.path); + else + rm = [tmp.path]; + return; + } + + if (!tmp.$is) { + CONCAT[0] = tmp.$data; + CONCAT[1] = data; + tmp.$data = Buffer.concat(CONCAT); + return; + } + + if (tmp.length) { + stream.write(data); + tmp.length += length; + return; + } + + var wh = null; + + switch (tmp.type) { + case 'image/jpeg': + wh = framework_image.measureJPG(buffer.slice(start)); + break; + case 'image/gif': + wh = framework_image.measureGIF(data); + break; + case 'image/png': + wh = framework_image.measurePNG(data); + break; + case 'image/svg+xml': + wh = framework_image.measureSVG(data); + break; + } + + if (wh) { + tmp.width = wh.width; + tmp.height = wh.height; + } else { + tmp.width = 0; + tmp.height = 0; + } + + req.files.push(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); + stream.once('error', fn_close); + stream.write(data); + tmp.length += length; + }; + + parser.onPartEnd = function() { + + if (stream) { + stream.end(); + stream = null; + } + + 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; + } + + tmp.$data = tmp.$data.toString(ENCODING); + + var temporary = req.body[tmp.name]; + if (temporary === undefined) { + req.body[tmp.name] = tmp.$data; + } else if (temporary instanceof Array) { + req.body[tmp.name].push(tmp.$data); + } else { + temporary = [temporary]; + temporary.push(tmp.$data); + req.body[tmp.name] = temporary; + } + }; + + parser.onEnd = function() { + + if (close) { + setImmediate(parser.onEnd); + } else { + rm && F.unlink(rm); + req.$total_end2(); + } + }; + + req.on('data', uploadparser); + req.on('end', uploadparser_done); +}; - if (req.buffer_exceeded) - return; +function uploadparser(chunk) { + this.buffer_parser.write(chunk); +} - if (tmp.isFile) { - req.data.files.push(new HttpFile(tmp.name, tmp.fileName, tmp.fileNameTmp, tmp.fileSize, tmp.contentType, tmp.width, tmp.height)); - return; - } +function uploadparser_done() { + !this.buffer_exceeded && (this.$upload = true); + this.buffer_parser.end(); +} - if (onXSS(tmp.value)) - isXSS = true; +function parse_multipart_header(header) { - var temporary = req.data.post[tmp.name]; + var arr = new Array(2); + var find = ' name="'; + var length = find.length; + var beg = header.indexOf(find); + var tmp = ''; - if (typeof(temporary) === UNDEFINED) { - req.data.post[tmp.name] = tmp.value; - return; - } + if (beg !== -1) + tmp = header.substring(beg + length, header.indexOf('"', beg + length)); - if (utils.isArray(temporary)) { - req.data.post[tmp.name].push(tmp.value); - return; - } + if (tmp) + arr[0] = tmp; + else + arr[0] = 'undefined_' + (Math.floor(Math.random() * 100000)).toString(); - temporary = [temporary]; - temporary.push(tmp.value); - req.data.post[tmp.name] = temporary; - }; + find = ' filename="'; + length = find.length; + beg = header.indexOf(find); + tmp = ''; - parser.onEnd = function() { + if (beg !== -1) + tmp = header.substring(beg + length, header.indexOf('"', beg + length)); - var cb = function() { + if (tmp) + arr[1] = tmp; + else + arr[1] = null; - if (close > 0) { - setImmediate(cb); - return; - } + return arr; +} - if (isXSS) { - req.flags.push('xss'); - framework.stats.request.xss++; - } +exports.routeSplit = function(url, noLower) { - if (rm !== null) - framework.unlink(rm); + var arr; - callback(); - }; + if (!noLower) { + arr = F.temporary.other[url]; + if (arr) + return arr; + } - cb(); - }; + if (!url || url === '/') { + arr = ['/']; + return arr; + } - req.on('data', parser.write.bind(parser)); - req.on('end', parser.end.bind(parser)); -}; + var prev = false; + var key = ''; + var count = 0; -/* - Internal function / Parse MIXED data - @req {ServerRequest} - @contentType {String} - @tmpDirectory {String} - @onFile {Function} :: this function is called when is a file downloaded - @callback {Function} -*/ -exports.parseMULTIPART_MIXED = function(req, contentType, tmpDirectory, onFile, callback) { - - var parser = new MultipartParser(); - var boundary = contentType.split(';')[1]; - var stream = null; - var tmp = { - name: '', - contentType: '', - fileName: '', - fileNameTmp: '', - fileSize: 0, - isFile: false, - step: 0, - width: 0, - height: 0 - }; - var ip = req.ip.replace(/\./g, ''); - var close = 0; - - boundary = boundary.substring(boundary.indexOf('=') + 1); - - req.buffer_exceeded = false; - req.buffer_has = true; - - parser.initWithBoundary(boundary); - - parser.onPartBegin = function() { - tmp.fileSize = 0; - tmp.step = 0; - tmp.isFile = false; - }; - - parser.onHeaderValue = function(buffer, start, end) { - - if (req.buffer_exceeded || tmp.step > 1) - return; - - var arr = buffer.slice(start, end).toString(ENCODING).split(';'); - - if (tmp.step === 1) { - tmp.contentType = arr[0]; - tmp.step = 2; - return; - } - - if (tmp.step === 0) { - - tmp.name = arr[1].substring(arr[1].indexOf('=') + 2); - tmp.name = tmp.name.substring(0, tmp.name.length - 1); - tmp.step = 1; - - if (arr.length !== 3) - return; - - tmp.fileName = arr[2].substring(arr[2].indexOf('=') + 2); - tmp.fileName = tmp.fileName.substring(0, tmp.fileName.length - 1); - tmp.isFile = true; - tmp.fileNameTmp = utils.combine(tmpDirectory, ip + '-' + new Date().getTime() + '-' + utils.random(100000) + '.upload'); - stream = fs.createWriteStream(tmp.fileNameTmp, { - flags: 'w' - }); - - stream.on('close', function() { - close--; - }); - - stream.on('error', function() { - close--; - }); - - close++; - return; - } - - }; - - parser.onPartData = function(buffer, start, end) { - var data = buffer.slice(start, end); - var length = data.length; - - if (!tmp.isFile) - return; - - if (tmp.fileSize === 0) { - var wh = null; - switch (tmp.contentType) { - case 'image/jpeg': - wh = require('./image').measureJPG(data); - break; - case 'image/gif': - wh = require('./image').measureGIF(data); - break; - case 'image/png': - wh = require('./image').measurePNG(data); - break; - } - - if (wh) { - tmp.width = wh.width; - tmp.height = wh.height; - } - } - - stream.write(data); - tmp.fileSize += length; - }; - - parser.onPartEnd = function() { - - if (stream !== null) { - stream.end(); - stream = null; - } + arr = []; - if (!tmp.isFile) - return; + for (var i = 0, length = url.length; i < length; i++) { + var c = url[i]; - onFile(new HttpFile(tmp.name, tmp.fileName, tmp.fileNameTmp, tmp.fileSize, tmp.contentType, tmp.width, tmp.height)); - }; + if (c === '/') { + if (key && !prev) { + arr.push(key); + count++; + key = ''; + } + continue; + } - parser.onEnd = function() { - var cb = function cb() { + key += noLower ? c : c.toLowerCase(); + prev = c === '/'; + } - if (close > 0) { - setImmediate(cb); - return; - } - - onFile(null); - callback(); - }; + if (key) + arr.push(key); + else if (!count) + arr.push('/'); - cb(); - }; - - req.on('data', parser.write.bind(parser)); + return arr; }; -/* - Internal function / Split string (url) to array - @url {String} - return {String array} -*/ -exports.routeSplit = function(url, noLower) { +exports.routeSplitCreate = function(url, noLower) { - if (!noLower) - url = url.toLowerCase(); + if (!noLower) + url = url.toLowerCase(); - if (url[0] === '/') - url = url.substring(1); + if (url[0] === '/') + url = url.substring(1); - if (url[url.length - 1] === '/') - url = url.substring(0, url.length - 1); + if (url[url.length - 1] === '/') + url = url.substring(0, url.length - 1); - var arr = url.split('/'); - if (arr.length === 1 && arr[0] === '') - arr[0] = '/'; + var count = 0; + var end = 0; + var arr = []; - return arr; -}; + for (var i = 0, length = url.length; i < length; i++) { + switch (url[i]) { + case '/': + if (count !== 0) + break; + arr.push(url.substring(end + (arr.length ? 1 : 0), i)); + end = i; + break; -/* - Internal function / Compare route with url - @route {String array} - @url {String} - @isSystem {Boolean} - return {Boolean} -*/ -exports.routeCompare = function(url, route, isSystem, isAsterix) { + case '{': + count++; + break; - var length = url.length; + case '}': + count--; + break; + } + } - if (route.length !== length && !isAsterix) - return false; + !count && arr.push(url.substring(end + (arr.length ? 1 : 0), url.length)); - var skip = length === 1 && url[0] === '/'; + if (arr.length === 1 && !arr[0]) + arr[0] = '/'; - for (var i = 0; i < length; i++) { + return arr; +}; - var value = route[i]; - if (!isSystem && isAsterix && typeof(value) === UNDEFINED) - return true; +exports.routeCompare = function(url, route, isSystem, isWildcard) { - if (!isSystem && (!skip && value[0] === '{')) - continue; + var length = url.length; + var lengthRoute = route.length; - if (url[i] !== value) { - if (!isSystem) - return isAsterix - return false; - } - } + if ((lengthRoute !== length && !isWildcard) || (isWildcard && length < lengthRoute)) + return false; - return true; -}; + if (isWildcard && lengthRoute === 1 && route[0] === '/') + return true; -/* - Internal function / Compare subdomain - @subdomain {String} - @arr {String array} - return {Boolean} -*/ -exports.routeCompareSubdomain = function(subdomain, arr) { + var skip = length === 1 && url[0] === '/'; + + for (var i = 0; i < length; i++) { - if (arr === null || subdomain === null || arr.length === 0) - return true; + var value = route[i]; - return arr.indexOf(subdomain) > -1; + if (!isSystem && isWildcard && value === undefined) + return true; + + if (!isSystem && (!skip && value[0] === '{')) + continue; + + if (url[i] !== value) + return isSystem ? false : isWildcard ? i >= lengthRoute : false; + } + + return true; }; -/* - Internal function / Compare flags - @arr1 {String array} - @arr2 {String array} - @noLoggedUnlogged {Boolean} - return {Number} -*/ -exports.routeCompareFlags = function(arr1, arr2, noLoggedUnlogged) { +exports.routeCompareSubdomain = function(subdomain, arr) { + if ((!subdomain && !arr) || (subdomain && !arr)) + return true; + if (!subdomain && arr) + return false; + for (var i = 0, length = arr.length; i < length; i++) { + if (arr[i] === '*') + return true; + var index = arr[i].lastIndexOf('*'); + if (index === -1) { + if (arr[i] === subdomain) + return true; + } else if (subdomain.indexOf(arr[i].replace('*', '')) !== -1) + return true; + } + return false; +}; - var isXSS = false; - var length = arr2.length; - //var LOGGED = 'logged'; - //var UNLOGGED = 'unlogged'; +exports.routeCompareFlags = function(arr1, arr2, membertype) { - var AUTHORIZE = 'authorize'; - var UNAUTHORIZE = 'unauthorize'; + var hasVerb = false; + var a1 = arr1; + var a2 = arr2; + var l1 = arr1.length; + var l2 = arr2.length; + var select = l1 > l2 ? a1 : a2; + var compare = l1 > l2 ? a2 : a1; + var length = Math.max(l1, l2); - for (var i = 0; i < length; i++) { - var value = arr2[i]; + var AUTHORIZE = 'authorize'; + var UNAUTHORIZE = 'unauthorize'; - if (value[0] === '!') - continue; + for (var i = 0; i < length; i++) { - if (noLoggedUnlogged && (value === AUTHORIZE || value === UNAUTHORIZE)) - continue; + var value = select[i]; + var c = value[0]; - var index = arr1.indexOf(value); + if (c === '!' || c === '#' || c === '$' || c === '@' || c === '+') // ignore roles + continue; - if (index === -1 && value === 'xss') { - isXSS = true; - continue; - } + if (!membertype && (value === AUTHORIZE || value === UNAUTHORIZE)) + continue; - if (value === 'xss') - isXSS = true; + var index = compare.indexOf(value); + if (index === -1 && !HTTPVERBS[value]) + return value === AUTHORIZE || value === UNAUTHORIZE ? -1 : 0; - // return value === LOGGED || value === UNLOGGED ? -1 : 0; - if (index === -1) - return value === AUTHORIZE || value === UNAUTHORIZE ? -1 : 0; - } + hasVerb = hasVerb || (index !== -1 && HTTPVERBS[value]); + } - if (!isXSS && arr1.indexOf('xss') !== -1) - return 0; + return hasVerb ? 1 : 0; +}; - return 1; +exports.routeCompareFlags2 = function(req, route, membertype) { + + // membertype 0 -> not specified + // membertype 1 -> auth + // membertype 2 -> unauth + + // 1. upload --> 0 + // 2. doAuth --> 1 or 2 + + // if (membertype && ((membertype !== 1 && route.MEMBER === 1) || (membertype !== 2 && route.MEMBER === 2))) + if (membertype && route.MEMBER && membertype !== route.MEMBER) + return -1; + + if (!route.isWEBSOCKET) { + if ((route.isXHR && !req.xhr) || (route.isMOBILE && !req.mobile) || (route.isROBOT && !req.robot) || (route.isUPLOAD && !req.$upload)) + return 0; + var method = req.method; + if (route.method) { + if (route.method !== method) + return 0; + } else if (!route.flags2[method.toLowerCase()]) + 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; + var hasRoles = false; + + for (var i = 0, length = req.flags.length; i < length; i++) { + + var flag = req.flags[i]; + switch (flag) { + case 'json': + continue; + case 'xml': + if (route.isRAW || route.isXML) + continue; + return 0; + + case 'debug': + if (!route.isDEBUG && route.isRELEASE) + return 0; + continue; + + case 'release': + if (!route.isRELEASE && route.isDEBUG) + return 0; + continue; + + case 'referer': + continue; + + case 'upload': + if (!route.isUPLOAD) + return 0; + continue; + + case 'https': + if (!route.isHTTPS && route.isHTTP) + return 0; + continue; + + case 'http': + if (!route.isHTTP && route.isHTTPS) + return 0; + continue; + + case 'xhr': + case '+xhr': + if (!route.isBOTH && !route.isXHR) + return 0; + continue; + } + + var role = flag[0] === '@'; + + if (membertype !== 1 && route.MEMBER !== 1) { + if ((!route.isGET && !role && !route.flags2[flag]) || (route.isROLE && role && !route.flags2[flag]) || (route.isROLE && !role)) + return 0; + continue; + } + + // Is some role verified? + if (role && isRole && !route.isROLE) + continue; + + if (!role && !route.flags2[flag]) + return 0; + + if (role) { + if (route.flags2[flag]) + isRole = true; + hasRoles = true; + } + } + + return (route.isROLE && hasRoles) ? isRole ? 1 : -1 : 1; }; -/* - Internal function - @routeUrl {String array} - @route {Controller route} - return {String array} -*/ +/** + * Create arguments for controller's action + * @param {String Array} routeUrl + * @param {Object} route + * @return {String Array} + */ exports.routeParam = function(routeUrl, route) { - var arr = []; - if (!route || !routeUrl) - return arr; + if (!route || !routeUrl || !route.param.length) + return EMPTYARRAY; - var length = route.param.length; - if (length === 0) - return arr; + var arr = []; - for (var i = 0; i < length; i++) { - var value = routeUrl[route.param[i]]; - arr.push(value === '/' ? '' : value); - } + for (var i = 0, length = route.param.length; i < length; i++) { + var value = routeUrl[route.param[i]]; + arr.push(value === '/' ? '' : value); + } - return arr; + return arr; }; -/* - HttpFile class - @name {String} - @filename {String} - @path {String} - @length {Number} - @contentType {String} - return {HttpFile} -*/ -function HttpFile(name, filename, path, length, contentType, width, height) { - this.name = name; - this.filename = filename; - this.length = length; - this.contentType = contentType; - this.path = path; - this.width = width; - this.height = height; +function HttpFile() { + this.name; + this.filename; + this.type; + this.path; + this.length = 0; + this.width = 0; + this.height = 0; + this.rem = true; } -/* - Read file to byte array - @filename {String} :: new filename - return {HttpFile} -*/ -HttpFile.prototype.copy = function(filename, callback) { - - var self = this; +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; + } +}; - if (!callback) { - fs.createReadStream(self.path).pipe(fs.createWriteStream(filename)); - return; - } +var HFP = HttpFile.prototype; - var reader = fs.createReadStream(self.path); - var writer = fs.createWriteStream(filename); +HFP.rename = HFP.move = function(filename, callback) { + var self = this; + Fs.rename(self.path, filename, function(err) { - reader.on('close', callback); - reader.pipe(writer); + if (!err) { + self.path = filename; + self.rem = false; + } - return self; + callback && callback(err); + }); + return self; }; -/* - Read file to buffer (SYNC) - return {Buffer} -*/ -HttpFile.prototype.readSync = function() { - return fs.readFileSync(this.path); -}; +HFP.copy = function(filename, callback) { + + var self = this; -/* - Read file to buffer (ASYNC) - @callback {Function} :: function(error, data); - return {HttpFile} -*/ -HttpFile.prototype.read = function(callback) { - var self = this; - fs.readFile(self.path, callback); - return self; + if (!callback) { + Fs.createReadStream(self.path).pipe(Fs.createWriteStream(filename)); + return; + } + + var reader = Fs.createReadStream(self.path); + var writer = Fs.createWriteStream(filename); + + reader.on('close', callback); + reader.pipe(writer); + return self; }; -/* - Create MD5 hash from a file - @callback {Function} :: function(error, hash); - return {HttpFile} -*/ -HttpFile.prototype.md5 = function(callback) { +HFP.$$rename = HFP.$$move = function(filename) { + var self = this; + return function(callback) { + return self.rename(filename, callback); + }; +}; - var self = this; - var md5 = crypto.createHash('md5'); - var stream = fs.createReadStream(self.path); +HFP.$$copy = function(filename) { + var self = this; + return function(callback) { + return self.copy(filename, callback); + }; +}; - stream.on('data', function(buffer) { - md5.update(buffer); - }); +HFP.readSync = function() { + return Fs.readFileSync(this.path); +}; - stream.on('error', function(error) { - callback(error, null); - }); +HFP.read = function(callback) { + var self = this; + F.stats.performance.open++; + Fs.readFile(self.path, callback); + return self; +}; - stream.on('end', function() { - callback(null, md5.digest('hex')); - }); +HFP.$$read = function() { + var self = this; + return function(callback) { + self.read(callback); + }; +}; - return self; +HFP.md5 = function(callback) { + var self = this; + var md5 = Crypto.createHash('md5'); + var stream = Fs.createReadStream(self.path); + + stream.on('data', (buffer) => md5.update(buffer)); + stream.on('error', function(error) { + if (callback) { + callback(error, null); + callback = null; + } + }); + + onFinished(stream, function() { + destroyStream(stream); + if (callback) { + callback(null, md5.digest('hex')); + callback = null; + } + }); + + return self; }; -/* - Get a stream - @options {Object} :: optional - return {Stream} -*/ -HttpFile.prototype.stream = function(options) { - var self = this; - return fs.createReadStream(self.path, options); +HFP.$$md5 = function() { + var self = this; + return function(callback) { + self.md5(callback); + }; }; -/* - Pipe a stream - @stream {Stream} - @options {Object} :: optional - return {Stream} -*/ -HttpFile.prototype.pipe = function(stream, options) { - var self = this; - return fs.createReadStream(self.path, options).pipe(stream, options); +HFP.stream = function(options) { + return Fs.createReadStream(this.path, options); }; -/* - return {Boolean} -*/ -HttpFile.prototype.isImage = function() { - var self = this; - return self.contentType.indexOf('image/') !== -1; +HFP.pipe = function(stream, options) { + return Fs.createReadStream(this.path, options).pipe(stream, options); }; -/* - return {Boolean} -*/ -HttpFile.prototype.isVideo = function() { - var self = this; - return self.contentType.indexOf('video/') !== -1; +HFP.isImage = function() { + return this.type.indexOf('image/') !== -1; }; -/* - return {Boolean} -*/ -HttpFile.prototype.isAudio = function() { - var self = this; - return self.contentType.indexOf('audio/') !== -1; +HFP.isVideo = function() { + return this.type.indexOf('video/') !== -1; }; -/* - @imageMagick {Boolean} :: optional - default false - return {Image} :: look at ./lib/image.js -*/ -HttpFile.prototype.image = function(imageMagick) { +HFP.isAudio = function() { + return this.type.indexOf('audio/') !== -1; +}; - var im = imageMagick; +HFP.image = function(im) { + if (im === undefined) + im = CONF.default_image_converter === 'im'; + return framework_image.init(this.path, im, this.width, this.height); +}; - // Not a clean solution because the framework hasn't a direct dependence. - // This is hack :-) - if (typeof(im) === UNDEFINED) - im = framework.config['default-image-converter'] === 'im'; +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); +}; - return require('./image').init(this.path, im); +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); }; // ********************************************************************************* @@ -670,646 +831,484 @@ HttpFile.prototype.image = function(imageMagick) { // ================================================================================= // ********************************************************************************* -function compile_jscss(css) { - - var comments = []; - var beg = 0; - var end = 0; - var tmp = ''; - var reg1 = /\n|\s{2,}/g; - var reg2 = /\s?\{\s{1,}/g; - var reg3 = /\s?\}\s{1,}/g; - var reg4 = /\s?\:\s{1,}/g; - var reg5 = /\s?\;\s{1,}/g; - var output = ''; - - var prepare = function(value) { - return value.replace(reg1, '').replace(reg2, '{').replace(reg3, '}').replace(reg4, ':').replace(reg5, ';').replace(/\s\}/g, '}').replace(/\s\{/g, '{').trim(); - }; - - while (true) { - - beg = css.indexOf('/*', beg); - - if (beg === -1) { - tmp += css; - break; - } - - end = css.indexOf('*/', beg); - if (end === -1) - continue; - - comments.push(css.substring(beg, end).trim()); - tmp += css.substring(0, beg).trim(); - css = css.substring(end + 2); - beg = 0; - } - - output = ''; - tmp = tmp.trim(); - - var length = comments.length; - var code = ''; - var avp = '@#auto-vendor-prefix#@'; - var isAuto = tmp.startsWith(avp); - - if (isAuto) - tmp = tmp.replace(avp, ''); - - for (var i = 0; i < length; i++) { - - var comment = comments[i]; - - // Auto vendor prefixes - if (comment.indexOf('auto') !== -1 && comment.length <= 10) { - isAuto = true; - continue; - } - - // Code for evaluating - if (comment.indexOf('var ') !== -1 || comment.indexOf('function ') !== -1) - code += comment.replace('/*', '').replace('*/', '') + '\n\n'; - - } - - beg = 0; - end = 0; - - var DELIMITER_UNESCAPE = '+unescape(\''; - var DELIMITER_UNESCAPE_END = '\')'; - - while (true) { - - beg = tmp.indexOf('$'); - - if (beg === -1) { - output += DELIMITER_UNESCAPE + escape(tmp) + DELIMITER_UNESCAPE_END; - break; - } - - output += DELIMITER_UNESCAPE + escape(tmp.substring(0, beg)) + DELIMITER_UNESCAPE_END; - tmp = tmp.substring(beg); - - length = tmp.length; - end = 0; - - var skipA = 0; - var skipB = 0; - var skipC = 0; - - for (var i = 0; i < length; i++) { - - if (tmp[i] === '"') { - - if (skipA > 0) { - skipA--; - continue; - } - - skipA++; - } - - if (tmp[i] === '{') - skipC++; - - if (tmp[i] === '\'') { +function compile_autovendor(css) { + var avp = '/*auto*/'; + var isAuto = css.substring(0, 100).indexOf(avp) !== -1; + if (isAuto) + css = autoprefixer(css.replace(avp, '')); + 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, '}').replace(REG_CSS_12, cssmarginpadding).replace(REG_CSS_13, csscolors).trim(); +} - if (skipB > 0) { - skipB--; - continue; - } +function csscolors(text) { + return text.substring(0, 4); +} - skipB++; - } +function cssmarginpadding(text) { - if (tmp[i] === '}' && skipC > 0) { - skipC--; - continue; - } + // margin + // padding - if (skipA > 0 || skipB > 0 || skipC > 0) - continue; + var prop = ''; + var val; + var l = text.length - 1; + var last = text[l]; - if (i === length - 1) { - end = i + 1; - break; - } + if (text[0] === 'm') { + prop = 'margin:'; + val = text.substring(7, l); + } else { + prop = 'padding:'; + val = text.substring(8, l); + } - if (tmp[i] === ';' || tmp[i] === '}' || tmp[i] === '\n') { - end = i; - break; - } - } + var a = val.split(' '); - if (end === 0) - continue; + for (var i = 0; i < a.length; i++) { + if (a[i][0] === '0' && a[i].charCodeAt(1) > 58) + a[i] = '0'; + } - var cmd = tmp.substring(0, end); - tmp = tmp.substring(end); - output += '+' + cmd.substring(1); - beg = 0; - } + // 0 0 0 0 --> 0 + if (a[0] === '0' && a[1] === '0' && a[2] === '0' && a[3] === '0') + return prop + '0' + last; - var length = output.length; - var compiled = ''; + // 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; - output = code + '\n\n;compiled = \'\'' + output; - eval(output); + // 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; - if (isAuto) - compiled = autoprefixer(compiled) + // 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 prepare(compiled); + return text; } -/* - Auto vendor prefixer - @value {String} :: Raw CSS - return {String} -*/ function autoprefixer(value) { - // 'box-shadow', 'border-radius' - var prefix = ['appearance', 'column-count', 'column-gap', 'column-rule', 'display', 'transform', 'transform-origin', 'transition', 'user-select', 'animation', 'animation-name', 'animation-duration', 'animation-timing-function', 'animation-delay', 'animation-iteration-count', 'animation-direction', 'animation-play-state', 'opacity', 'background', 'background-image', 'font-smoothing']; - - value = autoprefixer_keyframes(value); - - var builder = []; - var index = 0; - var property; - - // properties - for (var i = 0; i < prefix.length; i++) { - - property = prefix[i]; - index = 0; - - while (index !== -1) { - - index = value.indexOf(property, index + 1); - - if (index === -1) - continue; - - var a = value.indexOf(';', index); - var b = value.indexOf('}', index); - - var end = Math.min(a, b); - if (end === -1) - end = Math.max(a, b); - - if (end === -1) - continue; - - // text-transform - if (property === 'transform' && value.substring(index - 1, index) === '-') - continue; - - var css = value.substring(index, end); - end = css.indexOf(':'); - - if (end === -1) - continue; - - if (css.substring(0, end + 1).replace(/\s/g, '') !== property + ':') - continue; - - builder.push({ - name: property, - property: css - }); - } - } - - var output = []; - var length = builder.length; + value = autoprefixer_keyframes(value); + + var builder = []; + var index = 0; + var property; + + // properties + for (var i = 0, length = AUTOVENDOR.length; i < length; i++) { + + property = AUTOVENDOR[i]; + index = 0; + + while (index !== -1) { + + index = value.indexOf(property, index + 1); + if (index === -1) + continue; + + var a = value.indexOf(';', index); + var b = value.indexOf('}', index); + + var end = Math.min(a, b); + if (end === -1) + end = Math.max(a, b); + + if (end === -1) + continue; + + // text-transform + var before = value.substring(index - 1, index); + var isPrefix = before === '-'; + if (isPrefix) + continue; + + var css = value.substring(index, end); + end = css.indexOf(':'); + + if (end === -1 || css.substring(0, end + 1).replace(/\s/g, '') !== property + ':') + continue; + + builder.push({ name: property, property: before + css, css: css }); + } + } + + var output = []; + var length = builder.length; + + for (var i = 0; i < length; i++) { + + var name = builder[i].name; + 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, ''); + 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; + } + + if (name === 'font-smoothing') { + updated = plus + delimiter; + updated += plus.replacer('font-smoothing', '-webkit-font-smoothing') + delimiter; + updated += plus.replacer('font-smoothing', '-moz-osx-font-smoothing'); + value = value.replacer(replace, before + '@[[' + output.length + ']]'); + output.push(updated); + continue; + } + + if (name === 'background' || name === 'background-image') { + if (property.indexOf('repeating-linear-gradient') !== -1) { + updated = plus.replacer('repeating-linear-', '-webkit-repeating-linear-') + delimiter; + updated += plus.replacer('repeating-linear-', '-moz-repeating-linear-') + delimiter; + updated += plus.replacer('repeating-linear-', '-ms-repeating-linear-') + delimiter; + updated += plus; + 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(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(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(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(replace, before + '@[[' + output.length + ']]'); + output.push(updated); + continue; + } + + if (name === 'display') { + 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; + } + + updated += '-webkit-' + plus + delimiter; + updated += '-moz-' + plus; + + if (name.indexOf('animation') === -1) + updated += delimiter + '-ms-' + plus; + + value = value.replacer(replace, before + '@[[' + output.length + ']]'); + output.push(updated); + } + + length = output.length; + for (var i = 0; i < length; i++) + value = value.replacer('@[[' + i + ']]', output[i]); + + output = null; + builder = null; + return value; +} - for (var i = 0; i < length; i++) { +function autoprefixer_keyframes(value) { - var name = builder[i].name; - property = builder[i].property; + var builder = []; + var index = 0; - var plus = property; - var delimiter = ';'; - var updated = plus + delimiter; + while (index !== -1) { - if (name === 'opacity') { + index = value.indexOf('@keyframes', index + 1); + if (index === -1) + continue; - var opacity = parseFloat(plus.replace('opacity', '').replace(':', '').replace(/\s/g, '')); - if (isNaN(opacity)) - continue; + var counter = 0; + var end = -1; - updated += 'filter:alpha(opacity=' + Math.floor(opacity * 100) + ');'; + for (var indexer = index + 10; indexer < value.length; indexer++) { - value = value.replacer(property, '@[[' + output.length + ']]'); - output.push(updated); - continue; - } + if (value[indexer] === '{') + counter++; - if (name === 'background' || name === 'background-image') { + if (value[indexer] !== '}') + continue; - if (property.indexOf('linear-gradient') === -1) - continue; + if (counter > 1) { + counter--; + continue; + } - updated = plus.replacer('linear-', '-webkit-linear-') + delimiter; - updated += plus.replacer('linear-', '-moz-linear-') + delimiter; - updated += plus.replacer('linear-', '-o-linear-') + delimiter; - updated += plus.replacer('linear-', '-ms-linear-'); - updated += plus + delimiter; + end = indexer; + break; + } - value = value.replacer(property, '@[[' + output.length + ']]'); - output.push(updated); - continue; - } + if (end === -1) + continue; - if (name === 'text-overflow') { - updated = plus + delimiter; - updated += plus.replacer('text-overflow', '-ms-text-overflow'); - value = value.replacer(property, '@[[' + output.length + ']]'); - output.push(updated); - continue; - } + var css = value.substring(index, end + 1); + builder.push({ name: 'keyframes', property: css }); + } - if (name === 'display') { + var output = []; + var length = builder.length; - if (property.indexOf('box') === -1) - continue; + for (var i = 0; i < length; i++) { - updated = plus + delimiter; - updated += plus.replacer('box', '-webkit-box') + delimiter; - updated += plus.replacer('box', '-moz-box'); + var name = builder[i].name; + var property = builder[i].property; - value = value.replacer(property, '@[[' + output.length + ']]'); - output.push(updated); - continue; - } + if (name !== 'keyframes') + continue; - updated += '-webkit-' + plus + delimiter; - updated += '-moz-' + plus; + var plus = property.substring(1); + var delimiter = '\n'; - if (name.indexOf('animation') === -1) - updated += delimiter + '-ms-' + plus; + var updated = '@' + plus + delimiter; - updated += delimiter + '-o-' + plus; + updated += '@-webkit-' + plus + delimiter; + updated += '@-moz-' + plus + delimiter; + updated += '@-o-' + plus + delimiter; - value = value.replacer(property, '@[[' + output.length + ']]'); - output.push(updated); - } + value = value.replacer(property, '@[[' + output.length + ']]'); + output.push(updated); + } - length = output.length; - for (var i = 0; i < length; i++) - value = value.replacer('@[[' + i + ']]', output[i]); + length = output.length; - output = null; - builder = null; - prefix = null; + for (var i = 0; i < length; i++) + value = value.replace('@[[' + i + ']]', output[i]); - return value; + return value; } -function autoprefixer_keyframes(value) { - - var builder = []; - var index = 0; - - while (index !== -1) { - - index = value.indexOf('@keyframes', index + 1); - if (index === -1) - continue; - - var counter = 0; - var end = -1; - - for (var indexer = index + 10; indexer < value.length; indexer++) { - - if (value[indexer] === '{') - counter++; - - if (value[indexer] !== '}') - continue; - - if (counter > 1) { - counter--; - continue; - } - - end = indexer; - break; - } - - if (end === -1) - continue; - - var css = value.substring(index, end + 1); - builder.push({ - name: 'keyframes', - property: css - }); - } - - var output = []; - var length = builder.length; - - for (var i = 0; i < length; i++) { - - var name = builder[i].name; - var property = builder[i].property; - - if (name !== 'keyframes') - continue; - - var plus = property.substring(1); - var delimiter = '\n'; - - var updated = '@' + plus + delimiter; - - updated += '@-webkit-' + plus + delimiter; - updated += '@-moz-' + plus + delimiter; - updated += '@-o-' + plus; +function minify_javascript(data) { + + var index = 0; + var output = []; + var isCS = false; + var isCI = false; + var alpha = /[0-9a-z$]/i; + var white = /\W/; + var skip = { '$': true, '_': true }; + var newlines = { '\n': 1, '\r': 1 }; + var regexp = false; + var scope, prev, next, last; + var vtmp = false; + var regvar = /^(\s)*var /; + var vindex = 0; + + while (true) { + + var c = data[index]; + var prev = data[index - 1]; + var next = data[index + 1]; + + index++; + + if (c === undefined) + break; + + if (!scope) { + + if (!regexp) { + if (c === '/' && next === '*') { + isCS = true; + continue; + } else if (c === '*' && next === '/') { + isCS = false; + index++; + continue; + } + + if (isCS) + continue; + + if (c === '/' && next === '/') { + isCI = true; + continue; + } else if (isCI && newlines[c]) { + isCI = false; + alpha.test(last) && output.push(' '); + last = ''; + continue; + } + + if (isCI) + continue; + } + + if (c === '\t' || newlines[c]) { + if (!last || !alpha.test(last)) + continue; + output.push(' '); + last = ''; + continue; + } + + if (!regexp && (c === ' ' && (white.test(prev) || white.test(next)))) { + // if (!skip[prev] && !skip[next]) + if (!skip[prev]) { + if (!skip[next] || !alpha.test(prev)) + continue; + } + } + + if (regexp) { + if ((last !== '\\' && c === '/') || (last === '\\' && c === '/' && output[output.length - 2] === '\\')) + regexp = false; + } else + regexp = (last === '=' || last === '(' || last === ':' || last === '{' || last === '[' || last === '?') && (c === '/'); + } + + if (scope && c === '\\') { + output.push(c); + output.push(next); + index++; + last = next; + continue; + } + + if (!regexp && (c === '"' || c === '\'' || c === '`')) { + + if (scope && scope !== c) { + output.push(c); + continue; + } + + if (c === scope) + scope = 0; + else + 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(); + + output.push(c); + last = c; + } + + return output.join('').trim(); +} - value = value.replacer(property, '@[[' + output.length + ']]'); - output.push(updated); - } +exports.compile_css = function(value, filename, nomarkup) { - length = output.length; + // Internal markup + if (!nomarkup) + value = markup(value, filename); - for (var i = 0; i < length; i++) - value = value.replace('@[[' + i + ']]', output[i]); + if (global.F) { + value = modificators(value, filename, 'style'); + if (F.onCompileStyle) + return F.onCompileStyle(filename, value); + } - builder = null; - output = null; + try { - return value; -} + var isVariable = false; -exports.compile_css = function(value, minify) { + value = nested(value, '', () => isVariable = true); + value = compile_autovendor(value); - if (framework.onCompileCSS !== null) - return framework.onCompileCSS('', value); + if (isVariable) + value = variablesCSS(value); - try { - return compile_jscss(value); - } catch (ex) { - framework.error(new Error('JS CSS exception: ' + ex.message)); - return ''; - } + return value; + } catch (ex) { + F.error(new Error('CSS compiler error: ' + ex.message)); + return ''; + } }; -// ********************************************************************************* -// ================================================================================= -// JavaScript compressor -// ================================================================================= -// ********************************************************************************* - -// Copyright (c) 2002 Douglas Crockford (www.crockford.com) -// -// 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. - -/* - Minify JS - @source {String} - return {String} -*/ -function JavaScript(source) { - - var EOF = -1; - var sb = []; - var theA; // int - var theB; // int - var theLookahead = EOF; // int - var index = 0; - - function jsmin() { - theA = 13; - action(3); - var indexer = 0; - while (theA !== EOF) { - switch (theA) { - case 32: - if (isAlphanum(theB)) - action(1); - else - action(2); - break; - case 13: - switch (theB) { - case 123: - case 91: - case 40: - case 43: - case 45: - action(1); - break; - case 32: - action(3); - break; - default: - if (isAlphanum(theB)) - action(1); - else - action(2); - break; - } - break; - default: - switch (theB) { - case 32: - if (isAlphanum(theA)) { - action(1); - break; - } - action(3); - break; - - case 13: - switch (theA) { - case 125: - case 93: - case 41: - case 43: - case 45: - case 34: - case 92: - action(1); - break; - default: - if (isAlphanum(theA)) - action(1); - else - action(3); - break; - } - break; - default: - action(1); - break; - } - break; - } - } - } - - function action(d) { - if (d <= 1) { - put(theA); - } - if (d <= 2) { - theA = theB; - if (theA === 39 || theA === 34) { - for (;;) { - put(theA); - theA = get(); - if (theA === theB) { - break; - } - if (theA <= 13) { - //throw new Exception(string.Format("Error: JSMIN unterminated string literal: {0}\n", theA)); - c = EOF; - return; - } - if (theA === 92) { - put(theA); - theA = get(); - } - } - } - } - if (d <= 3) { - theB = next(); - if (theB === 47 && (theA === 40 || theA === 44 || theA === 61 || - theA === 91 || theA === 33 || theA === 58 || - theA === 38 || theA === 124 || theA === 63 || - theA === 123 || theA === 125 || theA === 59 || - theA === 13)) { - put(theA); - put(theB); - for (;;) { - theA = get(); - if (theA === 47) { - break; - } else if (theA === 92) { - put(theA); - theA = get(); - } else if (theA <= 13) { - c = EOF; - return; - } - put(theA); - } - theB = next(); - } - } - } - - function next() { - var c = get(); - - if (c !== 47) - return c; - - switch (peek()) { - case 47: - for (;;) { - c = get(); - if (c <= 13) - return c; - } - break; - case 42: - get(); - for (;;) { - switch (get()) { - case 42: - if (peek() === 47) { - get(); - return 32; - } - break; - case EOF: - c = EOF; - return; - } - } - break; - default: - return c; - } - - return c; - } - - function peek() { - theLookahead = get(); - return theLookahead; - } - - function get() { - var c = theLookahead; - theLookahead = EOF; - if (c === EOF) { - c = source.charCodeAt(index++); - if (isNaN(c)) - c = EOF; - } - if (c >= 32 || c === 13 || c === EOF) { - return c; - } - if (c === 10) // \r - { - return 13; - } - return 32; - } - - function put(c) { - if (c === 13 || c === 10) - sb.push(' '); - else - sb.push(String.fromCharCode(c)); - } - - function isAlphanum(c) { - return ((c >= 97 && c <= 122) || (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || c === 95 || c === 36 || c === 92 || c > 126); - } - - jsmin(); - return sb.join(''); -} +exports.compile_javascript = function(source, filename, nomarkup) { -exports.compile_javascript = function(source, framework) { - try { - if (framework) { - if (framework.onCompileJS !== null) - return framework.onCompileJS('', source); - } + // Internal markup + if (!nomarkup) + source = markup(source, filename); - return JavaScript(source); - } catch (ex) { + if (global.F) { + source = modificators(source, filename, 'script'); + if (F.onCompileScript) + return F.onCompileScript(filename, source).trim(); + } - if (framework) - framework.error(ex, 'JavaScript compressor'); + return minify_javascript(source); +}; - return source; - } +exports.compile_html = function(source, filename, nomarkup) { + return compressCSS(compressJS(compressHTML(source, true), 0, filename, nomarkup), 0, filename, nomarkup); }; // ********************************************************************************* @@ -1340,348 +1339,318 @@ exports.compile_javascript = function(source, framework) { // THE SOFTWARE. var Buffer = require('buffer').Buffer, - s = 0, - S = { - PARSER_UNINITIALIZED: s++, - START: s++, - START_BOUNDARY: s++, - HEADER_FIELD_START: s++, - HEADER_FIELD: s++, - HEADER_VALUE_START: s++, - HEADER_VALUE: s++, - HEADER_VALUE_ALMOST_DONE: s++, - HEADERS_ALMOST_DONE: s++, - PART_DATA_START: s++, - PART_DATA: s++, - PART_END: s++, - END: s++ - }, - - f = 1, - F = { - PART_BOUNDARY: f, - LAST_BOUNDARY: f *= 2 - }, - - LF = 10, - CR = 13, - SPACE = 32, - HYPHEN = 45, - COLON = 58, - A = 97, - Z = 122, - - lower = function(c) { - return c | 0x20; - }; + s = 0, + S = { + PARSER_UNINITIALIZED: s++, + START: s++, + START_BOUNDARY: s++, + HEADER_FIELD_START: s++, + HEADER_FIELD: s++, + HEADER_VALUE_START: s++, + HEADER_VALUE: s++, + HEADER_VALUE_ALMOST_DONE: s++, + HEADERS_ALMOST_DONE: s++, + PART_DATA_START: s++, + PART_DATA: s++, + PART_END: s++, + END: s++ + }, + + f = 1, + FB = { + PART_BOUNDARY: f, + LAST_BOUNDARY: f *= 2 + }, + + LF = 10, + CR = 13, + SPACE = 32, + HYPHEN = 45, + COLON = 58, + A = 97, + Z = 122, + + lower = function(c) { + return c | 0x20; + }; for (s in S) { - exports[s] = S[s]; + exports[s] = S[s]; } function MultipartParser() { - this.boundary = null; - this.boundaryChars = null; - this.lookbehind = null; - this.state = S.PARSER_UNINITIALIZED; - this.index = null; - this.flags = 0; + this.boundary = null; + this.boundaryChars = null; + this.lookbehind = null; + this.state = S.PARSER_UNINITIALIZED; + this.index = null; + this.flags = 0; } + exports.MultipartParser = MultipartParser; +const MultipartParserProto = MultipartParser.prototype; MultipartParser.stateToString = function(stateNumber) { - for (var state in S) { - var number = S[state]; - if (number === stateNumber) return state; - } + for (var state in S) { + var number = S[state]; + if (number === stateNumber) return state; + } }; -MultipartParser.prototype.initWithBoundary = function(str) { - var self = this; - self.boundary = new Buffer(str.length + 4); - - if (framework.versionNode >= 1111) { - self.boundary.write('\r\n--', 0, 'ascii'); - self.boundary.write(str, 4, 'ascii'); - } else { - self.boundary.write('\r\n--', 'ascii', 0); - self.boundary.write(str, 'ascii', 4); - } - - self.lookbehind = new Buffer(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; - } +MultipartParserProto.initWithBoundary = function(str) { + var self = this; + self.boundary = Buffer.alloc(str.length + 4); + self.boundary.write('\r\n--', 0, 'ascii'); + self.boundary.write(str, 4, 'ascii'); + 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) { - var self = this, - i = 0, - len = buffer.length, - prevIndex = self.index, - index = self.index, - state = self.state, - flags = self.flags, - lookbehind = self.lookbehind, - boundary = self.boundary, - boundaryChars = self.boundaryChars, - boundaryLength = self.boundary.length, - boundaryEnd = boundaryLength - 1, - bufferLength = buffer.length, - c, - cl, - - mark = function(name) { - self[name + 'Mark'] = i; - }, - clear = function(name) { - delete self[name + 'Mark']; - }, - callback = function(name, buffer, start, end) { - if (start !== undefined && start === end) { - return; - } - - var callbackSymbol = 'on' + name.substr(0, 1).toUpperCase() + name.substr(1); - if (callbackSymbol in self) { - self[callbackSymbol](buffer, start, end); - } - }, - dataCallback = function(name, clear) { - var markSymbol = name + 'Mark'; - if (!(markSymbol in self)) { - return; - } - - if (!clear) { - callback(name, buffer, self[markSymbol], buffer.length); - self[markSymbol] = 0; - } else { - callback(name, buffer, self[markSymbol], i); - delete self[markSymbol]; - } - }; - - 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) { - flags |= F.LAST_BOUNDARY; - } else if (c != CR) { - return i; - } - index++; - break; - } else if (index - 1 == boundary.length - 2) { - if (flags & F.LAST_BOUNDARY && c == HYPHEN) { - callback('end'); - state = S.END; - flags = 0; - } else if (!(flags & F.LAST_BOUNDARY) && c == LF) { - index = 0; - callback('partBegin'); - state = S.HEADER_FIELD_START; - } else { - return i; - } - break; - } - - if (c != boundary[index + 2]) { - index = -2; - } - 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'); - state = S.HEADERS_ALMOST_DONE; - break; - } - - index++; - if (c == HYPHEN) { - break; - } - - if (c == COLON) { - if (index == 1) { - // empty header field - return i; - } - dataCallback('headerField', true); - state = S.HEADER_VALUE_START; - break; - } - - 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); - callback('headerEnd'); - 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 === 0) { - // boyer-moore derrived algorithm to safely skip non-boundary data - i += boundaryEnd; - while (i < bufferLength && !(buffer[i] in boundaryChars)) { - i += boundaryLength; - } - i -= boundaryEnd; - c = buffer[i]; - } - - if (index < boundary.length) { - if (boundary[index] == c) { - if (index === 0) { - dataCallback('partData', true); - } - index++; - } else { - index = 0; - } - } else if (index == boundary.length) { - index++; - if (c == CR) { - // CR = part boundary - flags |= F.PART_BOUNDARY; - } else if (c == HYPHEN) { - // HYPHEN = end boundary - flags |= F.LAST_BOUNDARY; - } else { - index = 0; - } - } else if (index - 1 == boundary.length) { - if (flags & F.PART_BOUNDARY) { - index = 0; - if (c == LF) { - // unset the PART_BOUNDARY flag - flags &= ~F.PART_BOUNDARY; - callback('partEnd'); - callback('partBegin'); - state = S.HEADER_FIELD_START; - break; - } - } else if (flags & F.LAST_BOUNDARY) { - if (c == HYPHEN) { - callback('partEnd'); - callback('end'); - state = S.END; - flags = 0; - } else { - index = 0; - } - } else { - index = 0; - } - } - - if (index > 0) { - // when matching a possible boundary, keep a lookbehind reference - // in case it turns out to be a false lead - lookbehind[index - 1] = c; - } else if (prevIndex > 0) { - // if our boundary turned out to be rubbish, the captured lookbehind - // belongs to partData - callback('partData', lookbehind, 0, prevIndex); - prevIndex = 0; - mark('partData'); - - // reconsider the current character even so it interrupted the sequence - // it could be the beginning of a new sequence - i--; - } - break; - case S.END: - break; - default: - return i; - } - } - - dataCallback('headerField'); - dataCallback('headerValue'); - dataCallback('partData'); - - self.index = index; - self.state = state; - self.flags = flags; - - return len; +MultipartParserProto.write = function(buffer) { + var self = this, + i = 0, + len = buffer.length, + prevIndex = self.index, + index = self.index, + state = self.state, + flags = self.flags, + lookbehind = self.lookbehind, + boundary = self.boundary, + boundaryChars = self.boundaryChars, + boundaryLength = self.boundary.length, + boundaryEnd = boundaryLength - 1, + bufferLength = buffer.length, + c, + cl, + mark = function(name) { + self[name + 'Mark'] = i; + }, + clear = function(name) { + delete self[name + 'Mark']; + }, + callback = function(name, buffer, start, end) { + if (start !== undefined && start === end) + return; + var callbackSymbol = 'on' + name.substr(0, 1).toUpperCase() + name.substr(1); + if (callbackSymbol in self) + self[callbackSymbol](buffer, start, end); + }, + dataCallback = function(name, clear) { + var markSymbol = name + 'Mark'; + if (!(markSymbol in self)) + return; + if (!clear) { + callback(name, buffer, self[markSymbol], buffer.length); + self[markSymbol] = 0; + } else { + callback(name, buffer, self[markSymbol], i); + delete self[markSymbol]; + } + }; + + 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) + flags |= FB.LAST_BOUNDARY; + else if (c !== CR) + return i; + index++; + break; + } else if (index - 1 === boundary.length - 2) { + if (flags & FB.LAST_BOUNDARY && c === HYPHEN) { + callback('end'); + state = S.END; + flags = 0; + } else if (!(flags & FB.LAST_BOUNDARY) && c === LF) { + index = 0; + callback('partBegin'); + state = S.HEADER_FIELD_START; + } else + return i; + break; + } + + if (c !== boundary[index + 2]) + index = -2; + 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'); + state = S.HEADERS_ALMOST_DONE; + break; + } + + index++; + if (c === HYPHEN) + break; + + if (c === COLON) { + // empty header field + if (index === 1) + return i; + dataCallback('headerField', true); + state = S.HEADER_VALUE_START; + break; + } + + 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); + callback('headerEnd'); + 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; + while (i < bufferLength && !(buffer[i] in boundaryChars)) + i += boundaryLength; + i -= boundaryEnd; + c = buffer[i]; + } + + if (index < boundary.length) { + if (boundary[index] === c) { + if (!index) + dataCallback('partData', true); + index++; + } else + index = 0; + } else if (index === boundary.length) { + index++; + if (c === CR) { + // CR = part boundary + flags |= FB.PART_BOUNDARY; + } else if (c === HYPHEN) { + // HYPHEN = end boundary + flags |= FB.LAST_BOUNDARY; + } else + index = 0; + } else if (index - 1 === boundary.length) { + if (flags & FB.PART_BOUNDARY) { + index = 0; + if (c === LF) { + // unset the PART_BOUNDARY flag + flags &= ~FB.PART_BOUNDARY; + callback('partEnd'); + callback('partBegin'); + state = S.HEADER_FIELD_START; + break; + } + } else if (flags & FB.LAST_BOUNDARY) { + if (c === HYPHEN) { + callback('partEnd'); + callback('end'); + state = S.END; + flags = 0; + } else + index = 0; + } else + index = 0; + } + + if (index) { + // when matching a possible boundary, keep a lookbehind reference + // in case it turns out to be a false lead + lookbehind[index - 1] = c; + } else if (prevIndex) { + // if our boundary turned out to be rubbish, the captured lookbehind + // belongs to partData + callback('partData', lookbehind, 0, prevIndex); + prevIndex = 0; + mark('partData'); + // reconsider the current character even so it interrupted the sequence + // it could be the beginning of a new sequence + i--; + } + break; + + case S.END: + break; + + default: + return i; + } + } + + dataCallback('headerField'); + dataCallback('headerValue'); + dataCallback('partData'); + + self.index = index; + self.state = state; + self.flags = flags; + + return len; }; -MultipartParser.prototype.end = function() { - - var self = this; - - var callback = function(self, name) { - var callbackSymbol = 'on' + name.substr(0, 1).toUpperCase() + name.substr(1); - if (callbackSymbol in self) { - self[callbackSymbol](); - } - }; - - if ((self.state == S.HEADER_FIELD_START && self.index === 0) || - (self.state == S.PART_DATA && self.index == self.boundary.length)) { - callback(self, 'partEnd'); - callback(self, 'end'); - } else if (self.state != S.END) { - callback(self, 'partEnd'); - callback(self, 'end'); - return new Error('MultipartParser.end(): stream ended unexpectedly: ' + self.explain()); - } +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(); + } else if (this.state != S.END) { + this.onPartEnd && this.onPartEnd(); + this.onEnd && this.onEnd(); + return new Error('MultipartParser.end(): stream ended unexpectedly: ' + this.explain()); + } }; -MultipartParser.prototype.explain = function() { - return 'state = ' + MultipartParser.stateToString(this.state); +MultipartParserProto.explain = function() { + return 'state = ' + MultipartParser.stateToString(this.state); }; // ********************************************************************************* @@ -1690,1057 +1659,1808 @@ MultipartParser.prototype.explain = function() { // ================================================================================= // ********************************************************************************* -/* - View class - @controller {Controller} - return {View} -*/ -function View(controller) { - this.controller = controller; - this.cache = controller.cache; - this.prefix = controller.prefix; +function view_parse_localization(content, language) { + + var is = false; + + content = content.replace(REG_NOTRANSLATE, function() { + is = true; + return ''; + }).trim(); + + if (is) + return content; + + var command = view_find_localization(content, 0); + var output = ''; + var end = 0; + + if (!command) + return content; + + while (command) { + + if (command) + output += content.substring(end ? end + 1 : 0, command.beg) + (command.command ? localize(language, command) : ''); + + end = command.end; + command = view_find_localization(content, command.end); + } + + output += content.substring(end + 1); + return output; } -/* - Content class - @controller {Controller} - return {Content} -*/ -function Content(controller) { - this.controller = controller; - this.cache = controller.cache; - this.prefix = controller.prefix; +// Escaping ", ' and ` chars +function localize(language, command) { + + !language && (language = 'default'); + + if (F.resources[language] && F.resources[language].$empty) + return command.command; + + var output = F.translate(language, command.command); + + if (command.escape) { + var index = 0; + while (true) { + index = output.indexOf(command.escape, index); + if (index === -1) + break; + var c = output[index - 1]; + if (c !== '\\') { + output = output.substring(0, index) + '\\' + output.substring(index); + index++; + } else + index += 2; + } + } + + return output; } -/** - * View parser - * @param {String} content - * @param {Boolean} minify - * @return {Function} - */ -function view_parse(content, minify) { - - content = removeComments(compressCSS(compressJS(content, 0, framework), 0, framework)); - - var DELIMITER = '\''; - var DELIMITER_UNESCAPE = 'unescape(\''; - var DELIMITER_UNESCAPE_END = '\')'; - var SPACE = ' '; - var builder = 'var $EMPTY=\'\';var $length=0;var $source=null;var $tmp=index;var $output=$EMPTY'; - var command = view_find_command(content, 0); - var compressed = ''; - - function escaper(value) { - value = compressHTML(value, minify); - if (value === '') - return '$EMPTY'; - if (value.match(/\n|\t|\r|\'|\\/) !== null) - return DELIMITER_UNESCAPE + escape(value) + DELIMITER_UNESCAPE_END - return DELIMITER + value + DELIMITER; - } - - if (command === null) - builder += '+' + escaper(content); - - var old = null; - var newCommand = ''; - var tmp = ''; - var index = 0; - var counter = 0; - var functions = []; - var functionsName = []; - var isFN = false; - var isSECTION = false; - var builderTMP = ''; - var sectionName = ''; - - while (command !== null) { - - if (old !== null) { - var text = content.substring(old.end + 1, command.beg); - if (text !== '') { - if (view_parse_plus(builder)) - builder += '+'; - builder += escaper(text); - } - } else { - var text = content.substring(0, command.beg); - if (text !== '') { - if (view_parse_plus(builder)) - builder += '+'; - builder += escaper(text); - } - } - - var cmd = content.substring(command.beg + 2, command.end); - var cmd8 = cmd.substring(0, 8); - - if (cmd8 === 'section ' && cmd.lastIndexOf(')') === -1) { - - builderTMP = builder; - builder = '+(function(){var $output=$EMPTY'; - sectionName = cmd.substring(8); - isSECTION = true; - isFN = true; - - } else if (cmd.substring(0, 7) === 'helper ') { - - builderTMP = builder; - builder = 'function ' + cmd.substring(7).trim() + '{var $output=$EMPTY'; - isFN = true; - functionsName.push(cmd.substring(7, cmd.indexOf('(', 7)).trim()); - - } else if (cmd8 === 'foreach ') { - - counter++; - - if (cmd.indexOf('foreach var ') !== -1) - cmd = cmd.replace(' var ', SPACE); - - newCommand = (cmd.substring(8, cmd.indexOf(SPACE, 8)) || '').trim(); - index = cmd.indexOf('['); - - if (index === -1) - index = cmd.lastIndexOf(SPACE); - - builder += '+(function(){var $source=' + cmd.substring(index).trim() + ';if (!($source instanceof Array) || source.length === 0)return $EMPTY;var $length=$source.length;var $output=$EMPTY;var index=0;for(var i=0;i<$length;i++){index = i;var ' + newCommand + '=$source[i];$output+=$EMPTY'; - - } else if (cmd === 'end') { - - if (isFN && counter <= 0) { - - counter = 0; - - if (isSECTION) { - builder = builderTMP + builder + ';repository[\'$section_' + sectionName + '\']=$output;return $EMPTY})()'; - builderTMP = ''; - } else { - builder += ';return $output;}'; - functions.push(builder); - builder = builderTMP; - builderTMP = ''; - } - - isSECTION = false; - isFN = false; - - } else { - counter--; - builder += '}return $output;})()'; - newCommand = ''; - } - - } else if (cmd.substring(0, 3) === 'if ') { - builder += ';if (' + cmd.substring(3) + '){$output+=$EMPTY'; - } else if (cmd === 'else') { - builder += '} else {$output+=$EMPTY'; - } else if (cmd === 'endif') { - builder += '}$output+=$EMPTY' - } else { - tmp = view_prepare(command.command, newCommand, functionsName); - - if (tmp.length > 0) { - if (view_parse_plus(builder)) - builder += '+'; - builder += tmp; - } - } - - old = command; - command = view_find_command(content, command.end); - } - - if (old !== null) { - var text = content.substring(old.end + 1); - if (text.length > 0) - builder += '+' + escaper(text); - } - - var fn = '(function(self,repository,model,session,get,post,url,global,helpers,user,config,functions,index,sitemap,output){' + (functions.length > 0 ? functions.join('') + ';' : '') + 'var controller=self;' + builder + ';return $output;})'; - return eval(fn); +var VIEW_IF = { 'if ': 1, 'if(': 1 }; + +function view_parse(content, minify, filename, controller) { + + content = removeComments(content).ROOT(); + + var nocompressHTML = false; + var nocompressJS = false; + var nocompressCSS = false; + + content = content.replace(REG_NOCOMPRESS, function(text) { + + var index = text.lastIndexOf(' '); + if (index === -1) + return ''; + + switch (text.substring(index, text.length - 1).trim()) { + case 'all': + nocompressHTML = true; + nocompressJS = true; + nocompressCSS = true; + break; + case 'html': + nocompressHTML = true; + break; + case 'js': + case 'script': + case 'javascript': + nocompressJS = true; + break; + case 'css': + case 'style': + nocompressCSS = true; + break; + } + + return ''; + }).trim(); + + if (!nocompressJS) + content = compressJS(content, 0, filename, true); + + if (!nocompressCSS) + content = compressCSS(content, 0, filename, true); + + content = F.$versionprepare(content); + + if (!nocompressHTML) + content = compressView(content, minify, filename); + + var DELIMITER = '\''; + var SPACE = ' '; + var builder = 'var $EMPTY=\'\';var $length=0;var $source=null;var $tmp=index;var $output=$EMPTY'; + var command = view_find_command(content, 0); + var isFirst = false; + var txtindex = -1; + var index = 0; + var isCookie = false; + + function escaper(value) { + + var is = REG_TAGREMOVE.test(value); + + if (!nocompressHTML) { + // value = compressHTML(value, minify, true); + } else if (!isFirst) { + isFirst = true; + value = value.replace(/^\s+/, ''); + } + + if (!value) + return '$EMPTY'; + + if (!nocompressHTML && is) + value += ' '; + + txtindex = $VIEWCACHE.indexOf(value); + + if (txtindex === -1) { + txtindex = $VIEWCACHE.length; + $VIEWCACHE.push(value); + } + + return '$VIEWCACHE[' + txtindex + ']'; + } + + if (!command) + builder += '+' + escaper(content); + + index = 0; + + var old = null; + var newCommand = ''; + var tmp = ''; + var counter = 0; + var functions = []; + var functionsName = []; + var isFN = false; + var isSECTION = false; + 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) { + if (view_parse_plus(builder)) + builder += '+'; + builder += escaper(text); + } + } else { + text = content.substring(0, command.beg); + if (text) { + if (view_parse_plus(builder)) + builder += '+'; + builder += escaper(text); + } + } + + var cmd = content.substring(command.beg + 2, command.end).trim(); + + var cmd8 = cmd.substring(0, 8); + var cmd7 = cmd.substring(0, 7); + + if (cmd === 'continue' || cmd === 'break') { + builder += ';' + cmd + ';'; + old = command; + command = view_find_command(content, command.end); + continue; + } + + // cmd = cmd.replace + command.command = command.command.replace(REG_HELPERS, function(text) { + var index = text.indexOf('('); + return index === - 1 ? text : text.substring(0, index) + '.call(self' + (text.endsWith('()') ? ')' : ',' + text.substring(index + 1)); + }); + + if (cmd[0] === '\'' || cmd[0] === '"') { + if (cmd[1] === '%') { + var t = CONF[cmd.substring(2, cmd.length - 1)]; + if (t != null) + 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).trim() : '') + '\','; + builder = ''; + sectionName = cmd.substring(8); + isCOMPILATION = true; + isFN = true; + + } else if (cmd8 === 'section ' && cmd.lastIndexOf(')') === -1) { + + builderTMP = builder; + builder = '+(function(){var $output=$EMPTY'; + sectionName = cmd.substring(8); + isSECTION = true; + isFN = true; + } else if (cmd7 === 'helper ') { + + builderTMP = builder; + builder = 'function ' + cmd.substring(7).trim() + '{var $output=$EMPTY'; + isFN = true; + functionsName.push(cmd.substring(7, cmd.indexOf('(', 7)).trim()); + + } else if (cmd8 === 'foreach ') { + + counter++; + + if (cmd.indexOf('foreach var ') !== -1) + cmd = cmd.replace(' var ', SPACE); + + cmd = view_prepare_keywords(cmd); + newCommand = (cmd.substring(8, cmd.indexOf(SPACE, 8)) || '').trim(); + index = cmd.trim().indexOf(SPACE, newCommand.length + 10); + + if (index === -1) + index = cmd.indexOf('[', newCommand.length + 10); + + builder += '+(function(){var $source=' + cmd.substring(index).trim() + ';if(!($source instanceof Array))$source=framework_utils.ObjectToArray($source);if(!$source.length)return $EMPTY;var $length=$source.length;var $output=$EMPTY;var index=0;for(var $i=0;$i<$length;$i++){index=$i;var ' + newCommand + '=$source[$i];$output+=$EMPTY'; + } else if (cmd === 'end') { + + if (isFN && counter <= 0) { + counter = 0; + + if (isCOMPILATION) { + builder = builderTMP + 'unescape($EMPTY' + builder + '),model) || $EMPTY)'; + builderTMP = ''; + } else if (isSECTION) { + builder = builderTMP + builder + ';repository[\'$section_' + sectionName + '\']=repository[\'$section_' + sectionName + '\']?repository[\'$section_' + sectionName + '\']+$output:$output;return $EMPTY})()'; + builderTMP = ''; + } else { + builder += ';return $output;}'; + functions.push(builder); + builder = builderTMP; + builderTMP = ''; + } + + isSECTION = false; + isCOMPILATION = false; + isFN = false; + + } else { + counter--; + builder += '}return $output})()'; + newCommand = ''; + } + + } 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') { + builder += '} else {$output+=$EMPTY'; + } else if (cmd === 'endif' || cmd === 'fi') { + builder += '}$output+=$EMPTY'; + } else { + + tmp = view_prepare(command.command, newCommand, functionsName, controller, components); + var can = false; + + // Inline rendering is supported only in release mode + if (RELEASE && tmp.indexOf('+') === -1 && REG_SKIP_1.test(tmp) && !REG_SKIP_2.test(tmp)) { + for (var a = 0, al = RENDERNOW.length; a < al; a++) { + if (tmp.startsWith(RENDERNOW[a])) { + if (!a) { + var isMeta = tmp.indexOf('\'meta\'') !== -1; + var isHead = tmp.indexOf('\'head\'') !== -1; + 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 (tmpimp) + builder += '+self.$import(' + tmpimp + ')'; + } + } + + if (tmp !== 'self.$import()') + can = true; + + break; + } + } + } + + if (can && !counter) { + try { + + 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) { + txtindex = $VIEWCACHE.length; + $VIEWCACHE.push(r); + } + builder += '+$VIEWCACHE[' + txtindex + ']'; + } + } catch (e) { + + 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)) + builder += '+'; + builder += wrapTryCatch(tmp, command.command, command.line); + } + } else if (tmp) { + if (view_parse_plus(builder)) + builder += '+'; + if (tmp.substring(1, 4) !== '@{-' && tmp.substring(0, 11) !== 'self.$view') + builder += wrapTryCatch(tmp, command.command, command.line); + else + builder += tmp; + } + } + + old = command; + command = view_find_command(content, command.end); + } + + if (old) { + text = content.substring(old.end + 1); + if (text) + builder += '+' + escaper(text); + } + + 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 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()); + } + return fn; } -function view_parse_plus(builder) { - var c = builder[builder.length - 1]; - if (c !== '!' && c !== '?' && c !== '+' && c !== '.' && c !== ':') - return true; - return false; +function view_prepare_keywords(cmd) { + return cmd.replace(REG_SITEMAP, text => ' self.' + text.trim()); } -function view_prepare(command, dynamicCommand, functions) { +function wrapTryCatch(value, command, line) { + return F.isDebug ? ('(function(){try{return ' + value + '}catch(e){throw new Error(unescape(\'' + escape(command) + '\') + \' - Line: ' + line + ' - \' + e.message.toString());}return $EMPTY})()') : value; +} - var a = command.indexOf('.'); - var b = command.indexOf('('); - var c = command.indexOf('['); +function view_parse_plus(builder) { + var c = builder[builder.length - 1]; + return c !== '!' && c !== '?' && c !== '+' && c !== '.' && c !== ':'; +} - var max = []; - var tmp = 0; - - if (a !== -1) - max.push(a); - - if (b !== -1) - max.push(b); - - if (c !== -1) - max.push(c); - - var index = Math.min.apply(this, max); - - if (index === -1) - index = command.length; - - var name = command.substring(0, index); - - if (name === dynamicCommand) - return '(' + command + ').toString().encode()'; - - if (name[0] === '!' && name.substring(1) === dynamicCommand) - return '(' + command.substring(1) + ').toString()'; - - switch (name) { - case 'foreach': - case 'end': - return ''; - - case 'section': - tmp = command.indexOf('('); - if (tmp === -1) - return ''; - return '(repository[\'$section_' + command.substring(tmp + 1, command.length - 1).replace(/\'/g, '') + '\'] || \'\')'; - - case 'controller': - case 'repository': - case 'model': - case 'get': - case 'post': - case 'global': - case 'session': - case 'user': - case 'config': - case 'model': - - if (view_is_assign(command)) - return 'self.$set(' + command + ')'; - - return '(' + command + ').toString().encode()'; - - case 'functions': - return '(' + command + ').toString().encode()'; - - case '!controller': - case '!repository': - case '!get': - case '!post': - case '!global': - case '!session': - case '!user': - case '!config': - case '!functions': - case '!model': - return '(' + command.substring(1) + ')'; - - case 'body': - return 'output'; - - case 'resource': - return '(self.' + command + ').toString().encode()'; - - case '!resource': - return '(self.' + command.substring(1) + ')'; - - case 'host': - case 'hostname': - if (command.indexOf('(') === -1) - return 'self.host()'; - return 'self.' + command; - - case 'url': - if (command.indexOf('(') !== -1) - return 'self.$' + command; - return command = 'url'; - - case 'title': - case 'description': - case 'keywords': - if (command.indexOf('(') !== -1) - return 'self.' + command; - return '(repository[\'$' + command + '\'] || \'\').toString().encode()'; - - case '!title': - case '!description': - case '!keywords': - return '(repository[\'$' + command.substring(1) + '\'] || \'\')'; - - case 'head': - if (command.indexOf('(') !== -1) - return 'self.$' + command; - return 'self.' + command + '()'; - - case 'place': - case 'sitemap': - if (command.indexOf('(') !== -1) - return 'self.$' + command; - return '(repository[\'$' + command + '\'] || \'\')'; - - case 'meta': - if (command.indexOf('(') !== -1) - return 'self.$' + command; - return 'self.$meta()'; - - case 'js': - case 'script': - case 'css': - case 'favicon': - return 'self.$' + command + (command.indexOf('(') === -1 ? '()' : ''); - - case 'index': - return '(' + command + ')'; - - case 'routeJS': - case 'routeCSS': - case 'routeImage': - case 'routeFont': - case 'routeDownload': - case 'routeVideo': - case 'routeStatic': - return 'self.' + command; - - case 'ng': - case 'ngTemplate': - case 'ngController': - case 'ngCommon': - case 'ngInclude': - case 'ngLocale': - case 'ngService': - case 'ngFilter': - case 'ngDirective': - case 'ngResource': - case 'ngStyle': - return 'self.$' + command; - - case 'canonical': - case 'checked': - case 'helper': - case 'component': - case 'componentToggle': - case 'content': - case 'contentToggle': - case 'currentContent': - case 'currentCSS': - case 'currentDownload': - case 'currentImage': - case 'currentJS': - case 'currentTemplate': - case 'currentVideo': - case 'currentView': - case 'disabled': - case 'dns': - case 'download': - case 'etag': - case 'header': - case 'image': - case 'json': - case 'layout': - case 'modified': - case 'next': - case 'options': - case 'prefetch': - case 'prerender': - case 'prev': - case 'readonly': - case 'selected': - case 'template': - case 'templateToggle': - case 'view': - case 'viewToggle': - return 'self.$' + command; - case 'radio': - case 'text': - case 'checkbox': - case 'hidden': - case 'textarea': - case 'password': - return 'self.$' + exports.appendModel(command); - default: - - if (framework.helpers[name]) - return 'helpers.' + view_insert_call(command); - - return functions.indexOf(name) === -1 ? command[0] === '!' ? command.substring(1) + '.toString()' : command + '.toString().encode()' : command + '.toString()'; - } - - return command; +function view_prepare(command, dynamicCommand, functions, controller, components) { + + var a = command.indexOf('.'); + var b = command.indexOf('('); + var c = command.indexOf('['); + + var max = []; + var tmp = 0; + + if (a !== -1) + max.push(a); + + if (b !== -1) + max.push(b); + + if (c !== -1) + max.push(c); + + var index = Math.min.apply(this, max); + + if (index === -1) + index = command.length; + + var name = command.substring(0, index); + if (name === dynamicCommand) + return '$STRING(' + command + ').encode()'; + + if (name[0] === '!' && name.substring(1) === dynamicCommand) + return '$STRING(' + command.substring(1) + ')'; + + switch (name) { + + case 'foreach': + 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, '') + '\'] || \'\')'; + + case 'log': + case 'LOG': + return '(' + (name === 'log' ? 'F.' : '') + command + '?$EMPTY:$EMPTY)'; + + case 'logger': + case 'LOGGER': + return '(' + (name === 'logger' ? 'F.' : '') + command + '?$EMPTY:$EMPTY)'; + + case 'console': + return '(' + command + '?$EMPTY:$EMPTY)'; + + case '!cookie': + return '$STRING(' + command + ')'; + 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 '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()'); + + case 'body': + return view_is_assign(command) ? ('self.$set(' + command + ')') : command.lastIndexOf('.') === -1 ? 'output' : ('$STRING(' + command + ').encode()'); + + case 'files': + case 'mobile': + case 'continue': + case 'break': + case 'language': + case 'TRANSLATE': + case 'helpers': + return command; + + case 'cookie': + case 'isomorphic': + case 'settings': + case 'CONFIG': + case 'FUNC': + case 'function': + case 'MODEL': + case 'SCHEMA': + case 'MODULE': + case 'functions': + return '$STRING(' + command + ').encode()'; + + case '!M': + case '!R': + case '!G': + case '!controller': + case '!repository': + case '!get': + case '!post': + case '!body': + case '!query': + case '!global': + case '!session': + case '!user': + case '!config': + case '!CONF': + case '!functions': + case '!model': + case '!CONFIG': + case '!SCHEMA': + case '!function': + case '!MODEL': + case '!MODULE': + return '$STRING(' + command.substring(1) + ')'; + + case 'resource': + return '$STRING(self.' + command + ').encode()'; + case 'RESOURCE': + return '$STRING(' + command + ').encode()'; + + case '!resource': + return '$STRING(self.' + command.substring(1) + ')'; + case '!RESOURCE': + return '$STRING(' + command.substring(1) + ')'; + + case 'host': + case 'hostname': + return command.indexOf('(') === -1 ? 'self.host()' : 'self.' + command; + + case 'href': + return command.indexOf('(') === -1 ? 'self.href()' : 'self.' + command; + + case 'url': + return command.indexOf('(') === -1 ? 'self.' + command : 'self.$' + command; + + case 'title': + case 'description': + case 'keywords': + case 'author': + return command.indexOf('(') === -1 ? '(repository[\'$' + command + '\'] || \'\').toString().encode()' : 'self.$' + command; + + case 'title2': + return 'self.$' + command; + + case '!title': + case '!description': + case '!keywords': + case '!author': + return '(repository[\'$' + command.substring(1) + '\'] || \'\')'; + + case 'head': + return command.indexOf('(') === -1 ? 'self.' + command + '()' : 'self.$' + command; + + 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': + return command.indexOf('(') === -1 ? 'self.$meta()' : 'self.$' + command; + + case 'import': + case 'favicon': + case 'js': + case 'css': + case 'script': + case 'absolute': + return 'self.$' + command + (command.indexOf('(') === -1 ? '()' : ''); + + case 'components': + + 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 ? '()' : ''); + + case 'index': + return '(' + command + ')'; + + case 'component': + + 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 'routeScript': + case 'routeCSS': + case 'routeStyle': + case 'routeImage': + case 'routeFont': + case 'routeDownload': + case 'routeStatic': + 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': + case 'helper': + case 'view': + case 'layout': + case 'image': + case 'template': + case 'templateToggle': + case 'viewCompile': + case 'view_compile': + case 'viewToggle': + case 'download': + case 'selected': + case 'disabled': + case 'checked': + case 'header': + case 'options': + case 'readonly': + case 'canonical': + case 'dns': + case 'next': + case 'prefetch': + case 'prerender': + case 'prev': + return 'self.$' + command; + + case 'now': + return '(new Date()' + command.substring(3) + ')'; + + case 'radio': + case 'text': + case 'checkbox': + case 'hidden': + case 'textarea': + case 'password': + 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 + ')')); + } } function view_insert_call(command) { - var beg = command.indexOf('('); - if (beg === -1) - return command; + var beg = command.indexOf('('); + if (beg === -1) + return command; - var length = command.length; - var count = 0; + var length = command.length; + var count = 0; - for (var i = beg + 1; i < length; i++) { + for (var i = beg + 1; i < length; i++) { - var c = command[i]; + var c = command[i]; - if (c !== '(' && c !== ')') - continue; + if (c !== '(' && c !== ')') + continue; - if (c === '(') { - count++; - continue; - } + if (c === '(') { + count++; + continue; + } - if (count > 0) { - count--; - continue; - } + if (count > 0) { + count--; + continue; + } - return command.substring(0, beg) + '.call(self, ' + command.substring(beg + 1); - } + var arg = command.substring(beg + 1); + return command.substring(0, beg) + '.call(self' + (arg.length > 1 ? ',' + arg : ')'); + } - return command; + return command; } function view_is_assign(value) { - var length = value.length; - var skip = 0; - var plus = 0; + var length = value.length; + var skip = 0; - for (var i = 0; i < length; i++) { + for (var i = 0; i < length; i++) { - var c = value[i]; + var c = value[i]; - if (c === '[') { - skip++; - continue; - } + if (c === '[') { + skip++; + continue; + } - if (c === ']') { - skip--; - continue; - } + if (c === ']') { + skip--; + continue; + } - var next = value[i + 1] || ''; + var next = value[i + 1] || ''; - if (c === '+' && (next === '+' || next === '=')) { - if (skip === 0) - return true; - } + if (c === '+' && (next === '+' || next === '=')) { + if (!skip) + return true; + } - if (c === '-' && (next === '-' || next === '=')) { - if (skip === 0) - return true; - } + if (c === '-' && (next === '-' || next === '=')) { + if (!skip) + return true; + } - if (c === '*' && (next === '*' || next === '=')) { - if (skip === 0) - return true; - } + if (c === '*' && (next === '*' || next === '=')) { + if (!skip) + return true; + } - if (c === '=') { - if (skip === 0) - return true; - } + if (c === '=') { + if (!skip) + return true; + } + } + return false; +} - } +function view_find_command(content, index, entire) { - return false; -} + index = content.indexOf('@{', index); + if (index === -1) + return null; + var length = content.length; + var count = 0; -function view_find_command(content, index) { + for (var i = index + 2; i < length; i++) { + var c = content[i]; - var index = content.indexOf('@{', index); - if (index === -1) - return null; + if (c === '{') { + count++; + continue; + } - var length = content.length; - var count = 0; + if (c !== '}') + continue; + else if (count > 0) { + count--; + continue; + } - for (var i = index + 2; i < length; i++) { - var c = content[i]; + var command = content.substring(index + 2, i).trim(); - if (c === '{') { - count++; - continue; - } + // @{{ SKIP }} + if (command[0] === '{') + return view_find_command(content, index + 1); - if (c !== '}') - continue; - else { - if (count > 0) { - count--; - continue; - } - } + var obj = { beg: index, end: i, line: view_line_counter(content.substr(0, index)), command: command }; - return { - beg: index, - end: i, - command: content.substring(index + 2, i).trim() - }; - } + if (entire) + obj.phrase = content.substring(index, i + 1); - return null; + return obj; + } + + return null; } -function removeCondition(text, beg) { +function view_line_counter(value) { + var count = value.match(/\n/g); + return count ? count.length : 0; +} - if (beg) { - if (text[0] === '+') - return text.substring(1, text.length); - } else { - if (text[text.length - 1] === '+') - return text.substring(0, text.length - 1); - } +function view_find_localization(content, index) { - return text; -} + index = content.indexOf('@(', index); + if (index === -1) + return null; -function removeComments(html) { - var tagBeg = ''; - var beg = html.indexOf(tagBeg); - var end = 0; + var length = content.length; + var count = 0; + var beg = content[index - 1]; + var esc = ''; - while (beg !== -1) { - end = html.indexOf(tagEnd, beg + 4); + for (var i = index + 2; i < length; i++) { + var c = content[i]; - if (end === -1) - break; + if (c === '(') { + count++; + continue; + } - var comment = html.substring(beg, end + 3); + if (c !== ')') + continue; + else if (count) { + count--; + continue; + } - if (comment.indexOf('[if') !== -1 || comment.indexOf('[endif') !== -1) { - beg = html.indexOf(tagBeg, end + 3); - continue; - } + var end = content.substring(i + 1, i + 2); + if (beg === end && beg === '"' || beg === '\'' || beg === '`') + esc = beg; + return { beg: index, end: i, command: content.substring(index + 2, i).trim(), escape: esc }; + } - html = html.replacer(comment, ''); - beg = html.indexOf(tagBeg, end + 3); - } + return null; +} - return html; +function removeComments(html) { + var tagBeg = ''; + var beg = html.indexOf(tagBeg); + var end = 0; + + while (beg !== -1) { + end = html.indexOf(tagEnd, beg + 4); + + if (end === -1) + break; + + var comment = html.substring(beg, end + 3); + if (comment.indexOf('[if') !== -1 || comment.indexOf('[endif') !== -1) { + beg = html.indexOf(tagBeg, end + 3); + } else { + html = html.replacer(comment, ''); + beg = html.indexOf(tagBeg, beg); + } + } + + return html; } -/** - * Inline JS compressor - * @private - * @param {String} html HTML. - * @param {Number} index Last index. - * @param {Framework} framework Framework object. - * @return {String} - */ -function compressJS(html, index, framework) { - - var strFrom = ''; - - var indexBeg = html.indexOf(strFrom, index || 0); - if (indexBeg === -1) { - strFrom = ''; + + var indexBeg = html.indexOf(strFrom, index || 0); + if (indexBeg === -1) { + strFrom = '');buffer.push(" @{js('default.js')}")}buffer.push(" @{favicon('favicon.ico')}");buffer.push("");buffer.push("");buffer.push("");buffer.push(" @{body}");buffer.push("");buffer.push("");buffer.push("");fs.writeFileSync(path.join(dir,"_layout.html"),buffer.join(EOF))}function createFileResource(directory){var dir=path.join(directory,"resources");fs.writeFileSync(path.join(dir,"default.resource"),"name : value"+EOF)}function createFilePublicAngular(directory){var dir=path.join(directory,"app");var css=path.join(dir,"css");var controllers=path.join(dir,"controllers");var buffer=[];buffer.push("User-agent: *");buffer.push("Allow: /");buffer.push("");fs.writeFileSync(path.join(dir,"robots.txt"),buffer.join(EOF));fs.writeFileSync(path.join(css,"app.css"),"LyphdXRvKi8NCg0KLyoNCiAgICB2YXIgY29sb3IgPSAnY29sb3I6cmVkJzsNCiAgICB2YXIgd2lkdGggPSAnOTQwcHgnOw0KKi8NCg0KYm9keSB7IHBhZGRpbmc6MjBweDsgbWFyZ2luOjA7IGZvbnQ6bm9ybWFsIDEycHggQXJpYWw7IGNvbG9yOiM1MDUwNTA7IH0NCg0KLmNvbnRlbnQgeyBtYXJnaW46MCBhdXRvOyB3aWR0aDogJHdpZHRoOyBwYWRkaW5nOiAxMHB4OyBib3JkZXItcmFkaXVzOiA1cHg7ICRjb2xvcjsgYm94LXNoYWRvdzogMCAwIDIwcHggcmdiYSgwLDAsMCwwLjUpOyBhbmltYXRpb246IG15QW5pbWF0aW9uIDVzIGFsdGVybmF0ZTsgfQ0KDQoubGlzdCB7IGJvcmRlci1ib3R0b206MXB4IHNvbGlkICNFMEUwRTA7IHBhZGRpbmctYm90dG9tOjVweDsgbWFyZ2luLWJvdHRvbTo1cHg7IH0NCi5saXN0ID4gZGl2OmZpcnN0LWNoaWxkIHsgZm9udC1zaXplOiAxNXB4OyB9DQoNCkBrZXlmcmFtZXMgbXlBbmltYXRpb24NCnsNCgkwJSAgIHsgYmFja2dyb3VuZDogd2hpdGU7IH0NCgkyNSUgIHsgYmFja2dyb3VuZDogI0YwRjBGMDsgfQ0KCTUwJSAgeyBiYWNrZ3JvdW5kOiAjRDBEMEQwOyB9DQoJMTAwJSB7IGJhY2tncm91bmQ6ICNFMEUwRTA7IH0NCn0=","base64");buffer=[];buffer.push("var app = angular.module('app', []);");buffer.push("");fs.writeFileSync(path.join(dir,"app.js"),buffer.join(EOF));buffer=[];buffer.push("function HomeCtrl($scope) {\n $scope.name = 'total.js + angular.js = awesome';\n}");fs.writeFileSync(path.join(controllers,"home.js"),buffer.join(EOF));buffer=[];buffer.push("function UserCtrl($scope) {\n // example\n}");fs.writeFileSync(path.join(controllers,"user.js"),buffer.join(EOF))}function createFilePublic(directory){var dir=path.join(directory,"public");var css=path.join(dir,"css");var js=path.join(dir,"js");var img=path.join(dir,"img");fs.writeFileSync(path.join(dir,"favicon.ico"),"AAABAAIAGBgAAAEAIAAoCQAAJgAAABAQAAABACAAKAQAAE4JAAAoAAAAGAAAADAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGsAAADxAAAA/wAAAP4AAAD+AAAA/gAAAP4AAAD+AAAA/gAAAP4AAAD+AAAA/gAAAP4AAAD+AAAA/gAAAP4AAAD+AAAA/gAAAP8AAADwAAAAZAAAAAAAAAAAAAAAAAAAAPEAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA7wAAAAAAAAAAAAAAAAAAAP8AAAD/AAEB/wMJBv8ECwj/BAsI/wQLCP8ECwj/BAsI/wQLCP8ECwj/BAsI/wQLCP8ECwj/BAsI/wQLCP8ECwj/AwkG/wAAAP8AAAD/AAAA/wAAAAAAAAAAAAAAAAAAAP8AAAD/AwoH/yyGXf83qHT/NqRx/zakcf82pHH/NqRx/zakcf82pHH/NqRx/zakcf82pHH/NqRx/zakcf83qHT/LIVc/wMJBv8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAwI/ziodP9F0pL/Q82O/0TPj/9Ez4//RM+P/0TPj/9Ez4//RM+P/0TPj/9Ez4//RM+P/0PNjv9F0pL/N6h0/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9DzY7/QseK/0DChv8/wYX/P8GF/z/Ahf8/wIX/P8CF/z/Bhf8/v4P+QMKG/0LHiv9DzY7/NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CG/xU+K/8LIhf/DCUa/wwlGv8MJRr/DCUa/wwlGv8LIhf/FT8s/z/Bhv9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CE/wofFf8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/CiAW/z/Ahf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CE/wsiGP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/DCMY/z/Bhf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CE/wsiGP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/DCMY/z/Bhf9Ez4//NqRx/wQLCP8AAAD+AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CF/wsiGP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/DCMY/z/Bhf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CE/wsiGP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/DCMY/z/Bhf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CE/wsiGP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/DCMY/z/Bhf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CF/wsiGP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/DCMY/z/Bhf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CE/wsiGP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/DCMY/z/Bhf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CE/wsiGP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/DCMY/z/Bhf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8CE/wofFf8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/CiAW/z/Bhf9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9Ez4//P8GF/xQ9Kv8LIRf/DCQZ/wwlGf8MJRn/DCUZ/wwkGf8LIRf/FT4r/z/Bhv9Ez4//NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAsI/zakcv9DzY7/QseK/0DChf8/wYT/P8GF/z/Bhf8/wYX/P8GF/z/Bhf8/wYT/QMKF/0LHiv9DzY7/NqRx/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/BAwI/ziodf9F0pL/Q82O/0TOj/9Ez4//RM+P/0TPj/9Ez4//RM+P/0TPj/9Ez4//RM6P/0PNjv9F0pL/N6h0/wQLCP8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/AwoH/yyHXv83qHT/NqRx/zakcf82pHH/NqVx/zalcf82pXH/NqVx/zalcf82pHH/NqRx/zakcf83qHX/LIZd/wMIBv8AAAD/AAAA/gAAAAAAAAAAAAAAAAAAAP8AAAD/AAEB/wMJBv8ECwj/BAsI/wQLCP8ECwj/BAsI/wQLCP8ECwj/BAsI/wQLCP8ECwj/BAsI/wQLCP8ECwj/AwkG/wAAAf8AAAD/AAAA/wAAAAAAAAAAAAAAAAAAAOMAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA6wAAAAAAAAAAAAAAAAAAAEcAAADgAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADuAAAAWAAAAAAAAAAAKAAAABAAAAAgAAAAAQAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoAAADPAAAA7QAAAO0AAADtAAAA7QAAAO0AAADtAAAA7QAAAO0AAADtAAAA7QAAAO0AAADlAAAAYgAAAAAAAABYAAAA/wEEA/8FDwv/BQ8L/wUPC/8FDwv/BQ8L/wUPC/8FDwv/BQ8L/wUQC/8DCQb/AAAA/wAAAK4AAAAAAAAAVQAAAP8QMiP/OKl1/zmtd/85rnj/Oa54/zmueP85rnj/Oa54/zisdv86sXv/IGFD/wAAAP8AAACqAAAAAAAAAFUAAAD/FT4r/0XTkv9E0JD/QMSH/0DEh/9AxIj/QMSI/0DChf5Dyov/Sd2Z/yh6Vf8AAAD/AAAAqQAAAAAAAABVAAAA/xQ7Kf9G1JP/M5pr/wshFv8LIRf/CyIY/wsiGP8JHRT/ImZI/0jcmP8ndlH/AAAA/wAAAKkAAAAAAAAAVQAAAP8UOyn/RtaU/y+QYv8AAAD/AAAA/wAAAP8AAAD/AAAA/xpROP9J3pn/J3ZR/wAAAP8AAACpAAAAAAAAAFUAAAD/FDsp/0bWlP8wkmT/AAAA/wAAAP8AAAD/AAAA/wAAAP8cVjv/Sd6Z/yd2Uf8AAAD/AAAAqQAAAAAAAABVAAAA/xQ7Kf9G1pT/MJJk/wAAAP8AAAD/AAAA/wAAAP8AAAD/HFY7/0nemf8ndlH/AAAA/wAAAKkAAAAAAAAAVQAAAP8UOyn/RtaU/zCSZP8AAAD/AAAA/wAAAP8AAAD/AAAA/xxWO/9J3pn/J3ZR/wAAAP8AAACpAAAAAAAAAFUAAAD/FDsp/0bWlP8wkmT/AAAA/wAAAP8AAAD/AAAA/wAAAP8cVjv/Sd6Z/yd2Uf8AAAD/AAAAqQAAAAAAAABVAAAA/xQ7Kf9G1pT/L5Bi/wAAAP8AAAD/AAAA/wAAAP8AAAD/GlI4/0nemf8ndlH/AAAA/wAAAKkAAAAAAAAAVQAAAP8UOyn/RtST/zOaav8LIBb/CyEW/wsjGP8LIhj/CRwT/yJnR/9I3Jj/J3ZR/wAAAP8AAACpAAAAAAAAAFUAAAD/FT4r/0XTk/9E0JD/QMSH/0DEh/9AxIf/QMSH/0DDhv9Dyov/Sd2Z/yh6VP8AAAD/AAAAqQAAAAAAAABWAAAA/xAyI/84qXb/Oa13/zmueP85rnj/Oa54/zmueP85rnj/OKx2/zqxe/8gYUT/AAAA/wAAAKoAAAAAAAAAVgAAAP8BBAP/BQ8L/wUPC/8FDwv/BQ8L/wUPC/8FDwv/BQ8L/wUPC/8FEAv/AwkG/wAAAP8AAACuAAAAAAAAACAAAADAAAAA7QAAAO0AAADtAAAA7QAAAO0AAADtAAAA7QAAAO0AAADtAAAA7QAAAO0AAADkAAAAXQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","base64");if($type===3)return;if(!fs.existsSync(js))fs.mkdirSync(js);if(!fs.existsSync(css))fs.mkdirSync(css);if(!fs.existsSync(img))fs.mkdirSync(img);var buffer=[];buffer.push("User-agent: *");buffer.push("Allow: /");buffer.push("");fs.writeFileSync(path.join(dir,"robots.txt"),buffer.join(EOF));fs.writeFileSync(path.join(css,"default.css"),"LyphdXRvKi8NCg0KLyoNCiAgICB2YXIgY29sb3IgPSAnY29sb3I6cmVkJzsNCiAgICB2YXIgd2lkdGggPSAnOTQwcHgnOw0KKi8NCg0KYm9keSB7IHBhZGRpbmc6MjBweDsgbWFyZ2luOjA7IGZvbnQ6bm9ybWFsIDEycHggQXJpYWw7IGNvbG9yOiM1MDUwNTA7IH0NCg0KLmNvbnRlbnQgeyBtYXJnaW46MCBhdXRvOyB3aWR0aDogJHdpZHRoOyBwYWRkaW5nOiAxMHB4OyBib3JkZXItcmFkaXVzOiA1cHg7ICRjb2xvcjsgYm94LXNoYWRvdzogMCAwIDIwcHggcmdiYSgwLDAsMCwwLjUpOyBhbmltYXRpb246IG15QW5pbWF0aW9uIDVzIGFsdGVybmF0ZTsgfQ0KDQoubGlzdCB7IGJvcmRlci1ib3R0b206MXB4IHNvbGlkICNFMEUwRTA7IHBhZGRpbmctYm90dG9tOjVweDsgbWFyZ2luLWJvdHRvbTo1cHg7IH0NCi5saXN0ID4gZGl2OmZpcnN0LWNoaWxkIHsgZm9udC1zaXplOiAxNXB4OyB9DQoNCkBrZXlmcmFtZXMgbXlBbmltYXRpb24NCnsNCgkwJSAgIHsgYmFja2dyb3VuZDogd2hpdGU7IH0NCgkyNSUgIHsgYmFja2dyb3VuZDogI0YwRjBGMDsgfQ0KCTUwJSAgeyBiYWNrZ3JvdW5kOiAjRDBEMEQwOyB9DQoJMTAwJSB7IGJhY2tncm91bmQ6ICNFMEUwRTA7IH0NCn0=","base64");buffer=[];buffer.push("$(document).ready(function() {");buffer.push("");buffer.push("});");fs.writeFileSync(path.join(js,"default.js"),buffer.join(EOF))}function createFileModules(directory){var dir=path.join(directory,"modules");var buffer=[];buffer.push("exports.yourcode = function () {");buffer.push(" return 'Hello World';");buffer.push("};");fs.writeFileSync(path.join(dir,"example.js"),buffer.join(EOF))}function createFileControllerAngular(directory){var dir=path.join(directory,"controllers");var buffer=[];buffer.push("exports.install = function(framework) {");buffer.push(" framework.route('/*', view_app);");buffer.push("};");buffer.push("");buffer.push("function view_app() {");buffer.push(" var self = this;");buffer.push(" self.view('app');");buffer.push("}");fs.writeFileSync(path.join(dir,"default.js"),buffer.join(EOF))}function createFileController(directory){var dir=path.join(directory,"controllers");var buffer=[];buffer.push("exports.install = function(framework) {");buffer.push(" framework.route('/', view_homepage);");buffer.push(" framework.route('#400', error400);");buffer.push(" framework.route('#401', error401);");buffer.push(" framework.route('#403', error403);");buffer.push(" framework.route('#404', error404);");buffer.push(" framework.route('#408', error408);");buffer.push(" framework.route('#431', error431);");buffer.push(" framework.route('#500', error500);");buffer.push("};");buffer.push("");buffer.push("// Bad Request");buffer.push("function error400() {");buffer.push(" var self = this;");buffer.push(" self.status = 400;");buffer.push(" self.plain(utils.httpStatus(self.status));");buffer.push("}");buffer.push("");buffer.push("// Unauthorized");buffer.push("function error401() {");buffer.push(" var self = this;");buffer.push(" self.status = 401;");buffer.push(" self.plain(utils.httpStatus(self.status));");buffer.push("}");buffer.push("");buffer.push("// Forbidden");buffer.push("function error403() {");buffer.push(" var self = this;");buffer.push(" self.status = 403;");buffer.push(" self.plain(utils.httpStatus(self.status));");buffer.push("}");buffer.push("");buffer.push("// Not Found");buffer.push("function error404() {");buffer.push(" var self = this;");buffer.push(" self.status = 404;");buffer.push(" self.plain(utils.httpStatus(self.status));");buffer.push("}");buffer.push("");buffer.push("// Request Timeout");buffer.push("function error408() {");buffer.push(" var self = this;");buffer.push(" self.status = 408;");buffer.push(" self.plain(utils.httpStatus(self.status));");buffer.push("}");buffer.push("");buffer.push("// Request Header Fields Too Large");buffer.push("function error431() {");buffer.push(" var self = this;");buffer.push(" self.status = 431;");buffer.push(" self.plain(utils.httpStatus(self.status));");buffer.push("}");buffer.push("");buffer.push("// Internal Server Error");buffer.push("function error500() {");buffer.push(" var self = this;");buffer.push(" self.status = 500;");buffer.push(" self.plain(utils.httpStatus(self.status));");buffer.push("}");buffer.push("");buffer.push("function view_homepage() {");buffer.push(" var self = this;");if($type!==2)buffer.push(" self.view('homepage');");else buffer.push(" self.plain('homepage');");buffer.push("}");fs.writeFileSync(path.join(dir,"default.js"),buffer.join(EOF))}function createFileTest(directory){var dir=path.join(directory,"tests");var buffer=[];buffer.push("var assert = require('assert');");buffer.push("");buffer.push("exports.run = function(framework) {");buffer.push("");buffer.push(" framework.assert('Number validation', function(next, name) {");buffer.push(" assert.ok('1' === '1', name);");buffer.push(" next();");buffer.push(" });");buffer.push("");buffer.push(" framework.assert('Homepage', '/1/', ['xhr', 'json'], function(error, data, code, headers, cookies, name) {");buffer.push(" assert.ok(code === 200, name);");buffer.push(" }, { name: 'total.js (optional)' }, { cookie: 'value (optional)' }, { 'X-My-Header': 'optional' });");buffer.push("");buffer.push("};");fs.writeFileSync(path.join(dir,"default.js"),buffer.join(EOF))}function createFileComponent(directory){var dir=path.join(directory,"components");var buffer=[];buffer.push("// optional");buffer.push("exports.install = function(framework) {");buffer.push(" // component doesn't support routing");buffer.push(" console.log('OK');");buffer.push("};");buffer.push("");buffer.push("// optional");buffer.push("exports.usage = function(isDetailed) {");buffer.push(" return '';");buffer.push("};");buffer.push("");buffer.push("// optional");buffer.push("exports.configure = function(setup) {");buffer.push("");buffer.push("};");buffer.push("");buffer.push("// REQUIRED");buffer.push("exports.render = function(data) {");buffer.push(" // this === controller or this === framework");buffer.push(" return '';");buffer.push("}; ");fs.writeFileSync(path.join(dir,"example.js"),buffer.join(EOF))}function install(directory){exec("npm install total.js",{cwd:directory},function(error,stdout,stderr){}).on("exit",function(){console.log("total.js: success")})}function display_help(){console.log("");console.log("-m or -minimal = minimal");console.log("-n or -normal = normal (default)");console.log("-f or -full = full");console.log("-a or -angular = for angular.js application");console.log("-v or -version = total.js version");console.log("/path/ = target (default current directory)");console.log("")}function main(){var dir=process.cwd();for(var i=2;i0){var can=true;for(var i=0;i -// -// 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. - -"use strict";var VERSION="v1.5.1";var fs=require("fs");var ph=require("path");var parser=require("url");var zlib=require("zlib");var http=require("http");var https=require("https");var padding=120;var exec=require("child_process").exec;var os=require("os");var EOF=os.platform()==="win32"?"\r\n":"\n";var $type=0;var settings={};var colors={reset:"",underscore:"",dim:"",reverse:"",white:"",red:"",bgRed:"",bgGreen:""};var current_repository="default";var current_package="";String.prototype.padRight=function(max,c){var self=this;return self+new Array(Math.max(0,max-self.length+1)).join(c||" ")};function Backup(){this.file=[];this.directory=[];this.path="";this.fileName="";this.read={key:"",value:"",status:0};this.complete=function(){};this.filter=function(path){return true}}function Walker(){this.pending=[];this.pendingDirectory=[];this.directory=[];this.file=[];this.options={sort:true,addEmptyDirectory:false};this.onComplete=null;this.onFilter=null}Walker.prototype.reset=function(){var self=this;self.file=[];self.directory=[];self.pendingDirectory=[]};Walker.prototype.walk=function(path){var self=this;if(path instanceof Array){var length=path.length;for(var i=0;i0){var item=self.pending.shift();self.stat(item);return}if(self.pendingDirectory.length>0){var directory=self.pendingDirectory.shift();self.walk(directory);return}if(self.options.sort){self.file.sort(function(a,b){return a.localeCompare(b)})}self.onComplete(self.directory,self.file)};Backup.prototype.backup=function(path,fileName,callback,filter){if(fs.existsSync(fileName))fs.unlinkSync(fileName);var walker=new Walker;var self=this;self.fileName=fileName;self.path=path;if(callback)self.complete=callback;if(filter)self.filter=filter;walker.onComplete=function(directory,files){self.directory=directory;self.file=files;self.$compress()};walker.walk(path)};Backup.prototype.$compress=function(){var self=this;var length=self.path.length;var len=0;if(self.directory.length>0){len=self.directory.length;for(var i=0;i0)self.createDirectory(path)}var buffer=new Buffer(value,"base64");zlib.gunzip(buffer,function(err,data){fs.writeFileSync(ph.join(self.path,key),data);buffer=null})};Backup.prototype.createDirectory=function(path,root){if(path[0]==="/")path=path.substring(1);if(path[path.length-1]==="/")path=path.substring(0,path.length-1);var arr=path.split("/");var directory="";var self=this;var length=arr.length;for(var i=0;i0?"/":"")+name;var dir=ph.join(self.path,directory);if(root)dir="/"+dir;if(fs.existsSync(dir))continue;fs.mkdirSync(dir)}};Backup.prototype.clear=function(path,callback,filter){var self=this;var walker=new Walker;walker.options.addEmptyDirectory=true;if(callback)self.complete=callback;if(filter)self.filter=filter;walker.onComplete=function(directory,files){self.file=[];self.directory=[];if(typeof filter!=="function")filter=function(o){return true};var length=files.length;for(var i=0;ib.length)return-1;if(a.length0)current_repository=arg;else current_package=arg;continue}switch($type){case 1:if(current_package.length!==0){install(settings[current_repository],current_package,null,true);return}var filename=ph.join(process.cwd(),"package.json");var packagejson={};if(!fs.existsSync(filename))return;packagejson=JSON.parse(fs.readFileSync(filename).toString("utf8"));if(!packagejson.tpm)return;var index=0;var fn=function(){var key=Object.keys(packagejson.tpm)[index++];if(typeof key==="undefined")return;var url=packagejson.tpm[key];install(url,key,function(){fn()})};fn();break;case 2:create();break;case 3:repository_add();break;case 4:if(current_package.length!==0){uninstall(settings[current_repository],current_package,null,true);return}var filename=ph.join(process.cwd(),"package.json");var packagejson={};if(!fs.existsSync(filename))return;packagejson=JSON.parse(fs.readFileSync(filename).toString("utf8"));if(!packagejson.tpm)return;var index=0;var fn=function(){var key=Object.keys(packagejson.tpm)[index++];if(typeof key==="undefined")return;var url=packagejson.tpm[key];uninstall(url,key,function(){fn()},true)};fn();break;default:display_help();break}}function log(){console.log.apply(console,arguments)}main(); \ No newline at end of file diff --git a/minify/total.js/builders.js b/minify/total.js/builders.js deleted file mode 100644 index d99ca8cc7..000000000 --- a/minify/total.js/builders.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";var utils=require("./utils");var schema={};var schemaValidation={};var schemaValidator={};var schemaDefaults={};var UNDEFINED="undefined";var FUNCTION="function";var OBJECT="object";var STRING="string";var NUMBER="number";var BOOLEAN="boolean";var REQUIRED='The field "@" is required.';function ErrorBuilder(onResource){this.errors=[];this.onResource=onResource;this.resourceName="";this.resourcePrefix="";this.count=0;this.replacer=[];this.isPrepared=false;if(typeof onResource===UNDEFINED)this._resource()}function UrlBuilder(){this.builder={}}function Pagination(items,page,max,format){this.isNext=false;this.isPrev=false;this.items=items;this.count=0;this.skip=0;this.take=0;this.page=0;this.max=0;this.visible=false;this.format=format||"?page={0}";this.refresh(items,page,max)}exports.schema=function(name,obj,defaults,validator){if(typeof obj===UNDEFINED)return schema[name]||null;if(typeof defaults===FUNCTION)schemaDefaults[name]=defaults;if(typeof validator===FUNCTION)schemaValidator[name]=validator;schema[name]=obj;return obj};exports.isJoin=function(value){if(!value)return false;if(value[0]==="[")return true;return typeof schema[value]!==UNDEFINED};exports.validation=function(name,fn){if(typeof fn===FUNCTION){schemaValidator[name]=fn;return true}if(typeof fn===UNDEFINED)return schemaValidation[name]||[];schemaValidation[name]=fn;return fn};exports.validate=function(name,model){var fn=schemaValidator[name];var builder=new ErrorBuilder;if(typeof fn===UNDEFINED)return builder;return utils.validate.call(this,model,Object.keys(schema[name]),fn,builder)};exports.create=function(name){return exports.defaults(name)};exports.defaults=function(name){var obj=exports.schema(name);if(obj===null)return null;var defaults=schemaDefaults[name];var item=utils.extend({},obj,true);var properties=Object.keys(item);var length=properties.length;for(var i=0;i0};ErrorBuilder.prototype.read=function(name){var self=this;if(!self.isPrepared)self.prepare();var error=self.errors.find(function(o){return o.name===name});if(error!==null)return error.error;return null};ErrorBuilder.prototype.clear=function(){var self=this;self.errors=[];self.count=0;return self};ErrorBuilder.prototype.replace=function(search,newvalue){var self=this;self.isPrepared=false;self.replacer[search]=newvalue;return self};ErrorBuilder.prototype.json=function(beautify){if(beautify)return JSON.stringify(this.prepare().errors,null," ");return JSON.stringify(this.prepare().errors)};ErrorBuilder.prototype.JSON=function(beautify){if(beautify)return JSON.stringify(this.prepare().errors,null," ");return JSON.stringify(this.prepare().errors)};ErrorBuilder.prototype._prepare=function(){var self=this;if(self.onResource===null)return self;var errors=self.errors;var length=errors.length;for(var i=0;i0?1:0);self.page=page-1;if(self.page<0)self.page=0;self.items=items;self.skip=self.page*max;self.take=max;self.max=max;self.isPrev=self.page>0;self.isNext=self.page1;self.page++;return self};Pagination.prototype.prev=function(format){var self=this;var page=0;format=format||self.format;if(self.isPrev)page=self.page-1;else page=self.count;return{url:format.format(page,self.items,self.count),page:page,selected:false}};Pagination.prototype.next=function(format){var self=this;var page=0;format=format||self.format;if(self.isNext)page=self.page+1;else page=1;return{url:format.format(page,self.items,self.count),page:page,selected:false}};Pagination.prototype.render=function(max,format){var self=this;var builder=[];format=format||self.format;if(typeof max===STRING){var tmp=format;format=max;max=format}if(typeof max===UNDEFINED||max===null){for(var i=1;i=pages){pageTo=pages;pageFrom=pages-max}if(pageFrom<0)pageFrom=1;for(var i=pageFrom;ib.priority)return 1;else return-1});var length=self.builder.length;for(var i=0;i0?" ":"")+self.builder[i].cmd;return(self.isIM?"convert":"gm -convert")+' "'+filenameFrom+'"'+" "+cmd+' "'+filenameTo+'"'};Image.prototype.arg=function(first,last){var self=this;var arr=[];if(!self.isIM)arr.push("-convert");if(first)arr.push(first);self.builder.sort(function(a,b){if(a.priority>b.priority)return 1;else return-1});var length=self.builder.length;for(var i=0;ib.priority)return-1;if(a.priorityb.priority)return-1;if(a.priority0){subdomain=url.substring(1,index).trim().toLowerCase().split(",");url=url.substring(index+1);priority+=2}var isRaw=false;if(flags){var tmp=[];for(var i=0;i0){subdomain=url.substring(1,index).trim().toLowerCase().split(",");url=url.substring(index+1);priority+=2}if(isASTERIX){url=url.replace("*","").replace("//","/");priority=-10-priority}var arr=[];var routeURL=internal.routeSplit(url.trim());if(url.indexOf("{")!==-1){routeURL.forEach(function(o,i){if(o.substring(0,1)==="{")arr.push(i)});priority-=arr.length}if(typeof allow===STRING)allow=allow[allow];if(typeof protocols===STRING)protocols=protocols[protocols];if(typeof flags===STRING)flags=flags[flags];var isJSON=false;var isBINARY=false;var tmp=[];if(typeof flags===UNDEFINED)flags=[];for(var i=0;i50)self.errors.shift()}self.onError(err,name,uri);return self};Framework.prototype.problem=function(message,name,uri,ip){var self=this;if(self.problems!==null){self.problems.push({message:message,name:name,uri:uri,ip:ip});if(self.problems.length>50)self.problems.shift()}self.emit("problem",message,name,uri,ip);return self};Framework.prototype.change=function(message,name,uri,ip){var self=this;if(self.changes!==null){self.changes.push({message:message,name:name,uri:uri,ip:ip});if(self.changes.length>50)self.changes.shift()}self.emit("change",message,name,uri,ip);return self};Framework.prototype.module=function(name){var self=this;var module=self.modules[name];if(typeof module!==UNDEFINED)return module;if(self.isLoaded)return null;var configDirectory=self.config["directory-modules"];var filename=path.join(directory,configDirectory,name);var isDirectory=false;if(self.isCoffee){if(fs.existsSync(filename))filename+=EXTENSION_COFFEE;else filename+=EXTENSION_JS}else filename+=EXTENSION_JS;if(!fs.existsSync(filename)){filename=path.join(directory,configDirectory,name,name);if(self.isCoffee){if(fs.existsSync(filename+EXTENSION_COFFEE))filename+=EXTENSION_COFFEE;else filename+=EXTENSION_JS}else filename+=EXTENSION_JS;if(!fs.existsSync(filename)){filename=path.join(directory,configDirectory,name,"index");if(self.isCoffee){if(fs.existsSync(filename+EXTENSION_COFFEE))filename+=EXTENSION_COFFEE;else filename+=EXTENSION_JS}else filename+=EXTENSION_JS}else module=require(filename);if(fs.existsSync(filename))module=require(filename);isDirectory=true}else module=require(filename);if(typeof module===UNDEFINED)return null;_controller="#module-"+name;if(module!==null&&typeof module.directory===UNDEFINED)module.directory=isDirectory?path.join(directory,configDirectory):path.join(directory,configDirectory,name);self.modules[name]=module;return module};Framework.prototype.component=function(name){var self=this;var component=self.components[name];if(typeof component!==UNDEFINED)return component;if(self.isLoaded)return null;var configDirectory=self.config["directory-components"];var filename=path.join(directory,configDirectory,name);var isDirectory=false;if(self.isCoffee){if(fs.existsSync(filename+EXTENSION_COFFEE))filename+=EXTENSION_COFFEE;else filename+=EXTENSION_JS}else filename+=EXTENSION_JS;if(!fs.existsSync(filename)){filename=path.join(directory,configDirectory,name,name);if(self.isCoffee){if(fs.existsSync(filename+EXTENSION_COFFEE))filename+=EXTENSION_COFFEE;else filename+=EXTENSION_JS}else filename+=EXTENSION_JS;if(!fs.existsSync(filename)){filename=path.join(directory,configDirectory,name,"index");if(self.isCoffee){if(fs.existsSync(filename+EXTENSION_COFFEE))filename+=EXTENSION_COFFEE;else filename+=EXTENSION_JS}else filename+=EXTENSION_JS;if(fs.existsSync(filename))component=require(filename)}else component=require(filename);isDirectory=true}else component=require(filename);if(typeof component===UNDEFINED)return null;if(component!==null&&typeof component.directory===UNDEFINED)component.directory=isDirectory?path.join(directory,configDirectory):path.join(directory,configDirectory,name);_controller="";self.components[name]=component;if(component.install)component.install.call(self,self,name,component.directory);if(typeof component.render===UNDEFINED)throw new Error('Component must contain "export.render" function.');return component};Framework.prototype.install=function(){var self=this;var dir=path.join(directory,self.config["directory-controllers"]);var framework=self;function install_controller(directory,level){fs.readdirSync(directory).forEach(function(o){var isDirectory=fs.statSync(path.join(directory,o)).isDirectory();if(isDirectory){level++;install_controller(path.join(directory,o),level);return}var ext=path.extname(o).toLowerCase();if(ext!==EXTENSION_JS&&ext!==EXTENSION_COFFEE)return;self.controller((level>0?directory.replace(dir,"")+"/":"")+o.substring(0,o.length-ext.length))})}if(fs.existsSync(dir))install_controller(dir,0);dir=path.join(directory,self.config["directory-modules"]);if(fs.existsSync(dir)){fs.readdirSync(dir).forEach(function(o){var ext=path.extname(o);var isDirectory=fs.statSync(path.join(dir+o)).isDirectory();var extLower=ext.toLowerCase();if(!isDirectory&&extLower!==EXTENSION_JS&&extLower!==EXTENSION_COFFEE)return;var name=o.replace(ext,"");if(name==="#")return;var module=self.module(name);if(module===null||typeof module.install===UNDEFINED)return;try{module.install(self,self,name)}catch(err){self.error(err,name)}})}self._routeSort();dir=path.join(directory,self.config["directory-components"]);if(fs.existsSync(dir)){fs.readdirSync(dir).forEach(function(o){var ext=path.extname(o);var isDirectory=fs.statSync(path.join(dir+o)).isDirectory();var extLower=ext.toLowerCase();if(!isDirectory&&extLower!==EXTENSION_JS&&extLower!==EXTENSION_COFFEE)return;var name=o.replace(ext,"");if(name==="#")return;self.component(name)})}dir=path.join(directory,self.config["directory-definitions"]);if(fs.existsSync(dir)){fs.readdirSync(dir).forEach(function(o){var ext=path.extname(o).toLowerCase();if(ext!==EXTENSION_JS&&ext!==EXTENSION_COFFEE)return;var data=fs.readFileSync(path.join(dir,o),"utf8").toString();if(self.isCoffee)require("coffee-script").eval(data);else eval(data)})}return self};Framework.prototype.injectConfig=function(url,debug,rewrite){var self=this;if(typeof debug!==UNDEFINED&&self.config.debug!==debug)return self;if(typeof rewrite===UNDEFINED)rewrite=true;utils.request(url,"GET","",function(error,data){if(error){self.error(error,"injectConfig - "+url,null);return}self.configure(data.split("\n"),rewrite)});return self};Framework.prototype.injectVersions=function(url,rewrite){var self=this;if(typeof rewrite===UNDEFINED)rewrite=false;utils.request(url,"GET","",function(error,data){if(error){self.error(error,"injectVersions - "+url,null);return}self.configureMapping(data,rewrite)});return self};Framework.prototype.injectModule=function(name,url){var self=this;var framework=self;utils.request(url,"GET","",function(error,data){if(error){self.error(error,"injectModule - "+name,null);return}try{var result=eval("(new (function(){var module = this;var exports = {};this.exports=exports;"+data+"})).exports");_controller="#module-"+name;self.routes.web=self.routes.web.remove(function(route){return route.name===_controller});self.routes.files=self.routes.files.remove(function(route){return route.name===_controller});self.routes.websockets=self.routes.websockets.remove(function(route){return route.name===_controller});if(typeof result.install!==UNDEFINED){result.install(self,name);self._routeSort()}self.modules[name]=result;_controller=""}catch(ex){self.error(ex,"injectModule - "+name,null)}});return self};Framework.prototype.injectModel=function(name,url){var self=this;var framework=self;utils.request(url,"GET","",function(error,data){if(error){self.error(error,"injectModel - "+name,null);return}try{var result=eval("(new (function(){var module = this;var exports = {};this.exports=exports;"+data+"})).exports");self.models[name]=result}catch(ex){self.error(ex,"injectModel - "+name,null)}});return self};Framework.prototype.injectSource=function(name,url){var self=this;var framework=self;utils.request(url,"GET","",function(error,data){if(error){self.error(error,"injectSource - "+name,null);return}try{var result=eval("(new (function(){var module = this;var exports = {};this.exports=exports;"+data+"})).exports");self.sources[name]=result}catch(ex){self.error(ex,"injectSource - "+name,null)}});return self};Framework.prototype.injectController=function(name,url){var self=this;utils.request(url,"GET","",function(error,data){if(error){self.error(error,"injectController - "+name,null);return}try{var result=eval("(new (function(framework){var module = this;var exports = {};this.exports=exports;"+data+"})).exports");_controller=name;self.routes.web=self.routes.web.remove(function(route){return route.name===_controller});self.routes.files=self.routes.files.remove(function(route){return route.name===_controller});self.routes.websockets=self.routes.websockets.remove(function(route){return route.name===_controller});if(typeof result.install!==UNDEFINED){result.install(self,name);self._routeSort()}self.controllers[name]=result;_controller=""}catch(ex){self.error(ex,"injectController - "+name,null)}});return self};Framework.prototype.injectDefinition=function(url){var self=this;var framework=self;utils.request(url,"GET","",function(error,data){if(error){self.error(error,"injectDefinition - "+url,null);return}try{eval(data)}catch(ex){self.error(ex,"injectDefinition - "+url,null)}});return self};Framework.prototype.injectComponent=function(name,url){var self=this;var framework=self;utils.request(url,"GET","",function(error,data){if(error){self.error(error,"injectComponent - "+name,null);return}try{var result=eval("(new (function(){var module = this;var exports = {};this.exports=exports;"+data+"})).exports");if(typeof result.install!==UNDEFINED)result.install(self,name);self.components[name]=result}catch(ex){self.error(ex,"injectComponent - "+name,null)}});return self};Framework.prototype.eval=function(script){var self=this;var framework=self;if(typeof script===FUNCTION){try{eval("("+script.toString()+")()")}catch(ex){self.error(ex,"eval - "+script.toString(),null)}return self}if((script.startsWith("http://",true)||script.startsWith("https://",true))&&scripts.trim().indexOf("\n")===-1){utils.request(script,"GET","",function(err,data){if(!err){self.eval(data.toString());return}self.error(err)})}try{eval(script)}catch(ex){self.error(ex,"eval - "+script,null)}return self};Framework.prototype.onError=function(err,name,uri){console.log(err.toString(),err.stack);console.log("--------------------------------------------------------------------");return this};Framework.prototype.onRequest=null;Framework.prototype.onAuthorization=null;Framework.prototype.onPrefix=null;Framework.prototype.onVersion=null;Framework.prototype.onRoute=null;Framework.prototype.onValidation=null;Framework.prototype.onMail=function(address,subject,body,callback){var message=Mail.create(subject,body);if(address instanceof Array){var length=address.length;for(var i=0;i0&&tmp.isEmail())message.reply(self.config["mail.address.reply"]);tmp=self.config["mail.address.copy"];if(tmp&&tmp.length>0&&tmp.isEmail())message.bcc(tmp);var options={};var opt=self.config["mail.smtp.options"];if(opt&&opt.isJSON())options=JSON.parse(opt);message.send(self.config["mail.smtp"],options,callback);return self};Framework.prototype.onXSS=function(data){if(data===null||data.length===0)return false;data=decodeURIComponent(data);return data.indexOf("<")!==-1&&data.lastIndexOf(">")!==-1};Framework.prototype.onMeta=function(){var self=this;var builder="";var length=arguments.length;for(var i=0;i";break;case 1:builder+='';break;case 2:builder+='';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));builder+='';break}}return builder};Framework.prototype.log=function(){var self=this;var 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;for(var i=0;i0?" ":"")+(arguments[i]||"");self._verify_directory("logs");fs.appendFile(utils.combine(self.config["directory-logs"],filename+".log"),time+" | "+str+"\n");return self};Framework.prototype.usage=function(detailed){var self=this;var memory=process.memoryUsage();var cache=Object.keys(self.cache.repository);var resources=Object.keys(self.resources);var controllers=Object.keys(self.controllers);var connections=Object.keys(self.connections);var workers=Object.keys(self.workers);var modules=Object.keys(self.modules);var models=Object.keys(self.models);var components=Object.keys(self.components);var helpers=Object.keys(self.helpers);var staticFiles=Object.keys(self.temporary.path);var staticRange=Object.keys(self.temporary.range);var redirects=Object.keys(self.routes.redirects);var size=0;var sizeDatabase=0;var dir=utils.combine(self.config["directory-temp"]);var output={};output.framework={pid:process.pid,node:process.version,version:"v"+self.version_header,platform:process.platform,processor:process.arch,uptime:Math.floor(process.uptime()/60),memoryTotal:(memory.heapTotal/1024/1024).floor(2),memoryUsage:(memory.heapUsed/1024/1024).floor(2),mode:self.config.debug?"debug":"release",port:self.port,ip:self.ip,directory:process.cwd()};output.counter={resource:resources.length,controller:controllers.length,module:modules.length,component:components.length,cache:cache.length,worker:workers.length,connection:connections.length,helper:helpers.length,error:self.errors.length,problem:self.problem.length};output.routing={webpage:self.routes.web.length,websocket:self.routes.websockets.length,file:self.routes.files.length,partial:Object.keys(self.routes.partial).length,global:self.routes.partialGlobal.length,redirect:redirects.length};output.stats={request:self.stats.request,response:self.stats.response};output.redirects=redirects;if(self.restrictions.isRestrictions){output.restrictions={allowed:[],blocked:[],allowedHeaders:self.restrictions.allowedCustomKeys,blockedHeaders:self.restrictions.blockedCustomKeys}}if(!detailed)return output;output.controllers=[];controllers.forEach(function(o){var item=self.controllers[o];output.controllers.push({name:o,usage:typeof item.usage===UNDEFINED?null:item.usage()})});output.connections=[];connections.forEach(function(o){output.connections.push({name:o,online:self.connections[o].online})});output.modules=[];modules.forEach(function(o){var item=self.modules[o];output.modules.push({name:o,usage:typeof item.usage===UNDEFINED?null:item.usage()})});output.components=[];components.forEach(function(o){var item=self.components[o];output.components.push({name:o,usage:typeof item.usage===UNDEFINED?null:item.usage()})});output.models=[];models.forEach(function(o){var item=self.models[o];output.models.push({name:o,usage:typeof item.usage===UNDEFINED?null:item.usage()})});output.helpers=helpers;output.cache=cache;output.resources=resources;output.errors=self.errors;output.problems=self.problems;output.changes=self.changes;return output};Framework.prototype.onCompileCSS=null;Framework.prototype.onCompileJS=null;Framework.prototype.compileStatic=function(req,filename){if(!fs.existsSync(filename))return null;var self=this;var index=filename.lastIndexOf(".");var ext=filename.substring(index).toLowerCase();var output=fs.readFileSync(filename).toString(ENCODING);switch(ext){case EXTENSION_JS:output=self.config["allow-compile-js"]?self.onCompileJS===null?internal.compile_javascript(output,self):self.onCompileJS(filename,output):output;break;case".css":output=self.config["allow-compile-css"]?self.onCompileCSS===null?internal.compile_css(output):self.onCompileCSS(filename,output):output;var matches=output.match(/url\(.*?\)/g);if(matches!==null){matches.forEach(function(o){var url=o.substring(4,o.length-1);output=output.replace(o,"url("+self._version(url)+")")})}break}self._verify_directory("temp");var fileCompiled=utils.combine(self.config["directory-temp"],req.uri.pathname.replace(/\//g,"-").substring(1));fs.writeFileSync(fileCompiled,output);return fileCompiled};Framework.prototype.responseStatic=function(req,res){var self=this;if(res.success)return self;var name=req.url;var index=name.indexOf("?");if(index!==-1)name=name.substring(0,index);index=name.lastIndexOf("/");var resizer=self.routes.resize[name.substring(0,index+1)]||null;var isResize=false;if(resizer!==null){name=name.substring(index+1);index=name.lastIndexOf(".");isResize=resizer.extension==="*"||resizer.extension.indexOf(name.substring(index).toLowerCase())!==-1;if(isResize)name=resizer.path+name}var filename=utils.combine(self.config["directory-public"],decodeURIComponent(name));if(!isResize){self.responseFile(req,res,filename,"");return self}self.responseImage(req,res,filename,function(image){if(resizer.width||resizer.height){if(resizer.width&&resizer.height)image.resizeCenter(resizer.width,resizer.height);else image.resize(resizer.width,resizer.height)}if(resizer.grayscale)image.grayscale();if(resizer.blur)image.blur(typeof resizer.blur==="number"?resizer.blur:1);if(resizer.rotate&&typeof resizer.rotate==NUMBER)image.rotate(resizer.rotate);if(resizer.flop)image.flop();if(resizer.flip)image.flip();if(resizer.sepia)image.sepia(typeof resizer.sepia==="number"?resizer.sepia:100);image.quality(self.config["default-image-quality"]);image.minify()});return self};Framework.prototype.isProcessed=function(filename){var self=this;if(filename.url){var name=filename.url;var index=name.indexOf("?");if(index!==-1)name=name.substring(0,index);filename=utils.combine(self.config["directory-public"],decodeURIComponent(name))}if(typeof self.temporary.path[filename]!==UNDEFINED)return true;return false};Framework.prototype.isProcessing=function(filename){var self=this; -if(filename.url){var name=filename.url;var index=name.indexOf("?");if(index!==-1)name=name.substring(0,index);filename=utils.combine(self.config["directory-public"],decodeURIComponent(name))}var name=this.temporary.processing[filename];if(typeof self.temporary.processing[filename]!==UNDEFINED)return true;return false};Framework.prototype.noCache=function(req,res){req.noCache();if(res)res.noCache();return this};Framework.prototype.responseFile=function(req,res,filename,downloadName,headers,key){var self=this;if(res.success)return self;req.clear(true);key=key||filename;var name=self.temporary.path[key];if(framework.config.debug)name=undefined;if(name===null){self.response404(req,res);return self}var extension=path.extname(key).substring(1);if(extension.length===0)extension=path.extname(name).substring(1);if(self.config["static-accepts"].indexOf("."+extension)===-1){self.response404(req,res);return self}var etag=utils.etag(req.url,self.config["etag-version"]);if(!self.config.debug&&req.headers["if-none-match"]===etag){res.success=true;res.writeHead(304);res.end();self.stats.response.notModified++;self._request_stats(false,req.isStaticFile);if(!req.isStaticFile)self.emit("request-end",req,res);return self}if(typeof name===UNDEFINED){if(!fs.existsSync(filename)){var tmpname=self.isWindows?filename.replace(self.config["directory-public"].replace(/\//g,"\\"),self.config["directory-angular"].replace(/\//g,"\\")):filename.replace(self.config["directory-public"],self.config["directory-angular"]);var notfound=true;if(tmpname!==filename){filename=tmpname;notfound=!fs.existsSync(filename)}if(notfound){self.temporary.path[key]=null;self.response404(req,res);return self}}name=filename;if(extension==="js"||extension==="css"){if(name.lastIndexOf(".min.")===-1&&name.lastIndexOf("-min.")===-1){name=self.compileStatic(req,name);self.temporary.path[key]=name}}name+=";"+fs.statSync(name).size;self.temporary.path[key]=name;if(self.config.debug)delete self.temporary.path[key]}var index=name.lastIndexOf(";");var size=null;if(index===-1)index=name.length;else size=name.substring(index+1);name=name.substring(0,index);var accept=req.headers["accept-encoding"]||"";var returnHeaders={};returnHeaders["Accept-Ranges"]="bytes";returnHeaders[RESPONSE_HEADER_CACHECONTROL]="public";returnHeaders["Expires"]=(new Date).add("d",15);returnHeaders["Vary"]="Accept-Encoding";if(headers)utils.extend(returnHeaders,headers,true);if(downloadName&&downloadName.length>0)returnHeaders["Content-Disposition"]='attachment; filename="'+downloadName+'"';if(etag.length>0)returnHeaders["Etag"]=etag;if(!returnHeaders[RESPONSE_HEADER_CONTENTTYPE])returnHeaders[RESPONSE_HEADER_CONTENTTYPE]=utils.getContentType(extension);var compress=self.config["allow-gzip"]&&REQUEST_COMPRESS_CONTENTTYPE.indexOf(returnHeaders[RESPONSE_HEADER_CONTENTTYPE])!==-1;var range=req.headers["range"]||"";var supportsGzip=accept.lastIndexOf("gzip")!==-1;res.success=true;if(range.length>0)return self.responseRange(name,range,returnHeaders,req,res);if(size!==null&&size!=="0"&&!compress)returnHeaders["Content-Length"]=size;var stream;if(compress&&supportsGzip){returnHeaders["Content-Encoding"]="gzip";res.writeHead(200,returnHeaders);stream=fs.createReadStream(name).pipe(zlib.createGzip());stream.pipe(res);self.stats.response.file++;self._request_stats(false,req.isStaticFile);if(!req.isStaticFile)self.emit("request-end",req,res);return self}res.writeHead(200,returnHeaders);stream=fs.createReadStream(name);stream.pipe(res);self.stats.response.file++;self._request_stats(false,req.isStaticFile);if(!req.isStaticFile)self.emit("request-end",req,res);return self};Framework.prototype.responsePipe=function(req,res,url,headers,timeout,callback){var self=this;if(res.success)return self;var uri=parser.parse(url);var h={};h[RESPONSE_HEADER_CACHECONTROL]="private";if(headers)utils.extend(h,headers,true);h["X-Powered-By"]="total.js v"+self.version_header;var options={protocol:uri.protocol,auth:uri.auth,method:"GET",hostname:uri.hostname,port:uri.port,path:uri.path,agent:false,headers:h};var connection=options.protocol==="https:"?https:http;var supportsGZIP=(req.headers["accept-encoding"]||"").lastIndexOf("gzip")!==-1;var client=connection.get(options,function(response){var contentType=response.headers["content-type"];var isGZIP=(response.headers["content-encoding"]||"").lastIndexOf("gzip")!==-1;var compress=!isGZIP&&supportsGZIP&&(contentType.indexOf("text/")!==-1||contentType.lastIndexOf("javascript")!==-1||contentType.lastIndexOf("json")!==-1);var attachment=response.headers["content-disposition"]||"";if(attachment.length>0)res.setHeader("Content-Disposition",attachment);res.setHeader(RESPONSE_HEADER_CONTENTTYPE,contentType);res.setHeader("Vary","Accept-Encoding");if(compress){res.setHeader("Content-Encoding","gzip");response.pipe(zlib.createGzip()).pipe(res);return}if(!supportsGZIP&&isGZIP)response.pipe(zlib.createGunzip()).pipe(res);else response.pipe(res)});if((timeout||0)>0){client.setTimeout(timeout||3e3,function(){self.response408(req,res);if(callback)callback()})}client.on("close",function(){if(res.success)return;req.clear(true);res.success=true;self.stats.response.pipe++;self._request_stats(false,req.isStaticFile);res.success=true;if(!req.isStaticFile)self.emit("request-end",req,res);if(callback)callback()});return self};Framework.prototype.responseCustom=function(req,res){var self=this;if(res.success)return self;req.clear(true);self.stats.response.custom++;res.success=true;self._request_stats(false,req.isStaticFile);if(!req.isStaticFile)self.emit("request-end",req,res);return self};Framework.prototype.responseImage=function(req,res,filename,fnProcess,headers,useImageMagick){var self=this;var stream=null;if(typeof filename===OBJECT)stream=filename;var key="image-"+req.url.substring(1);var name=self.temporary.path[key];if(name===null){self.response404(req,res);return self}if(typeof name!==UNDEFINED){self.responseFile(req,res,filename,"",headers,key);return self}var im=useImageMagick;if(typeof im===UNDEFINED)im=self.config["default-image-converter"]==="im";if(self.isProcessing(key)){if(req.processing>self.config["default-request-timeout"]){self.response408(req,res);return}req.processing+=500;setTimeout(function(){self.responseImage(req,res,filename,fnProcess,headers,im)},500);return}var Image=require("./image");name=self.path.temp(key.replace(/\//g,"-"));self.temporary.processing[key]=true;if(stream!==null){fs.exists(name,function(exist){if(exist){delete self.temporary.processing[key];self.temporary.path[key]=name;self.responseFile(req,res,name,"",headers,key);return}self._verify_directory("temp");var image=Image.load(stream,im);fnProcess(image);var extension=path.extname(name);if(extension.substring(1)!==image.outputType)name=name.substring(0,name.lastIndexOf(extension))+"."+image.outputType;image.save(name,function(err){delete self.temporary.processing[key];if(err){self.temporary.path[key]=null;self.response500(req,res,err);return}self.temporary.path[key]=name+";"+fs.statSync(name).size;self.responseFile(req,res,name,"",headers,key)})});return self}fs.exists(filename,function(exist){if(!exist){delete self.temporary.processing[key];self.temporary.path[key]=null;self.response404(req,res);return}self._verify_directory("temp");var image=Image.load(filename,im);fnProcess(image);var extension=path.extname(name);if(extension.substring(1)!==image.outputType)name=name.substring(0,name.lastIndexOf(extension))+"."+image.outputType;image.save(name,function(err){delete self.temporary.processing[key];if(err){self.temporary.path[key]=null;self.response500(req,res,err);return}self.temporary.path[key]=name+";"+fs.statSync(name).size;self.responseFile(req,res,name,"",headers,key)})});return self};Framework.prototype.responseImageWithoutCache=function(req,res,filename,fnProcess,headers,useImageMagick){var self=this;var stream=null;if(typeof filename===OBJECT)stream=filename;var key="image-"+req.url.substring(1);var im=useImageMagick;if(typeof im===UNDEFINED)im=self.config["default-image-converter"]==="im";if(self.isProcessing(key)){if(req.processing>self.config["default-request-timeout"]){self.response408(req,res);return}req.processing+=500;setTimeout(function(){self.responseImageWithoutCache(req,res,filename,fnProcess,headers,im)},500);return}var Image=require("./image");if(stream!==null){var image=Image.load(stream,im);fnProcess(image);self.responseStream(req,res,utils.getContentType(image.outputType),image.stream(),null,headers);return self}fs.exists(filename,function(exist){if(!exist){self.response404(req,res);return}self._verify_directory("temp");var image=Image.load(filename,im);fnProcess(image);self.responseStream(req,res,utils.getContentType(image.outputType),image.stream(),null,headers)});return self};Framework.prototype.responseStream=function(req,res,contentType,stream,downloadName,headers){var self=this;if(res.success)return self;req.clear(true);if(contentType.lastIndexOf("/")===-1)contentType=utils.getContentType(contentType);var compress=self.config["allow-gzip"]&&REQUEST_COMPRESS_CONTENTTYPE.indexOf(contentType)!==-1;var accept=req.headers["accept-encoding"]||"";var returnHeaders={};returnHeaders[RESPONSE_HEADER_CACHECONTROL]="public";returnHeaders["Expires"]=(new Date).add("d",15);returnHeaders["Vary"]="Accept-Encoding";if(headers)utils.extend(returnHeaders,headers,true);downloadName=downloadName||"";if(downloadName.length>0)returnHeaders["Content-Disposition"]="attachment; filename="+encodeURIComponent(downloadName);returnHeaders[RESPONSE_HEADER_CONTENTTYPE]=contentType;if(compress&&accept.lastIndexOf("gzip")!==-1){returnHeaders["Content-Encoding"]="gzip";res.writeHead(200,returnHeaders);var gzip=zlib.createGzip();stream.pipe(gzip).pipe(res);self.stats.response.stream++;self._request_stats(false,req.isStaticFile);if(!req.isStaticFile)self.emit("request-end",req,res);return self}res.writeHead(200,returnHeaders);stream.pipe(res);self.stats.response.stream++;self._request_stats(false,req.isStaticFile);if(!req.isStaticFile)self.emit("request-end",req,res);return self};Framework.prototype.responseRange=function(name,range,headers,req,res){var self=this;var arr=range.replace(/bytes=/,"").split("-");var beg=parseInt(arr[0]||"0",10);var end=parseInt(arr[1]||"0",10);var total=self.temporary.range[name]||0;if(total===0){total=fs.statSync(name).size;self.temporary.range[name]=total}if(end===0)end=total-1;if(beg>end){beg=0;end=total-1}var length=end-beg+1;headers["Content-Length"]=length;headers["Content-Range"]="bytes "+beg+"-"+end+"/"+total;res.writeHead(206,headers);var stream=fs.createReadStream(name,{start:beg,end:end});stream.pipe(res);self.stats.response.streaming++;self._request_stats(false,req.isStaticFile);if(!req.isStaticFile)self.emit("request-end",req,res);return self};Framework.prototype.setModified=function(req,res,value){var self=this;var isEtag=typeof value===STRING;if(isEtag){res.setHeader("Etag",value+":"+self.config["etag-version"]);return self}value=value||new Date;res.setHeader("Last-Modified",value.toUTCString());return self};Framework.prototype.notModified=function(req,res,compare,strict){var self=this;var type=typeof compare;if(type===BOOLEAN){var tmp=compare;compare=strict;strict=tmp;type=typeof compare}var isEtag=type===STRING;var val=req.headers[isEtag?"if-none-match":"if-modified-since"];if(isEtag){if(typeof val===UNDEFINED)return false;var myetag=compare+":"+self.config["etag-version"];if(val!==myetag)return false}else{if(typeof val===UNDEFINED)return false;var date=typeof 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))0)req.data.get=qs.parse(req.uri.query);req.session=null;req.user=null;req.flags=[req.isSecure?"https":"http"];var path=utils.path(req.uri.pathname);var websocket=new WebSocketClient(req,socket,head);req.path=internal.routeSplit(req.uri.pathname);if(self.onAuthorization===null){var route=self.lookup_websocket(req,websocket.uri.pathname,true);if(route===null){websocket.close();req.connection.destroy();return}self._upgrade_continue(route,req,websocket,path);return}self.onAuthorization.call(self,req,websocket,req.flags,function(isLogged,user){if(user)req.user=user;req.flags.push(isLogged?"authorize":"unauthorize");var route=self.lookup_websocket(req,websocket.uri.pathname,false);if(route===null){websocket.close();req.connection.destroy();return}self._upgrade_continue(route,req,websocket,path)})};Framework.prototype._upgrade_continue=function(route,req,socket,path){var self=this;if(!socket.prepare(route.flags,route.protocols,route.allow,route.length,self.version_header)){socket.close();req.connection.destroy();return}var id=path+(route.flags.length>0?"#"+route.flags.join("-"):"");if(route.isBINARY)socket.type=1;else if(route.isJSON)socket.type=3;if(typeof self.connections[id]===UNDEFINED){var connection=new WebSocket(self,path,route.name,id);self.connections[id]=connection;route.onInitialize.apply(connection,internal.routeParam(route.param.length>0?internal.routeSplit(req.uri.pathname,true):req.path,route))}socket.upgrade(self.connections[id])};Framework.prototype._service=function(count){var self=this;if(self.config.debug)self.resources={};if(count%20===0){self.emit("clear","resources");self.resources={};if(typeof gc!==UNDEFINED)gc()}if(count%3===0){self.emit("clear","temporary",self.temporary);self.temporary.path={};self.temporary.range={};self.temporary.views={}}self.emit("service",count)};Framework.prototype._request=function(req,res){var self=this;if(self.config["allow-performance"]){req.connection.setNoDelay(true);req.connection.setTimeout(0)}if(self.onRequest!==null&&self.onRequest(req,res))return;res.setHeader("X-Powered-By","total.js v"+self.version_header);var headers=req.headers;var protocol=req.connection.encrypted?"https":"http";if(self._request_check_redirect){var redirect=self.routes.redirects[protocol+"://"+req.host];if(redirect){self.stats.response.forwarding++;self.responseRedirect(req,res,redirect.url+(redirect.path?req.url:""),redirect.permanent);return self}}if(self.restrictions.isRestrictions){if(self.restrictions.isAllowedIP){if(self.restrictions.allowedIP.indexOf(req.ip)===-1){self.stats.response.restriction++;req.connection.destroy();return self}}if(self.restrictions.isBlockedIP){if(self.restrictions.blockedIP.indexOf(req.ip)!==-1){self.stats.response.restriction++;req.connection.destroy();return self}}if(self.restrictions.isAllowedCustom){if(!self.restrictions._allowedCustom(headers)){self.stats.response.restriction++;req.connection.destroy();return self}}if(self.restrictions.isBlockedCustom){if(self.restrictions._blockedCustom(headers)){self.stats.response.restriction++;req.connection.destroy();return self}}}if(self.config.debug)res.setHeader("Mode","debug");res.success=false;req.uri=parser.parse(protocol+"://"+req.host+req.url);req.path=internal.routeSplit(req.uri.pathname);req.processing=0;if(utils.isStaticFile(req.uri.pathname)){req.isStaticFile=true;self.stats.request.file++;self._request_stats(true,true);if(self._length_files===0){self.responseStatic(req,res);return}new Subscribe(self,req,res,3).file();return}req.xhr=headers["x-requested-with"]==="XMLHttpRequest";req.isProxy=headers["x-proxy"]==="total.js";req.data={get:{},post:{},files:[]};req.flags=null;req.buffer_exceeded=false;req.buffer_data="";req.buffer_has=false;req.session=null;req.user=null;req.prefix="";req.isAuthorized=true;var isXSS=false;var accept=headers.accept;self._request_stats(true,false);self.stats.request.web++;if(req.uri.query&&req.uri.query.length>0){if(self.onXSS!==null)isXSS=self.onXSS(req.uri.query);req.data.get=qs.parse(req.uri.query)}if(self.onRoute!==null){try{if(!self.onRoute(req,res)){if(!res.success){self._request_stats(false,false);self.stats.request.blocked++;req.connection.destroy()}return}}catch(err){self.response500(req,res,err);return}}var flags=[req.method.toLowerCase()];var multipart=req.headers["content-type"]||"";flags.push(protocol);if(multipart.indexOf("multipart/form-data")===-1){if(multipart.indexOf("application/json")!==-1)flags.push("json");if(multipart.indexOf("mixed")===-1)multipart="";else flags.push("mmr")}if(multipart.length>0)flags.push("upload");if(req.isProxy)flags.push("proxy");if(accept==="text/event-stream")flags.push("sse");if(self.config.debug)flags.push("debug");req.prefix=self.onPrefix===null?"":self.onPrefix(req)||"";if(req.prefix.length>0)flags.push("#"+req.prefix);flags.push("+xhr");if(req.xhr){self.stats.request.xhr++;flags.push("xhr")}if(isXSS){flags.push("xss");self.stats.request.xss++}if(self._request_check_referer){var referer=headers["referer"]||"";if(referer!==""&&referer.indexOf(headers["host"])!==-1)flags.push("referer")}req.flags=flags;self.emit("request-begin",req,res);if(req.method==="GET"||req.method==="DELETE"||req.method==="OPTIONS"){if(req.method==="DELETE")self.stats.request["delete"]++;else self.stats.request.get++;new Subscribe(self,req,res,0).end();return}if(self._request_check_POST&&(req.method==="POST"||req.method==="PUT")){if(multipart.length>0){self.stats.request.upload++;new Subscribe(self,req,res,2).multipart(multipart)}else{if(req.method==="PUT")self.stats.request.put++;else self.stats.request.post++;new Subscribe(self,req,res,1).urlencoded()}return}self.emit("request-end",req,res);self._request_stats(false,false);self.stats.request.blocked++;req.connection.destroy()};Framework.prototype._request_stats=function(beg,isStaticFile){var self=this;if(beg)self.stats.request.pending++;else self.stats.request.pending--;if(self.stats.request.pending<0)self.stats.request.pending=0;return self};Framework.prototype.model=function(name){var self=this;var model=self.models[name];if(model)return model;var filename=path.join(directory,self.config["directory-models"],name);if(self.isCoffee){if(fs.existsSync(filename+EXTENSION_COFFEE))filename+=EXTENSION_COFFEE;else filename+=EXTENSION_JS}else filename+=EXTENSION_JS;model=require(filename);self.models[name]=model;return model};Framework.prototype.source=function(name){var self=this;var source=self.sources[name];if(source)return source;var filename=path.join(directory,self.config["directory-source"],name);if(self.isCoffee){if(fs.existsSync(filename+EXTENSION_COFFEE))filename+=EXTENSION_COFFEE;else filename+=EXTENSION_JS}else filename+=EXTENSION_JS;source=require(filename);self.sources[name]=source;return source};Framework.prototype.assert=function(name,url,flags,callback,data,cookies,headers){var self=this;if(typeof url===FUNCTION){self.tests[_test+": "+name]={run:url};return self}var method="GET";var length=0;var isJSON=false;headers=utils.extend({},headers||{});if(flags instanceof Array){length=flags.length;for(var i=0;i0)headers["Cookie"]=builder.join("; ")}if(typeof data!==STRING)data=isJSON?JSON.stringify(data):qs.stringify(data);if(data&&data.length>0)headers["Content-Length"]=data.length;var obj={url:url,callback:callback,method:method,data:data||"",headers:headers};self.tests[_test+": "+name]=obj;return self};Framework.prototype.testing=function(stop,callback){if(typeof stop===UNDEFINED)stop=true;var self=this;var keys=Object.keys(self.tests);if(keys.length===0){if(callback)callback();if(stop)self.stop();return self}var key=keys[0];var test=self.tests[key];var caption="Success .............. "+key;delete self.tests[key];if(test.run){try{test.run.call(self,function(){console.log(caption);self.testing(stop,callback)},key)}catch(e){setTimeout(function(){self.stop()},500);throw e}return self}var response=function(res){res._buffer="";res.on("data",function(chunk){this._buffer+=chunk.toString(ENCODING)});res.on("end",function(){var cookie=res.headers["cookie"]||"";var cookies={};if(cookie.length!==0){var arr=cookie.split(";");var length=arr.length;for(var i=0;i0||test.url.indexOf("https://")>0?"":"http://"+self.ip+":"+self.port)+test.url);var con=options.protocol==="https:"?https:http;var req=test.method==="POST"||test.method==="PUT"?con.request(options,response):con.get(options,response);req.on("error",function(error){setTimeout(function(){self.stop()},500);throw error});if(test.data.length>0)req.end(test.data,ENCODING);else req.end();return self};Framework.prototype.test=function(stop,names,cb){var self=this;if(typeof names===FUNCTION){cb=names;names=[]}else names=names||[];var counter=0;self.isTest=true;var dir=self.config["directory-tests"];if(!fs.existsSync(utils.combine(dir))){if(cb)cb();if(stop)setTimeout(function(){framework.stop() -},500);return self}fs.readdirSync(utils.combine(dir)).forEach(function(name){var filename=path.join(directory,dir,name);var ext=path.extname(filename).toLowerCase();if(ext!==EXTENSION_JS&&ext!==EXTENSION_COFFEE)return;if(names.length>0&&names.indexOf(name.substring(0,name.length-3))===-1)return;var test=require(filename);try{var isRun=typeof test.run!==UNDEFINED;var isInstall=typeof test.isInstall!==UNDEFINED;var isInit=typeof test.init!==UNDEFINED;var isLoad=typeof test.load!==UNDEFINED;_test=name;if(isRun)test.run(self,name);else if(isInstall)test.install(self,name);else if(isInit)test.init(self,name);else if(isLoad)test.load(self,name);counter++}catch(ex){setTimeout(function(){framework.stop()},500);throw ex}});_test="";if(counter===0){if(cb)cb();if(stop)setTimeout(function(){framework.stop()},500);return self}setTimeout(function(){console.log("====== TESTING ======");console.log("");self.testing(stop,function(){self.isTest=false;if(cb)cb()})},100);return self};Framework.prototype.clear=function(){var self=this;var dir=utils.combine(self.config["directory-temp"]);if(!fs.existsSync(dir))return self;fs.readdir(dir,function(err,files){if(err)return;var arr=[];var length=files.length;for(var i=0;i0){accepts.forEach(function(accept){if(self.config["static-accepts"].indexOf(accept)===-1)self.config["static-accepts"].push(accept)})}if(self.config["allow-performance"])http.globalAgent.maxSockets=9999;self.emit("configure",self.config);return self};Framework.prototype.routeJS=function(name){var self=this;if(name.lastIndexOf(EXTENSION_JS)===-1)name+=EXTENSION_JS;return self._routeStatic(name,self.config["static-url-js"])};Framework.prototype.routeCSS=function(name){var self=this;if(name.lastIndexOf(".css")===-1)name+=".css";return self._routeStatic(name,self.config["static-url-css"])};Framework.prototype.routeImage=function(name){var self=this;return self._routeStatic(name,self.config["static-url-image"])};Framework.prototype.routeVideo=function(name){var self=this;return self._routeStatic(name,self.config["static-url-video"])};Framework.prototype.routeFont=function(name){var self=this;return self._routeStatic(name,self.config["static-url-font"])};Framework.prototype.routeDownload=function(name){var self=this;return self._routeStatic(name,self.config["static-url-download"])};Framework.prototype.routeStatic=function(name){var self=this;return self._routeStatic(name,self.config["static-url"])};Framework.prototype._routeStatic=function(name,directory){return directory+this._version(name)};Framework.prototype._version=function(name){var self=this;if(self.versions!==null)name=self.versions[name]||name;if(self.onVersion!==null)name=self.onVersion(name)||name;return name};Framework.prototype.lookup=function(req,url,flags,noLoggedUnlogged){var self=this;var isSystem=url[0]==="#";if(isSystem)req.path=[url];var subdomain=req.subdomain===null?null:req.subdomain.join(".");var length=self.routes.web.length;for(var i=0;i0){var result=internal.routeCompareFlags(flags,route.flags,noLoggedUnlogged?true:route.isMEMBER);if(result===-1)req.isAuthorized=false;if(result<1)continue}else{if(flags.indexOf("xss")!==-1)continue}return route}return null};Framework.prototype.lookup_websocket=function(req,url,noLoggedUnlogged){var self=this;var subdomain=req.subdomain===null?null:req.subdomain.join(".");var length=self.routes.websockets.length;for(var i=0;i0){var result=internal.routeCompareFlags(req.flags,route.flags,noLoggedUnlogged?true:route.isMEMBER);if(result===-1)req.isAuthorized=false;if(result<1)continue}return route}return null};Framework.prototype.accepts=function(extension,contentType){var self=this;if(extension[0]!==".")extension="."+extension;if(self.config["static-accepts"].indexOf(extension)===-1)self.config["static-accepts"].push(extension);if(contentType)utils.setContentType(extension,contentType);return self};Framework.prototype.worker=function(name,id,timeout){var self=this;var fork=null;var type=typeof id;if(type===NUMBER&&typeof timeout===UNDEFINED){timeout=id;id=null;type=UNDEFINED}if(type===STRING)fork=self.workers[id]||null;if(fork!==null)return fork;var filename=utils.combine(self.config["directory-workers"],name);if(self.isCoffee){if(fs.existsSync(filename+EXTENSION_COFFEE))filename+=EXTENSION_COFFEE;else filename+=EXTENSION_JS}else filename+=EXTENSION_JS;fork=child.fork(filename,{cwd:directory});id=name+"_"+(new Date).getTime();fork.__id=id;self.workers[id]=fork;fork.on("exit",function(){var self=this;if(self.__timeout)clearTimeout(self.__timeout);delete framework.workers[self.__id]});if(typeof timeout!==NUMBER)return fork;fork.__timeout=setTimeout(function(){fork.kill();fork=null},timeout);return fork};function FrameworkRestrictions(framework){this.framework=framework;this.isRestrictions=false;this.isAllowedIP=false;this.isBlockedIP=false;this.isAllowedCustom=false;this.isBlockedCustom=false;this.allowedIP=[];this.blockedIP=[];this.allowedCustom={};this.blockedCustom={};this.allowedCustomKeys=[];this.blockedCustomKeys=[]}FrameworkRestrictions.prototype.allow=function(name,value){var self=this;if(typeof value===UNDEFINED){self.allowedIP.push(name);self.refresh();return self.framework}if(typeof self.allowedCustom[name]===UNDEFINED)self.allowedCustom[name]=[value];else self.allowedCustom[name].push(value);self.refresh();return self.framework};FrameworkRestrictions.prototype.disallow=function(name,value){var self=this;if(typeof value===UNDEFINED){self.blockedIP.push(name);self.refresh();return self.framework}if(typeof self.blockedCustom[name]===UNDEFINED)self.blockedCustom[name]=[value];else self.blockedCustom[name].push(value);self.refresh();return self.framework};FrameworkRestrictions.prototype.refresh=function(){var self=this;self.isAllowedIP=self.allowedIP.length>0;self.isBlockedIP=self.blockedIP.length>0;self.isAllowedCustom=!utils.isEmpty(self.allowedCustom);self.isBlockedCustom=!utils.isEmpty(self.blockedCustom);self.allowedCustomKeys=Object.keys(self.allowedCustom);self.blockedCustomKeys=Object.keys(self.blockedCustom);self.isRestrictions=self.isAllowedIP||self.isBlockedIP||self.isAllowedCustom||self.isBlockedCustom;return self.framework};FrameworkRestrictions.prototype.clearIP=function(){var self=this;self.allowedIP=[];self.blockedIP=[];self.refresh();return self.framework};FrameworkRestrictions.prototype.clearHeaders=function(){var self=this;self.allowedCustom={};self.blockedCustom={};self.allowedCustomKeys=[];self.blockedCustomKeys=[];self.refresh();return self.framework};FrameworkRestrictions.prototype._allowedCustom=function(headers){var self=this;var length=self.allowedCustomKeys.length;for(var i=0;i399&&(self.route===null||self.route.name[0]==="#")){switch(status){case 400:self.framework.stats.response.error400++;break;case 401:self.framework.stats.response.error401++;break;case 403:self.framework.stats.response.error403++;break;case 404:self.framework.stats.response.error404++;break;case 408:self.framework.stats.response.error408++;break;case 431:self.framework.stats.response.error431++;break;case 500:self.framework.stats.response.error500++;break;case 501:self.framework.stats.response.error501++;break}}if(self.route===null){self.framework.responseContent(self.req,self.res,status||404,utils.httpStatus(status||404),CONTENTTYPE_TEXTPLAIN,self.framework.config["allow-gzip"]);return self}var name=self.route.name;self.controller=new Controller(name,self.req,self.res,self);self.controller.exception=self.exception;if(!self.isCanceled&&!self.isMixed&&self.route.timeout>0)self.timeout=setTimeout(self.handlers._cancel,self.route.timeout);if(self.framework._length_partial_private===0&&self.framework._length_partial_global===0){self.handlers._execute();return self}if(self.framework._length_partial_global===0&&self.route.partial===null){self.handlers._execute();return self}var funcs=[];var count=0;if(self.framework._length_partial_global>0){for(var i=0;i0?internal.routeSplit(self.req.uri.pathname,true):self.req.path,self.route));return}self.framework._verify_directory("temp");internal.parseMULTIPART_MIXED(self.req,self.header,self.framework.config["directory-temp"],function(file){self.route.onExecute.call(self.controller,file)},self.handlers._end)}catch(err){self.controller=null;self.framework.error(err,name,self.req.uri);self.route=self.framework.lookup(self.req,"#500",[]);self.execute(500)}};Subscribe.prototype._authorization=function(isLogged,user){var self=this;if(user)self.req.user=user;self.req.flags.push(isLogged?"authorize":"unauthorize");self.route=self.framework.lookup(self.req,self.req.buffer_exceeded?"#431":self.req.uri.pathname,self.req.flags);if(self.route===null)self.route=self.framework.lookup(self.req,self.req.isAuthorized?"#404":"#401",[]);self.execute(self.req.buffer_exceeded?431:404)};Subscribe.prototype._end=function(){var self=this;if(self.isMixed){self.req.clear(true);var headers={};headers[RESPONSE_HEADER_CONTENTTYPE]="text/plain; charset=utf-8";headers[RESPONSE_HEADER_CACHECONTROL]="private, max-age=0";self.res.writeHead(200,headers);self.res.end("END");self.framework._request_stats(false,false);self.framework.emit("request-end",self.req,self.res);return}if(self.req.buffer_exceeded){self.route=self.framework.lookup(self.req,"#431",[]);if(self.route===null){self.framework.response431(self.req,self.res);return}self.execute(431);return}if(self.req.buffer_data.length===0){if(self.route!==null&&!self.route.isXSS&&self.req.flags.indexOf("xss")!==-1){self.route400();return}self.prepare(self.req.flags,self.req.uri.pathname);return}if(self.route.isJSON){try{if(!self.req.buffer_data.isJSON()){self.route400();return}self.req.data.post=JSON.parse(self.req.buffer_data);self.req.buffer_data=null;self.prepare(self.req.flags,self.req.uri.pathname)}catch(err){self.route400()}return}if(!self.route.isXSS&&self.framework.onXSS!==null){if(self.framework.onXSS(self.req.buffer_data)){self.req.flags.push("xss");self.framework.stats.request.xss++;self.route400();return}}if(self.route!==null&&self.route.isRAW){self.req.data.post=self.req.buffer_data}else{if((self.req.headers["content-type"]||"").indexOf("x-www-form-urlencoded")===-1){self.route400();return}self.req.data.post=qs.parse(self.req.buffer_data)}self.prepare(self.req.flags,self.req.uri.pathname)};Subscribe.prototype.route400=function(){var self=this;self.route=self.framework.lookup(self.req,"#400",[]);self.execute(400) -};Subscribe.prototype._endfile=function(){var self=this;if(self.req.uri.query&&self.req.uri.query.length>0){self.req.data={};self.req.data.get=qs.parse(self.req.uri.query)}for(var i=0;i0){var result=internal.routeCompareFlags(route.flags,flags,true);if(result===-1)req.isAuthorized=false;if(result<1)continue}else{if(flags.indexOf("xss")!==-1)continue}selected=route;break}if(!selected)return false;self.cancel();self.req.path=[];self.subscribe.success();self.subscribe.route=selected;self.subscribe.execute(404);return true};Controller.prototype.cancel=function(){var self=this;if(typeof self._async!==UNDEFINED)self._async.cancel();self.isCanceled=true;return self};Controller.prototype.log=function(){var self=this;self.framework.log.apply(self.framework,arguments);return self};Controller.prototype.meta=function(){var self=this;self.repository[REPOSITORY_META_TITLE]=arguments[0]||"";self.repository[REPOSITORY_META_DESCRIPTION]=arguments[1]||"";self.repository[REPOSITORY_META_KEYWORDS]=arguments[2]||"";self.repository[REPOSITORY_META_IMAGE]=arguments[3]||"";return self};Controller.prototype.$meta=function(){var self=this;if(arguments.length!==0){self.meta.apply(self,arguments);return""}var repository=self.repository;return self.framework.onMeta.call(self,repository[REPOSITORY_META_TITLE],repository[REPOSITORY_META_DESCRIPTION],repository[REPOSITORY_META_KEYWORDS],repository[REPOSITORY_META_IMAGE])};Controller.prototype.title=function(value){var self=this;self.$title(value);return self};Controller.prototype.description=function(value){var self=this;self.$description(value);return self};Controller.prototype.keywords=function(value){var self=this;self.$keywords(value);return self};Controller.prototype.$title=function(value){var self=this;if(!value)return self.repository[REPOSITORY_META_TITLE]||"";self.repository[REPOSITORY_META_TITLE]=value;return""};Controller.prototype.$description=function(value){var self=this;if(!value)return self.repository[REPOSITORY_META_DESCRIPTION]||"";self.repository[REPOSITORY_META_DESCRIPTION]=value;return""};Controller.prototype.$keywords=function(value){var self=this;if(!value)return self.repository[REPOSITORY_META_KEYWORDS]||"";self.repository[REPOSITORY_META_KEYWORDS]=value;return""};Controller.prototype.sitemap=function(name,url,index){var self=this;if(typeof name===UNDEFINED)return self.repository.sitemap||[];if(typeof url===UNDEFINED)url=self.req.url;if(typeof self.repository.sitemap===UNDEFINED)self.repository.sitemap=[];self.repository.sitemap.push({name:name,url:url,index:index||self.repository.sitemap.length});if(typeof index!==UNDEFINED&&self.sitemap.length>1){self.repository.sitemap.sort(function(a,b){if(a.indexb.index)return 1;return 0})}return self};Controller.prototype.$sitemap=function(name,url,index){var self=this;self.sitemap.apply(self,arguments);return""};Controller.prototype.module=function(name){return this.framework.module(name)};Controller.prototype.layout=function(name){var self=this;self.layoutName=name;return self};Controller.prototype.$layout=function(name){var self=this;self.layoutName=name;return""};Controller.prototype.model=function(name){var self=this;return self.framework.model(name)};Controller.prototype.models=function(name){var self=this;return(self.controllers[name||self.name]||{}).models};Controller.prototype.mail=function(address,subject,view,model,callback){if(typeof model===FUNCTION){callback=model;model=null}var self=this;var body=self.view(view,model,true);framework.onMail(address,subject,body,callback);return self};Controller.prototype.functions=function(name){var self=this;return(self.controllers[name||self.name]||{}).functions};Controller.prototype.notModified=function(compare,strict){var self=this;return self.framework.notModified(self.req,self.res,compare,strict)};Controller.prototype.setModified=function(value){var self=this;self.framework.setModified(self.req,self.res,value);return self};Controller.prototype.setExpires=function(date){var self=this;if(typeof date===UNDEFINED)return self;self.res.setHeader("Expires",date.toUTCString());return self};Controller.prototype.$view=function(name,model){return this.$viewToggle(true,name,model)};Controller.prototype.$viewToggle=function(visible,name,model){if(!visible)return"";var self=this;var layout=self.layoutName;self.layoutName="";var value=self.view(name,model,null,true);self.layoutName=layout;return value};Controller.prototype.$ng=function(name){var self=this;var length=arguments.length;if(length>1){for(var i=0;i1){for(var i=0;i2){for(var i=1;i'};Controller.prototype.$ngController=function(name){var self=this;var length=arguments.length;if(length>1){for(var i=0;i'+tmp+""};Controller.prototype.$ngDirective=function(name){var self=this;var length=arguments.length;if(length>1){for(var i=0;i1){for(var i=0;i1){for(var i=0;i1){for(var i=0;i1){for(var i=0;i";var value=model[name]||attr.value||"";return builder+">"+value.toString().encode()+""};Controller.prototype.$input=function(model,type,name,attr){var builder=[""+builder+" "+attr.label+"";return builder};Controller.prototype.$dns=function(value){var builder="";var self=this;var length=arguments.length;for(var i=0;i';self.head(builder);return""};Controller.prototype.$prefetch=function(){var builder="";var self=this;var length=arguments.length;for(var i=0;i';self.head(builder);return""};Controller.prototype.$prerender=function(value){var builder="";var self=this;var length=arguments.length;for(var i=0;i';self.head(builder);return""};Controller.prototype.$next=function(value){var self=this;self.head('');return""};Controller.prototype.$prev=function(value){var self=this;self.head('');return""};Controller.prototype.$canonical=function(value){var self=this;self.head('');return""};Controller.prototype._prepareHost=function(value){var tmp=value.substring(0,5);if(tmp!=="http:"&&tmp!=="https://"){if(tmp[0]!=="/"||tmp[1]!=="/")value=this.host(value)}return value};Controller.prototype.head=function(){var self=this;var length=arguments.length;var header=self.repository[REPOSITORY_HEAD]||"";if(length===0){var angularBeg=(self.repository[REPOSITORY_ANGULAR]||"")+(self.repository[REPOSITORY_ANGULAR_COMMON]||"")+(self.repository[REPOSITORY_ANGULAR_LOCALE]||"");var angularEnd=(angularBeg.length>0?self.$script_create("/app.js"):"")+(self.repository[REPOSITORY_ANGULAR_OTHER]||"")+(self.repository[REPOSITORY_ANGULAR_CONTROLLER]||"");return(self.config.author&&self.config.author.length>0?'':"")+angularBeg+header+angularEnd}var output="";for(var i=0;i0&&header.indexOf(val)!==-1)continue;if(val.indexOf("<")!==-1){output+=val;continue}var tmp=val.substring(0,7);var isRoute=tmp[0]!=="/"&&tmp[1]!=="/"&&tmp!=="http://"&&tmp!=="https:/";if(val.lastIndexOf(EXTENSION_JS)!==-1)output+='';else if(val.lastIndexOf(".css")!==-1)output+=''}header+=output;self.repository[REPOSITORY_HEAD]=header;return self};Controller.prototype.$head=function(){var self=this;self.head.apply(self,arguments);return""};Controller.prototype.place=function(name){var self=this;var key=REPOSITORY_PLACE+"_"+name;var length=arguments.length;if(length===1)return self.repository[key]||"";var output="";for(var i=1;i'}self.repository[key]=(self.repository[key]||"")+output;return self};Controller.prototype.$place=function(){var self=this;if(arguments.length===1)return self.place.apply(self,arguments);self.place.apply(self,arguments);return""};Controller.prototype.$isValue=function(bool,charBeg,charEnd,value){if(!bool)return"";charBeg=charBeg||" ";charEnd=charEnd||"";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=utils.parseInt(date[0]||"");var month=utils.parseInt(date[1]||"")-1;var day=utils.parseInt(date[2]||"")-1;if(month<0)month=0;if(day<0)day=0;var hour=utils.parseInt(time[0]||"");var minute=utils.parseInt(time[1]||"");var second=utils.parseInt(time[2]||"");date=new Date(year,month,day,hour,minute,second,0)}else if(utils.isDate(value))date=value;if(typeof date===UNDEFINED)return"";self.setModified(date);return""};Controller.prototype.$etag=function(value){this.setModified(value);return""};Controller.prototype.$options=function(arr,selected,name,value){var self=this;var type=typeof arr;if(arr===null||typeof arr===UNDEFINED)return"";var isObject=false;var tmp=null;if(!(arr instanceof Array)&&type===OBJECT){isObject=true;tmp=arr;arr=Object.keys(arr)}if(!utils.isArray(arr))arr=[arr];selected=selected||"";var options="";if(!isObject){if(typeof value===UNDEFINED)value=value||name||"value";if(typeof name===UNDEFINED)name=name||"name"}var isSelected=false;var length=0;length=arr.length;for(var i=0;i"+text.toString().encode()+""}return options};Controller.prototype.$script=function(name){return this.routeJS(name,true)};Controller.prototype.$js=function(name){return this.routeJS(name,true)};Controller.prototype.$css=function(name){return this.routeCSS(name,true)};Controller.prototype.$image=function(name,width,height,alt,className){var style="";if(typeof width===OBJECT){height=width.height;alt=width.alt;className=width.class;style=width.style;width=width.width}var builder='0)builder+=' height="'+height+ATTR_END;if(alt)builder+=' alt="'+alt.encode()+ATTR_END;if(className)builder+=' class="'+className+ATTR_END;if(style)builder+=' style="'+style+ATTR_END;return builder+' border="0" />'};Controller.prototype.$download=function(filename,innerHTML,downloadName,className){var builder='"+(innerHTML||filename)+""};Controller.prototype.$json=function(obj,name,beautify){if(typeof name===BOOLEAN){var tmp=name;name=beautify;beautify=name}var value=beautify?JSON.stringify(obj,null,4):JSON.stringify(obj);if(!name)return value;return'"};Controller.prototype.$favicon=function(name){var self=this;var contentType="image/x-icon";if(typeof name===UNDEFINED)name="favicon.ico";if(name.lastIndexOf(".png")!==-1)contentType="image/png";if(name.lastIndexOf(".gif")!==-1)contentType="image/gif";name=self.framework.routeStatic("/"+name);return''};Controller.prototype._routeHelper=function(current,name,fn){var self=this;if(current.length===0)return fn.call(self.framework,name);if(current.substring(0,2)==="//"||current.substring(0,6)==="http:/"||current.substring(0,7)==="https:/")return fn.call(self.framework,current+name);if(current[0]==="~")return fn.call(self.framework,utils.path(current.substring(1))+name);return fn.call(self.framework,utils.path(current)+name)};Controller.prototype.routeJS=function(name,tag){var self=this;if(typeof name===UNDEFINED)name="default.js";var url=self._routeHelper(self._currentJS,name,self.framework.routeJS);return tag?'':url};Controller.prototype.routeCSS=function(name,tag){var self=this;if(typeof name===UNDEFINED)name="default.css";var url=self._routeHelper(self._currentCSS,name,self.framework.routeCSS);return tag?'':url};Controller.prototype.routeImage=function(name){var self=this;return self._routeHelper(self._currentImage,name,self.framework.routeImage)};Controller.prototype.routeVideo=function(name){var self=this;return self._routeHelper(self._currentVideo,name,self.framework.routeVideo)};Controller.prototype.routeFont=function(name){var self=this;return self.framework.routeFont(name)};Controller.prototype.routeDownload=function(name){var self=this;return self._routeHelper(self._currentDownload,name,self.framework.routeDownload)};Controller.prototype.routeStatic=function(name){var self=this;return self.framework.routeStatic(name)};Controller.prototype.$currentJS=function(path){this._currentJS=path&&path.length>0?path:"";return""};Controller.prototype.$currentView=function(path){var self=this;if(typeof path===UNDEFINED){self._currentView=self.name[0]!=="#"&&self.name!=="default"?"/"+self.name+"/":"";return self}self._currentView=path&&path.length>0?utils.path(path):"";return""};Controller.prototype.$currentTemplate=function(path){this._currentTemplate=path&&path.length>0?utils.path(path):"";return""};Controller.prototype.$currentContent=function(path){this._currentContent=path&&path.length>0?utils.path(path):"";return""};Controller.prototype.currentView=function(path){var self=this; -self.$currentView(path);self._defaultView=self._currentView;return self};Controller.prototype.currentTemplate=function(path){var self=this;self.$currentTemplate(path);self._defaultTemplate=self._currentTemplate;return self};Controller.prototype.currentContent=function(path){var self=this;self.$currentContent(path);self._defaultContent=self._currentContent;return self};Controller.prototype.$currentCSS=function(path){this._currentCSS=path&&path.length>0?path:"";return""};Controller.prototype.$currentImage=function(path){this._currentImage=path&&path.length>0?path:"";return""};Controller.prototype.$currentVideo=function(path){this._currentVideo=path&&path.length>0?path:"";return""};Controller.prototype.$currentDownload=function(path){this._currentDownload=path&&path.length>0?path:"";return""};Controller.prototype.currentImage=function(path){var self=this;self.$currentImage(path);self._defaultImage=self._currentImage;return self};Controller.prototype.currentDownload=function(path){var self=this;self.$currentDownload(path);self._defaultDownload=self._currentDownload;return self};Controller.prototype.currentCSS=function(path){var self=this;self.$currentCSS(path);self._defaultCSS=self._currentCSS;return self};Controller.prototype.currentJS=function(path){var self=this;self.$currentJS(path);self._defaultJS=self._currentJS;return self};Controller.prototype.currentVideo=function(path){var self=this;self.$currentVideo(path);self._defaultVideo=self._currentVideo;return self};Controller.prototype.resource=function(name,key){var self=this;return self.framework.resource(name,key)};Controller.prototype.template=function(name,model,nameEmpty,repository){var self=this;if(typeof nameEmpty===OBJECT){repository=nameEmpty;nameEmpty=""}if(typeof model===UNDEFINED||model===null||model.length===0){if(typeof nameEmpty!==UNDEFINED&&nameEmpty.length>0)return self.$content(nameEmpty);return""}if(typeof repository===UNDEFINED)repository=self.repository;var plus="";if(name[0]!=="~")plus=self._currentTemplate;try{return internal.generateTemplate(self,name,model,repository,plus)}catch(ex){self.error(new Error("Template: "+name+" - "+ex.toString()));return""}};Controller.prototype.component=function(name){var self=this;var component=framework.component(name);if(component===null)return"";var length=arguments.length;var params=[];for(var i=1;i0)self.problem(problem);if(self.res.success||!self.isConnected)return self;self.req.path=[];self.subscribe.success();self.subscribe.route=self.framework.lookup(self.req,"#400",[]);self.subscribe.exception=problem;self.subscribe.execute(400);return self};Controller.prototype.throw401=function(problem){return this.view401(problem)};Controller.prototype.view401=function(problem){var self=this;if(problem&&problem.length>0)self.problem(problem);if(self.res.success||!self.isConnected)return self;self.req.path=[];self.subscribe.success();self.subscribe.route=self.framework.lookup(self.req,"#401",[]);self.subscribe.exception=problem;self.subscribe.execute(401);return self};Controller.prototype.throw403=function(problem){return this.view403(problem)};Controller.prototype.view403=function(problem){var self=this;if(problem&&problem.length>0)self.problem(problem);if(self.res.success||!self.isConnected)return self;self.req.path=[];self.subscribe.success();self.subscribe.route=self.framework.lookup(self.req,"#403",[]);self.subscribe.exception=problem;self.subscribe.execute(403);return self};Controller.prototype.throw404=function(problem){return this.view404(problem)};Controller.prototype.view404=function(problem){var self=this;if(problem&&problem.length>0)self.problem(problem);if(self.res.success||!self.isConnected)return self;self.req.path=[];self.subscribe.success();self.subscribe.route=self.framework.lookup(self.req,"#404",[]);self.subscribe.exception=problem;self.subscribe.execute(404);return self};Controller.prototype.view500=function(error){var self=this;self.framework.error(typeof error===STRING?new Error(error):error,self.name,self.req.uri);if(self.res.success||!self.isConnected)return self;self.req.path=[];self.subscribe.exception=error;self.subscribe.success();self.subscribe.route=self.framework.lookup(self.req,"#500",[]);self.subscribe.exception=error;self.subscribe.execute(500);return self};Controller.prototype.throw500=function(error){return this.view500(error)};Controller.prototype.view501=function(problem){var self=this;if(problem&&problem.length>0)self.problem(problem);if(self.res.success||!self.isConnected)return self;self.req.path=[];self.subscribe.success();self.subscribe.route=self.framework.lookup(self.req,"#501",[]);self.subscribe.exception=problem;self.subscribe.execute(501);return self};Controller.prototype.throw501=function(problem){return this.view501(problem)};Controller.prototype.redirect=function(url,permanent){var self=this;if(self.res.success||!self.isConnected)return self;self.subscribe.success();self.req.clear(true);self.res.success=true;self.res.writeHead(permanent?301:302,{Location:url});self.res.end();self.framework._request_stats(false,false);self.framework.emit("request-end",self.req,self.res);self.framework.stats.response.redirect++;return self};Controller.prototype.redirectAsync=function(url,permanent){var self=this;var fn=function(){self.redirect(url,permanent)};self.async.complete(fn);return self};Controller.prototype.binary=function(buffer){var self=this;if(self.res.success||!self.isConnected)return self;self.subscribe.success();self.req.clear(true);self.res.success=true;self.res.write(buffer.toString("binary"),"binary");self.res.end();self.framework._request_stats(false,false);self.framework.emit("request-end",self.req,self.res);self.framework.stats.response.binary++;return self};Controller.prototype.baa=function(name){var self=this;var authorization=self.req.headers["authorization"]||"";if(authorization===""){self.res.setHeader("WWW-Authenticate",'Basic realm="'+(name||"Administration")+'"');self.view401();return null}return self.req.authorization()};Controller.prototype.sse=function(data,eventname,id,retry){var self=this;var res=self.res;if(!self.isConnected)return self;if(self.type===0&&res.success)throw new Error("Response was sent.");if(self.type>0&&self.type!==1)throw new Error("Response was used.");if(self.type===0){self.type=1;if(typeof retry===UNDEFINED)retry=self.subscribe.route.timeout;self.subscribe.success();self.req.on("close",self.close.bind(self));res.success=true;var headers={Pragma:"no-cache"};headers[RESPONSE_HEADER_CACHECONTROL]="no-cache, no-store, max-age=0, must-revalidate";headers[RESPONSE_HEADER_CONTENTTYPE]="text/event-stream";res.writeHead(self.status,headers)}if(typeof data===OBJECT)data=JSON.stringify(data);else data=data.replace(/\n/g,"\\n").replace(/\r/g,"\\r");var newline="\n";var builder="";if(eventname&&eventname.length>0)builder="event: "+eventname+newline;builder+="data: "+data+newline;if(id&&id.toString().length>0)builder+="id: "+id+newline;if(retry&&retry>0)builder+="retry: "+retry+newline;builder+=newline;res.write(builder);self.framework.stats.response.sse++;return self};Controller.prototype.mmr=function(filename,stream,cb){var self=this;var res=self.res;if(!self.isConnected)return self;if(self.type===0&&res.success)throw new Error("Response was sent.");if(self.type>0&&self.type!==2)throw new Error("Response was used.");if(self.type===0){self.type=2;self.boundary="----totaljs"+utils.GUID(10);self.subscribe.success();res.success=true;self.req.on("close",self.close.bind(self));var headers={Pragma:"no-cache"};headers[RESPONSE_HEADER_CONTENTTYPE]="multipart/x-mixed-replace; boundary="+self.boundary;headers[RESPONSE_HEADER_CACHECONTROL]="no-cache, no-store, max-age=0, must-revalidate";res.writeHead(self.status,headers)}var type=typeof stream;if(type===FUNCTION){cb=stream;stream=null}res.write("--"+self.boundary+"\r\n"+RESPONSE_HEADER_CONTENTTYPE+": "+utils.getContentType(path.extname(filename))+"\r\n\r\n");if(typeof stream!==UNDEFINED&&stream!==null){stream.on("end",function(){self=null;if(cb)cb()});stream.pipe(res,{end:false});self.framework.stats.response.mmr++;return self}stream=fs.createReadStream(filename);stream.on("end",function(){self=null;if(cb)cb()});stream.pipe(res,{end:false});self.framework.stats.response.mmr++;return self};Controller.prototype.close=function(end){var self=this;if(typeof end===UNDEFINED)end=true;if(!self.isConnected)return self;if(self.type===0){self.isConnected=false;if(!self.res.success){self.res.success=true;if(end)self.res.end();self.framework._request_stats(false,false);self.framework.emit("request-end",self.req,self.res)}return self}if(self.type===2)self.res.write("\r\n\r\n--"+self.boundary+"--");self.isConnected=false;self.res.success=true;if(end)self.res.end();self.framework._request_stats(false,false);self.framework.emit("request-end",self.req,self.res);self.type=0;return self};Controller.prototype.proxy=function(url,obj,fnCallback,timeout){var self=this;var headers={"X-Proxy":"total.js"};headers[RESPONSE_HEADER_CONTENTTYPE]="application/json";var tmp;if(typeof fnCallback===NUMBER){tmp=timeout;timeout=fnCallback;fnCallback=tmp}if(typeof obj===FUNCTION){tmp=fnCallback;fnCallback=obj;obj=tmp}utils.request(url,["post","json"],obj,function(error,data,code,headers){if(!fnCallback)return;if((headers["content-type"]||"").indexOf("application/json")!==-1)data=JSON.parse(data);fnCallback.call(self,error,data,code,headers)},null,headers,"utf8",timeout||1e4);return self};Controller.prototype.database=function(){var self=this.framework;return self.database.apply(self,arguments)};Controller.prototype.view=function(name,model,headers,isPartial){var self=this;if(typeof isPartial===UNDEFINED&&typeof headers===BOOLEAN){isPartial=headers;headers=null}if(self.res.success&&!isPartial)return self;var skip=name[0]==="~";var filename=name;var isLayout=self.isLayout;self.isLayout=false;if(!self.isLayout&&!skip)filename=self._currentView+name;if(skip)filename=name.substring(1);var generator=internal.generateView(self,name,filename);if(generator===null){if(isPartial)return self.outputPartial;var err='View "'+filename+'" not found.';if(isLayout){self.subscribe.success();self.framework.response500(self.req,self.res,err);return}self.view500(err);return}var value="";self.$model=model;var sitemap=function(){return self.sitemap.apply(self,arguments)};if(isLayout){self._currentCSS=self._defaultCSS||"";self._currentJS=self._defaultJS||"";self._currentDownload=self._defaultDownload||"";self._currentVideo=self._defaultVideo||"";self._currentImage=self._defaultImage||"";self._currentView=self._defaultView||"";self._currentTemplate=self._defaultTemplate||"";self._currentContent=self._defaultContent||""}var helpers=self.framework.helpers;try{value=generator.call(self,self,self.repository,model,self.session,self.get,self.post,self.url,self.framework.global,helpers,self.user,self.config,self.framework.functions,0,sitemap,isPartial?self.outputPartial:self.output)}catch(ex){var err=new Error("View: "+name+" - "+ex.toString());if(!isPartial){self.view500(err);return}self.error(err);if(self.isPartial){value=self.outputPartial;self.outputPartial=""}else{value=self.output;self.output=""}isLayout=false;return value}if(!isLayout&&self.precache&&self.status===200)self.precache(value,CONTENTTYPE_TEXTHTML,headers,true);if(isLayout||utils.isNullOrEmpty(self.layoutName)){self.outputPartial="";self.output="";isLayout=false;if(isPartial)return value;self.subscribe.success();if(!self.isConnected)return;self.framework.responseContent(self.req,self.res,self.status,value,CONTENTTYPE_TEXTHTML,self.config["allow-gzip"],headers);self.framework.stats.response.view++;return self}if(isPartial)self.outputPartial=value;else self.output=value;self.isLayout=true;value=self.view(self.layoutName,self.$model,headers,isPartial);if(isPartial){self.outputPartial="";self.isLayout=false;return value}return self};Controller.prototype.memorize=function(key,expire,disabled,fnTo,fnFrom){var self=this;var output=self.cache.read(key);if(output===null){if(disabled===true){fnTo();return self}self.precache=function(value,contentType,headers,isView){var options={content:value,type:contentType};if(headers)options.headers=headers;if(isView){var keys=Object.keys(self.repository);var length=keys.length;options.repository=[];for(var i=0;i0){if(allow.indexOf("*")===-1){for(var i=0;i0){for(var i=0;iself.length){self.errors++;self.container.emit("error",new Error("Maximum request length exceeded."),self);return}switch(data[0]&15){case 1:if(self.type!==1)self.parse(data);break;case 2:if(self.type===1)self.parse(data);break;case 8:self.close();break;case 9:self.socket.write(self._state("pong"));break;case 10:break}};WebSocketClient.prototype.parse=function(data){var self=this;if(data!=null)self.buffer=Buffer.concat([self.buffer,data]);var bLength=self.buffer[1];if((bLength&128)>>7!==1)return self;var length=utils.getMessageLength(self.buffer,self.container.framework.isLE);var index=self.buffer[1]&127;index=index==126?4:index==127?10:2;if(index+length+4>self.buffer.length)return self;var mask=new Buffer(4);self.buffer.copy(mask,0,index,index+4);if(self.type!==1){var output="";for(var i=0;i=2)self.parse(null);return self};WebSocketClient.prototype._onerror=function(error){var self=this;if(error.stack.indexOf("ECONNRESET")!==-1||error.stack.indexOf("socket is closed")!==-1||error.stack.indexOf("EPIPE")!==-1)return;self.container.emit("error",error,self)};WebSocketClient.prototype._onclose=function(){var self=this;if(self._isClosed)return;self._isClosed=true;self.container._remove(self._id);self.container._refresh();self.container.emit("close",self);self.container.framework.emit("websocket-end",self.container,self)};WebSocketClient.prototype.send=function(message){var self=this;if(self.isClosed)return;if(self.type!==1){var data=self.type===3?JSON.stringify(message):(message||"").toString();if(self.container.config["default-websocket-encodedecode"]===true&&data.length>0)data=encodeURIComponent(data);self.socket.write(utils.getWebSocketFrame(0,data,1))}else{if(message!==null)self.socket.write(utils.getWebSocketFrame(0,message,2))}return self};WebSocketClient.prototype.close=function(message,code){var self=this;if(self.isClosed)return self;self.isClosed=true;self.socket.end(utils.getWebSocketFrame(code||1e3,message||"",8));return self};WebSocketClient.prototype._state=function(type){var value=new Buffer(6);switch(type){case"close":value[0]=8;value[0]|=128;value[1]=128;break;case"ping":value[0]=9;value[0]|=128;value[1]=128;break;case"pong":value[0]=10;value[0]|=128;value[1]=128;break}var iMask=Math.floor(Math.random()*255);value[2]=iMask>>8;value[3]=iMask;iMask=Math.floor(Math.random()*255);value[4]=iMask>>8;value[5]=iMask;return value};WebSocketClient.prototype._request_accept_key=function(req){var sha1=crypto.createHash("sha1");sha1.update((req.headers["sec-websocket-key"]||"")+SOCKET_HASH);return sha1.digest("base64")};http.ServerResponse.prototype.cookie=function(name,value,expires,options){var builder=[name+"="+encodeURIComponent(value)];if(expires&&!utils.isDate(expires)&&typeof expires==="object"){options=expires;expires=options.expires||options.expire||null}if(!options)options={};options.path=options.path||"/";if(expires)builder.push("Expires="+expires.toUTCString());if(options.domain)builder.push("Domain="+options.domain);if(options.path)builder.push("Path="+options.path);if(options.secure)builder.push("Secure");if(options.httpOnly||options.httponly||options.HttpOnly)builder.push("HttpOnly");var self=this;var arr=self.getHeader("set-cookie")||[];arr.push(builder.join("; "));self.setHeader("Set-Cookie",arr);return self};http.ServerResponse.prototype.noCache=function(){var self=this;self.removeHeader("Etag");self.removeHeader("Last-Modified");return self};var _tmp=http.IncomingMessage.prototype;http.IncomingMessage.prototype={get ip(){var self=this;var proxy=self.headers["x-forwarded-for"];if(typeof proxy!==UNDEFINED)return proxy.split(",",1)[0]||self.connection.removiewddress;return self.connection.remoteAddress},get subdomain(){var self=this;if(self._subdomain)return self._subdomain;var subdomain=self.uri.host.toLowerCase().replace(/^www\./i,"").split(".");if(subdomain.length>2)self._subdomain=subdomain.slice(0,subdomain.length-2);else self._subdomain=null;return self._subdomain},get host(){return this.headers["host"]},get isSecure(){return this.uri.protocol==="https"||this.uri.protocol==="wss"},get language(){return((this.headers["accept-language"].split(";")[0]||"").split(",")[0]||"").toLowerCase() -}};http.IncomingMessage.prototype.__proto__=_tmp;http.IncomingMessage.prototype.signature=function(){var self=this;return framework.encrypt((self.headers["user-agent"]||"")+"#"+self.ip+"#"+self.url,"request-signature",false)};http.IncomingMessage.prototype.noCache=function(){var self=this;delete self.headers["if-none-match"];delete self.headers["if-modified-since"];return self};http.IncomingMessage.prototype.cookie=function(name){var self=this;if(typeof self.cookies!==UNDEFINED)return decodeURIComponent(self.cookies[name]||"");self.cookies={};var cookie=self.headers["cookie"]||"";if(cookie.length===0)return"";var arr=cookie.split(";");var length=arr.length;for(var i=0;i=maximumSize){req.buffer_exceeded=true;if(rm===null)rm=[tmp.fileNameTmp];else rm.push(tmp.fileNameTmp);return}if(!tmp.isFile){tmp.value+=data.toString(ENCODING);return}if(tmp.fileSize===0){var wh=null;switch(tmp.contentType){case"image/jpeg":wh=require("./image").measureJPG(data);break;case"image/gif":wh=require("./image").measureGIF(data);break;case"image/png":wh=require("./image").measurePNG(data);break}if(wh){tmp.width=wh.width;tmp.height=wh.height}}stream.write(data);tmp.fileSize+=length};parser.onPartEnd=function(){if(stream!==null){stream.end();stream=null}if(req.buffer_exceeded)return;if(tmp.isFile){req.data.files.push(new HttpFile(tmp.name,tmp.fileName,tmp.fileNameTmp,tmp.fileSize,tmp.contentType,tmp.width,tmp.height));return}if(onXSS(tmp.value))isXSS=true;var temporary=req.data.post[tmp.name];if(typeof temporary===UNDEFINED){req.data.post[tmp.name]=tmp.value;return}if(utils.isArray(temporary)){req.data.post[tmp.name].push(tmp.value);return}temporary=[temporary];temporary.push(tmp.value);req.data.post[tmp.name]=temporary};parser.onEnd=function(){var cb=function(){if(close>0){setImmediate(cb);return}if(isXSS){req.flags.push("xss");framework.stats.request.xss++}if(rm!==null)framework.unlink(rm);callback()};cb()};req.on("data",parser.write.bind(parser));req.on("end",parser.end.bind(parser))};exports.parseMULTIPART_MIXED=function(req,contentType,tmpDirectory,onFile,callback){var parser=new MultipartParser;var boundary=contentType.split(";")[1];var stream=null;var tmp={name:"",contentType:"",fileName:"",fileNameTmp:"",fileSize:0,isFile:false,step:0,width:0,height:0};var ip=req.ip.replace(/\./g,"");var close=0;boundary=boundary.substring(boundary.indexOf("=")+1);req.buffer_exceeded=false;req.buffer_has=true;parser.initWithBoundary(boundary);parser.onPartBegin=function(){tmp.fileSize=0;tmp.step=0;tmp.isFile=false};parser.onHeaderValue=function(buffer,start,end){if(req.buffer_exceeded||tmp.step>1)return;var arr=buffer.slice(start,end).toString(ENCODING).split(";");if(tmp.step===1){tmp.contentType=arr[0];tmp.step=2;return}if(tmp.step===0){tmp.name=arr[1].substring(arr[1].indexOf("=")+2);tmp.name=tmp.name.substring(0,tmp.name.length-1);tmp.step=1;if(arr.length!==3)return;tmp.fileName=arr[2].substring(arr[2].indexOf("=")+2);tmp.fileName=tmp.fileName.substring(0,tmp.fileName.length-1);tmp.isFile=true;tmp.fileNameTmp=utils.combine(tmpDirectory,ip+"-"+(new Date).getTime()+"-"+utils.random(1e5)+".upload");stream=fs.createWriteStream(tmp.fileNameTmp,{flags:"w"});stream.on("close",function(){close--});stream.on("error",function(){close--});close++;return}};parser.onPartData=function(buffer,start,end){var data=buffer.slice(start,end);var length=data.length;if(!tmp.isFile)return;if(tmp.fileSize===0){var wh=null;switch(tmp.contentType){case"image/jpeg":wh=require("./image").measureJPG(data);break;case"image/gif":wh=require("./image").measureGIF(data);break;case"image/png":wh=require("./image").measurePNG(data);break}if(wh){tmp.width=wh.width;tmp.height=wh.height}}stream.write(data);tmp.fileSize+=length};parser.onPartEnd=function(){if(stream!==null){stream.end();stream=null}if(!tmp.isFile)return;onFile(new HttpFile(tmp.name,tmp.fileName,tmp.fileNameTmp,tmp.fileSize,tmp.contentType,tmp.width,tmp.height))};parser.onEnd=function(){var cb=function cb(){if(close>0){setImmediate(cb);return}onFile(null);callback()};cb()};req.on("data",parser.write.bind(parser))};exports.routeSplit=function(url,noLower){if(!noLower)url=url.toLowerCase();if(url[0]==="/")url=url.substring(1);if(url[url.length-1]==="/")url=url.substring(0,url.length-1);var arr=url.split("/");if(arr.length===1&&arr[0]==="")arr[0]="/";return arr};exports.routeCompare=function(url,route,isSystem,isAsterix){var length=url.length;if(route.length!==length&&!isAsterix)return false;var skip=length===1&&url[0]==="/";for(var i=0;i-1};exports.routeCompareFlags=function(arr1,arr2,noLoggedUnlogged){var isXSS=false;var length=arr2.length;var AUTHORIZE="authorize";var UNAUTHORIZE="unauthorize";for(var i=0;i0){skipA--;continue}skipA++}if(tmp[i]==="{")skipC++;if(tmp[i]==="'"){if(skipB>0){skipB--;continue}skipB++}if(tmp[i]==="}"&&skipC>0){skipC--;continue}if(skipA>0||skipB>0||skipC>0)continue;if(i===length-1){end=i+1;break}if(tmp[i]===";"||tmp[i]==="}"||tmp[i]==="\n"){end=i;break}}if(end===0)continue;var cmd=tmp.substring(0,end);tmp=tmp.substring(end);output+="+"+cmd.substring(1);beg=0}var length=output.length;var compiled="";output=code+"\n\n;compiled = ''"+output;eval(output);if(isAuto)compiled=autoprefixer(compiled);return prepare(compiled)}function autoprefixer(value){var prefix=["appearance","column-count","column-gap","column-rule","display","transform","transform-origin","transition","user-select","animation","animation-name","animation-duration","animation-timing-function","animation-delay","animation-iteration-count","animation-direction","animation-play-state","opacity","background","background-image","font-smoothing"];value=autoprefixer_keyframes(value);var builder=[];var index=0;var property;for(var i=0;i1){counter--;continue}end=indexer;break}if(end===-1)continue;var css=value.substring(index,end+1);builder.push({name:"keyframes",property:css})}var output=[];var length=builder.length;for(var i=0;i=32||c===13||c===EOF){return c}if(c===10){return 13}return 32}function put(c){if(c===13||c===10)sb.push(" ");else sb.push(String.fromCharCode(c))}function isAlphanum(c){return c>=97&&c<=122||c>=48&&c<=57||c>=65&&c<=90||c===95||c===36||c===92||c>126}jsmin();return sb.join("")}exports.compile_javascript=function(source,framework){try{if(framework){if(framework.onCompileJS!==null)return framework.onCompileJS("",source)}return JavaScript(source)}catch(ex){if(framework)framework.error(ex,"JavaScript compressor");return source}};var Buffer=require("buffer").Buffer,s=0,S={PARSER_UNINITIALIZED:s++,START:s++,START_BOUNDARY:s++,HEADER_FIELD_START:s++,HEADER_FIELD:s++,HEADER_VALUE_START:s++,HEADER_VALUE:s++,HEADER_VALUE_ALMOST_DONE:s++,HEADERS_ALMOST_DONE:s++,PART_DATA_START:s++,PART_DATA:s++,PART_END:s++,END:s++},f=1,F={PART_BOUNDARY:f,LAST_BOUNDARY:f*=2},LF=10,CR=13,SPACE=32,HYPHEN=45,COLON=58,A=97,Z=122,lower=function(c){return c|32};for(s in S){exports[s]=S[s]}function MultipartParser(){this.boundary=null;this.boundaryChars=null;this.lookbehind=null;this.state=S.PARSER_UNINITIALIZED;this.index=null;this.flags=0}exports.MultipartParser=MultipartParser;MultipartParser.stateToString=function(stateNumber){for(var state in S){var number=S[state];if(number===stateNumber)return state}};MultipartParser.prototype.initWithBoundary=function(str){var self=this;self.boundary=new Buffer(str.length+4);if(framework.versionNode>=1111){self.boundary.write("\r\n--",0,"ascii");self.boundary.write(str,4,"ascii")}else{self.boundary.write("\r\n--","ascii",0);self.boundary.write(str,"ascii",4)}self.lookbehind=new Buffer(self.boundary.length+8);self.state=S.START;self.boundaryChars={};for(var i=0;iZ){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);callback("headerEnd");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===0){i+=boundaryEnd;while(i0){lookbehind[index-1]=c}else if(prevIndex>0){callback("partData",lookbehind,0,prevIndex);prevIndex=0;mark("partData");i--}break;case S.END:break;default:return i}}dataCallback("headerField");dataCallback("headerValue");dataCallback("partData");self.index=index;self.state=state;self.flags=flags;return len};MultipartParser.prototype.end=function(){var self=this;var callback=function(self,name){var callbackSymbol="on"+name.substr(0,1).toUpperCase()+name.substr(1);if(callbackSymbol in self){self[callbackSymbol]()}};if(self.state==S.HEADER_FIELD_START&&self.index===0||self.state==S.PART_DATA&&self.index==self.boundary.length){callback(self,"partEnd");callback(self,"end")}else if(self.state!=S.END){callback(self,"partEnd");callback(self,"end");return new Error("MultipartParser.end(): stream ended unexpectedly: "+self.explain())}};MultipartParser.prototype.explain=function(){return"state = "+MultipartParser.stateToString(this.state)};function View(controller){this.controller=controller;this.cache=controller.cache;this.prefix=controller.prefix}function Content(controller){this.controller=controller;this.cache=controller.cache;this.prefix=controller.prefix}function view_parse(content,minify){content=removeComments(compressCSS(compressJS(content,0,framework),0,framework));var DELIMITER="'";var DELIMITER_UNESCAPE="unescape('";var DELIMITER_UNESCAPE_END="')";var SPACE=" ";var builder="var $EMPTY='';var $length=0;var $source=null;var $tmp=index;var $output=$EMPTY";var command=view_find_command(content,0);var compressed="";function escaper(value){value=compressHTML(value,minify);if(value==="")return"$EMPTY";if(value.match(/\n|\t|\r|\'|\\/)!==null)return DELIMITER_UNESCAPE+escape(value)+DELIMITER_UNESCAPE_END;return DELIMITER+value+DELIMITER}if(command===null)builder+="+"+escaper(content);var old=null;var newCommand="";var tmp="";var index=0;var counter=0;var functions=[];var functionsName=[];var isFN=false;var isSECTION=false;var builderTMP="";var sectionName="";while(command!==null){if(old!==null){var text=content.substring(old.end+1,command.beg);if(text!==""){if(view_parse_plus(builder))builder+="+";builder+=escaper(text)}}else{var text=content.substring(0,command.beg);if(text!==""){if(view_parse_plus(builder))builder+="+";builder+=escaper(text)}}var cmd=content.substring(command.beg+2,command.end);var cmd8=cmd.substring(0,8);if(cmd8==="section "&&cmd.lastIndexOf(")")===-1){builderTMP=builder;builder="+(function(){var $output=$EMPTY";sectionName=cmd.substring(8);isSECTION=true;isFN=true}else if(cmd.substring(0,7)==="helper "){builderTMP=builder;builder="function "+cmd.substring(7).trim()+"{var $output=$EMPTY";isFN=true;functionsName.push(cmd.substring(7,cmd.indexOf("(",7)).trim())}else if(cmd8==="foreach "){counter++;if(cmd.indexOf("foreach var ")!==-1)cmd=cmd.replace(" var ",SPACE);newCommand=(cmd.substring(8,cmd.indexOf(SPACE,8))||"").trim();index=cmd.indexOf("[");if(index===-1)index=cmd.lastIndexOf(SPACE);builder+="+(function(){var $source="+cmd.substring(index).trim()+";if (!($source instanceof Array) || source.length === 0)return $EMPTY;var $length=$source.length;var $output=$EMPTY;var index=0;for(var i=0;i<$length;i++){index = i;var "+newCommand+"=$source[i];$output+=$EMPTY"}else if(cmd==="end"){if(isFN&&counter<=0){counter=0;if(isSECTION){builder=builderTMP+builder+";repository['$section_"+sectionName+"']=$output;return $EMPTY})()";builderTMP=""}else{builder+=";return $output;}";functions.push(builder);builder=builderTMP;builderTMP=""}isSECTION=false;isFN=false}else{counter--;builder+="}return $output;})()";newCommand=""}}else if(cmd.substring(0,3)==="if "){builder+=";if ("+cmd.substring(3)+"){$output+=$EMPTY"}else if(cmd==="else"){builder+="} else {$output+=$EMPTY"}else if(cmd==="endif"){builder+="}$output+=$EMPTY"}else{tmp=view_prepare(command.command,newCommand,functionsName);if(tmp.length>0){if(view_parse_plus(builder))builder+="+";builder+=tmp}}old=command;command=view_find_command(content,command.end)}if(old!==null){var text=content.substring(old.end+1);if(text.length>0)builder+="+"+escaper(text)}var fn="(function(self,repository,model,session,get,post,url,global,helpers,user,config,functions,index,sitemap,output){"+(functions.length>0?functions.join("")+";":"")+"var controller=self;"+builder+";return $output;})";return eval(fn)}function view_parse_plus(builder){var c=builder[builder.length-1];if(c!=="!"&&c!=="?"&&c!=="+"&&c!=="."&&c!==":")return true;return false}function view_prepare(command,dynamicCommand,functions){var a=command.indexOf(".");var b=command.indexOf("(");var c=command.indexOf("[");var max=[];var tmp=0;if(a!==-1)max.push(a);if(b!==-1)max.push(b);if(c!==-1)max.push(c);var index=Math.min.apply(this,max);if(index===-1)index=command.length;var name=command.substring(0,index);if(name===dynamicCommand)return"("+command+").toString().encode()";if(name[0]==="!"&&name.substring(1)===dynamicCommand)return"("+command.substring(1)+").toString()";switch(name){case"foreach":case"end":return"";case"section":tmp=command.indexOf("(");if(tmp===-1)return"";return"(repository['$section_"+command.substring(tmp+1,command.length-1).replace(/\'/g,"")+"'] || '')";case"controller":case"repository":case"model":case"get":case"post":case"global":case"session":case"user":case"config":case"model":if(view_is_assign(command))return"self.$set("+command+")";return"("+command+").toString().encode()";case"functions":return"("+command+").toString().encode()";case"!controller":case"!repository":case"!get":case"!post":case"!global":case"!session":case"!user":case"!config":case"!functions":case"!model":return"("+command.substring(1)+")";case"body":return"output";case"resource":return"(self."+command+").toString().encode()";case"!resource":return"(self."+command.substring(1)+")";case"host":case"hostname":if(command.indexOf("(")===-1)return"self.host()";return"self."+command;case"url":if(command.indexOf("(")!==-1)return"self.$"+command;return command="url";case"title":case"description":case"keywords":if(command.indexOf("(")!==-1)return"self."+command;return"(repository['$"+command+"'] || '').toString().encode()";case"!title":case"!description":case"!keywords":return"(repository['$"+command.substring(1)+"'] || '')";case"head":if(command.indexOf("(")!==-1)return"self.$"+command;return"self."+command+"()";case"place":case"sitemap":if(command.indexOf("(")!==-1)return"self.$"+command;return"(repository['$"+command+"'] || '')";case"meta":if(command.indexOf("(")!==-1)return"self.$"+command;return"self.$meta()";case"js":case"script":case"css":case"favicon":return"self.$"+command+(command.indexOf("(")===-1?"()":"");case"index":return"("+command+")";case"routeJS":case"routeCSS":case"routeImage":case"routeFont":case"routeDownload":case"routeVideo":case"routeStatic":return"self."+command;case"ng":case"ngTemplate":case"ngController":case"ngCommon":case"ngInclude":case"ngLocale":case"ngService":case"ngFilter":case"ngDirective":case"ngResource":case"ngStyle":return"self.$"+command;case"canonical":case"checked":case"helper":case"component":case"componentToggle":case"content":case"contentToggle":case"currentContent":case"currentCSS":case"currentDownload":case"currentImage":case"currentJS":case"currentTemplate":case"currentVideo":case"currentView":case"disabled":case"dns":case"download":case"etag":case"header":case"image":case"json":case"layout":case"modified":case"next":case"options":case"prefetch":case"prerender":case"prev":case"readonly":case"selected":case"template":case"templateToggle":case"view":case"viewToggle":return"self.$"+command;case"radio":case"text":case"checkbox":case"hidden":case"textarea":case"password":return"self.$"+exports.appendModel(command);default:if(framework.helpers[name])return"helpers."+view_insert_call(command);return functions.indexOf(name)===-1?command[0]==="!"?command.substring(1)+".toString()":command+".toString().encode()":command+".toString()"}return command}function view_insert_call(command){var beg=command.indexOf("(");if(beg===-1)return command;var length=command.length;var count=0;for(var i=beg+1;i0){count--;continue}return command.substring(0,beg)+".call(self, "+command.substring(beg+1)}return command}function view_is_assign(value){var length=value.length;var skip=0;var plus=0;for(var i=0;i0){count--;continue}}return{beg:index,end:i,command:content.substring(index+2,i).trim()}}return null}function removeCondition(text,beg){if(beg){if(text[0]==="+")return text.substring(1,text.length)}else{if(text[text.length-1]==="+")return text.substring(0,text.length-1)}return text}function removeComments(html){var tagBeg="";var beg=html.indexOf(tagBeg);var end=0;while(beg!==-1){end=html.indexOf(tagEnd,beg+4);if(end===-1)break;var comment=html.substring(beg,end+3);if(comment.indexOf("[if")!==-1||comment.indexOf("[endif")!==-1){beg=html.indexOf(tagBeg,end+3);continue}html=html.replacer(comment,"");beg=html.indexOf(tagBeg,end+3)}return html}function compressJS(html,index,framework){var strFrom='";var indexBeg=html.indexOf(strFrom,index||0);if(indexBeg===-1){strFrom=" diff --git a/test/components/contactform.js b/test/components/contactform.js new file mode 100644 index 000000000..fee308eeb --- /dev/null +++ b/test/components/contactform.js @@ -0,0 +1,6 @@ +exports.group = 'serverside'; +exports.install = function() { + F.route('/components/contactform/', function() { + this.plain('CONTACTFORM COMPONENTS'); + }); +}; \ No newline at end of file diff --git a/test/components/newsletter.html b/test/components/newsletter.html new file mode 100644 index 000000000..e8a0f8c9f --- /dev/null +++ b/test/components/newsletter.html @@ -0,0 +1,12 @@ + + + + +SOME CONTENT \ No newline at end of file diff --git a/test/components/products.js b/test/components/products.js deleted file mode 100755 index 15b767170..000000000 --- a/test/components/products.js +++ /dev/null @@ -1,20 +0,0 @@ -// optional -exports.install = function(framework) { - // component doesn't support routing -}; - -// optional -exports.usage = function(isDetailed) { - return ''; -}; - -// optional -exports.configure = function(setup) { - -}; - -// REQUIRED -exports.render = function(data) { - // this === controller or this === framework - return 'COMPONENT'; -}; \ No newline at end of file diff --git a/test/config-debug b/test/config-debug index 2be0a5851..ae9af02e2 100755 --- a/test/config-debug +++ b/test/config-debug @@ -1,2 +1,8 @@ etag-version : 1 -secret : total.js test \ No newline at end of file +secret : total.js test +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 2be0a5851..ae9af02e2 100755 --- a/test/config-release +++ b/test/config-release @@ -1,2 +1,8 @@ etag-version : 1 -secret : total.js test \ No newline at end of file +secret : total.js test +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/configs/my-config b/test/configs/my-config new file mode 100644 index 000000000..5ccbf3de1 --- /dev/null +++ b/test/configs/my-config @@ -0,0 +1 @@ +custom-config2 : 2YES \ No newline at end of file diff --git a/test/contents/b.html b/test/contents/b.html deleted file mode 100755 index f6d5afa37..000000000 --- a/test/contents/b.html +++ /dev/null @@ -1 +0,0 @@ -BBB \ No newline at end of file diff --git a/test/contents/test.html b/test/contents/test.html deleted file mode 100755 index 1ec6b8a65..000000000 --- a/test/contents/test.html +++ /dev/null @@ -1 +0,0 @@ -EMPTY \ No newline at end of file diff --git a/test/controllers/default.js b/test/controllers/default.js index b042f1d0a..c10694b94 100755 --- a/test/controllers/default.js +++ b/test/controllers/default.js @@ -1,536 +1,729 @@ var assert = require('assert'); -exports.install = function(framework) { - framework.route('/logged/', view_logged, { - flags: ['authorize'], - timeout: 1000, - length: 3000 - }); - framework.route('/unauthorize/', view_unauthorize, { - flags: ['unauthorize'] - }); - framework.route('/homepage/', view_homepage); - framework.route('/usage/', view_usage); - framework.route('/sse/', viewSSE_html); - framework.route('/pipe/', pipe); - framework.route('/app/*', asterix); - framework.route('/sse/', viewSSE, ['sse']); - framework.route('/http/', viewHTTP, ['http']); - framework.route('/https/', viewHTTPS, ['https']); - framework.route('/dynamic/', viewDynamic); - framework.route('/routeto/', viewRouteto); - framework.route('/f/', viewSocket); - framework.route('/js/', viewJS); - framework.route('/', viewIndex); - framework.route('/cookie/', view_cookie); - framework.route('/layout/', view_layout); - framework.route('/custom/', viewCustomTesting); - framework.route('/views/', viewViews, [], ['partial']); - framework.route('/view-notfound/', viewError); - framework.route('/views-if/', viewViewsIf); - framework.route('/{a}/', viewRouteA); - framework.route('/{a}/{b}/', viewRouteAB); - framework.route('/a/{a}/', viewRouteAA); - framework.route('/a/b/c/', viewRouteABC); - framework.route('/test/', viewTest); - framework.route('/test-view/', view_test_view); - framework.route('/login/google/callback/', aa); - framework.route('/timeout/', function() {}, [], null, [], 50); - - /* - framework.file('Resizing of images', function(req, res) { - return req.url.indexOf('.jpg') !== -1; - }, resize_image); +exports.install = function() { + + //F.localize('/templates/'); + + F.route(function DEFER(url, req, flags) { + return url === '/custom/route/'; + }, function DEFER() { + this.plain('CUSTOM'); + }); + + F.route('/logged/', view_logged, ['authorize', 1000], 3000); + F.route('/unauthorize/', ['unauthorize'], view_unauthorize); + + F.route('/a/b/c/d/authorize/', ['authorize'], function() { + this.plain('authorize'); + }); + +/* + F.route('/', function() { + this.plain('OK'); + }, ['unauthorize']); */ + F.route('/', function DEFER() { + 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']); + F.route('/exception/', 'exception'); + F.route('/html-compressor/', view_compressor); + F.route('/html-nocompress/', view_nocompress); + F.route('/sync/', synchronize); + F.route('/schema-filter/', ['post', '*filter#update']); + F.route('/package/', '@testpackage/test'); + F.route('/homepage/', view_homepage); + F.route('/usage/', view_usage); + F.route('/sse/', viewSSE_html); + F.route('/pipe/', pipe); + F.route('/binary/', binary); + F.route('/mobile/', mobile, ['mobile']); + F.route('/mobile/', mobile_none); + F.route('/reg/exp/{/^\\d+$/}/', regexp); + F.route('/app/*', asterix); + F.route('/sse/', viewSSE, ['sse']); + F.route('/http/', viewHTTP, ['http']); + F.route('/https/', viewHTTPS, ['https']); + F.route('/dynamic/', viewDynamic); + F.route('/routeto/', viewRouteto); + F.route('/f/', viewSocket); + F.route('/js/', viewJS); + F.route('/', viewIndex); + F.route('/cookie/', view_cookie); + F.route('/layout/', view_layout); + F.route('/custom/', viewCustomTesting); + F.route('/views/', viewViews, ['#middleware']); + F.route('/view-notfound/', viewError); + F.route('/views-if/', viewViewsIf); + F.route('/{a}/', viewRouteA); + F.route('/{a}/{b}/', viewRouteAB); + F.route('/a/{a}/', viewRouteAA); + F.route('/a/b/c/', viewRouteABC); + F.route('/test/', viewTest); + F.route('/translate/', viewTranslate); + F.route('/test-view/', view_test_view); + F.route('/login/google/callback/', aa); + F.route('/timeout/', function DEFER() {}, [50]); + + F.route('/get/', plain_get); + F.route('/post/raw/', plain_post_raw, ['post', 'raw']); + F.route('/post/parse/', plain_post_parse, ['post']); + 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 /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']); + F.route('/rest/', plain_rest, ['delete']); + F.route('/put/raw/', plain_put_raw, ['put', 'raw']); + F.route('/put/parse/', plain_put_parse, ['put']); + F.route('/put/json/', plain_put_json, ['json', 'put']); + F.route('/put/xml/', plain_put_xml, ['xml', 'put']); + F.route('/upload/', plain_upload, ['upload']); + F.route('/index/', 'homepage'); + 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() { + var self = this; + F.global.timeout++; + self.plain('408'); + }); + + 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) === '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'); + + F.route('/basic/', viewBAA); + + F.file(file_plain_middleware, ['#file']); + F.file('/robots.txt', file_plain); + F.file(file_plain_status); + + F.route('#401', function() { + this.plain('401'); + }); + + F.file((req) => req.url.indexOf('.jpg') !== -1, resize_image); + + // url + // function + // flags [json, logged, unlogged] + // protocols [] + // allow [] + // maximumSize + F.websocket('/', socket); + F.route('/theme-green/', view_theme); + F.cors('/api/*'); + F.cors('/cors/origin-all/'); + F.cors('/cors/origin-not/', ['http://www.petersirka.eu', 'http://www.858project.com']); + F.cors('/cors/headers/', ['post', 'put', 'delete', 'options', 'X-Ping'], true); +}; - framework.route('/live/', viewLive); - framework.route('/live/incoming/', viewLiveIncoming, ['mixed']); +function plain_options() { + this.plain('OPTIONS'); +} - framework.redirect('http://www.google.sk', 'http://www.petersirka.sk'); +function *synchronize() { + var self = this; + var content = (yield sync(require('fs').readFile)(PATH.public('file.txt'))).toString('utf8'); + self.plain(content); +} - framework.route('#408', function() { - var self = this; - self.global.timeout++; - self.plain('408'); - }, []); +function plain_rest() { + this.plain(this.req.method); +} - assert.ok(framework.encrypt('123456', 'key', false) === 'MjM9QR8HExlaHQJQBxcGAEoaFQoGGgAW', 'framework.encrypt(string)'); - assert.ok(framework.decrypt('MjM9QR8HExlaHQJQBxcGAEoaFQoGGgAW', 'key', false) === '123456', 'framework.decrypt(string)'); +function plain_multiple() { + var self = this; + self.plain('POST-GET-PUT-DELETE'); +} + +function plain_get() { + var self = this; + self.json(self.query); +} - assert.ok(framework.encrypt({ - name: 'Peter' - }, 'key', false) === 'MzM9QVUXTkwCThBbF3RXQRlYBkUFVRdOTAJOEFsXdFdBGQ', 'framework.encrypt(object)'); - assert.ok(framework.decrypt('MzM9QVUXTkwCThBbF3RXQRlYBkUFVRdOTAJOEFsXdFdBGQ', 'key').name === 'Peter', 'framework.decrypt(object)') +function plain_post_raw() { + var self = this; + self.plain(self.body); +} - assert.ok(source('main').hello() === 'world', 'source'); - assert.ok(include('main').hello() === 'world', 'source'); +function plain_post_parse() { + var self = this; + self.layout(''); + var output = self.view('params', null, true); + 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); +} - framework.route('/basic/', viewBAA); +function plain_post_schema_parse() { + var self = this; + self.body.type = 'schema'; + self.json(self.body); +} - // url - // function - // flags [json, logged, unlogged] - // protocols [] - // allow [] - // maximumSize - framework.websocket('/', socket); -}; +function plain_post_json() { + var self = this; + self.body.type = 'json'; + self.json(self.body); +} + +function plain_post_xml() { + var self = this; + self.body.type = 'xml'; + self.json(self.body); +} + +function plain_put_raw() { + var self = this; + self.plain(self.body); +} + +function plain_put_parse() { + var self = this; + self.body.type = 'parse'; + self.json(self.body); +} + +function plain_put_json() { + var self = this; + self.body.type = 'json'; + self.json(self.body); +} + +function plain_put_xml() { + var self = this; + self.body.type = 'xml'; + self.json(self.body); +} + +function plain_upload() { + var self = this; + var file = self.files[0]; + self.json({ name: file.filename, length: file.length, type: file.type }); +} + +function file_plain(req, res, is) { + res.send(req.url); +} + +function file_plain_middleware(req, res, isValidation) { + if (isValidation) + return req.url === '/middleware.txt'; + + res.send({ url: req.url }); +} + +function file_plain_status(req, res, isValidation) { + if (isValidation) + return req.url === '/status.txt'; + + res.send(404); +} function resize_image(req, res) { - var fs = require('fs'); - // this.responseImage(req, res, fs.createReadStream(this.path.public(req.url)), function(image) { - this.responseImage(req, res, this.path.public(req.url), function(image) { - image.resize('20%'); - }); + var fs = require('fs'); + this.responseImage(req, res, fs.createReadStream(this.path.public(req.url)), function DEFER(image) { + image.resize('20%'); + }); } function viewRouteto() { - var self = this; - var result = self.transfer('/router/'); - assert.ok(result, 'controller.routeTo()'); + var self = this; + var result = self.transfer('/router/'); + assert.ok(result, 'controller.routeTo()'); } function asterix() { - this.plain('ASTERIX'); + this.plain('ASTERIX'); } function view_homepage() { - /* + /* framework.server.getConnections(function(a, b, c) { console.log(a, b, c); });*/ - //console.log(framework.server._connection); + //console.log(framework.server._connection); - console.log(this.hash('sha1', '123456', false)); + console.log(this.hash('sha1', '123456', false)); - //this.view('homepage'); - this.plain(this.framework.usage(true)); + //this.view('homepage'); + this.plain(framework.usage(true)); } function view_layout() { - this.view('test'); + this.view('test'); } function view_usage() { - this.plain(this.framework.usage(true)); + this.plain(this.framework.usage(true)); } function viewBAA() { - var user = this.baa(); + var user = this.baa(); - if (user === null) - return; + if (user === null) + return; - this.json(user); + this.json(user); } function viewSSE_html() { - this.view('g'); + this.view('g'); } function view_logged() { - var self = this; - assert.ok(self.session.ready === true, 'Session problem'); - assert.ok(self.user.alias === 'Peter Širka', 'User problem'); - self.plain('OK'); + var self = this; + assert.ok(self.session.ready === true, 'Session problem'); + assert.ok(self.user.alias === 'Peter Širka', 'User problem'); + self.plain('OK'); } function view_unauthorize() { - var self = this; - self.plain('UNAUTHORIZED'); + var self = this; + self.plain('UNAUTHORIZED'); } function viewSSE() { - var self = this; - self.sse('TEST\n\nTEST'); + var self = this; + self.sse('TEST\n\nTEST'); } function viewLiveIncoming(file) { - console.log(file); + console.log(file); } function viewSocket() { - this.view('f'); + this.view('f'); } function view_test_view() { - this.view('test'); + this.view('test'); } -function viewCustomTesting() { - this.plain(this.template('one', [{ - name: 'A', - price: 10, - B: false - }, { - name: 'B', - price: 10.5, - B: true - }])); - //this.plain(this.template('new', [{ tag: 'A' }, { tag: 'B' }])); +function viewCustomTesting() {/* + this.plain(this.template('one', [{ + name: 'A', + price: 10, + B: false + }, { + name: 'B', + price: 10.5, + B: true + }]));*/ + this.plain(this.template('new', [{ tag: 'A' }, { tag: 'B' }])); + setTimeout(function DEFER() { + framework.stop(); + }, 500); } function socket(self, framework) { - self.on('open', function(client) { - console.log('open ->', client.id); - console.log(client.get); - }); + self.on('open', function DEFER(client) { + console.log('open ->', client.id); + console.log(client.get); + }); - self.on('close', function(client) { - console.log('close ->', client.id); - }); + self.on('close', function DEFER(client) { + console.log('close ->', client.id); + }); - self.on('message', function(client, message) { - console.log('message ->', client.id, message); + self.on('message', function DEFER(client, message) { + console.log('message ->', client.id, message); - if (message === 'disconnect') - client.close(); - }); + if (message === 'disconnect') + client.close(); + }); - self.on('error', function(error, client) { - console.log('error –>', error); - }); + self.on('error', function DEFER(error, client) { + console.log('error –>', error); + }); } function aa() { - this.json(this.get); + this.json(this.get); } function viewTest() { - var name = 'views: '; - var self = this; - - self.repository.arr = ['Q', 'R', 'S']; - self.repository.title = 'TEST'; - self.repository.tag = 'A'; - - self.repository.optionsEmpty = [{ - name: 'A', - value: 'A' - }, { - name: 'B', - value: 'B' - }]; - - self.repository.options = [{ - k: 'C', - v: 'C' - }, { - k: 'D', - v: 'D' - }]; - self.repository.template = [{ - name: 'A', - price: 10, - B: false - }, { - name: 'B', - price: 10.5, - B: true - }]; - - //this.layout(''); - //this.view('e'); - var output = self.view('a', { - a: 'A', - b: 'B', - arr: ['1', '2', '3'] - }); + var name = 'views: '; + var self = this; + + self.repository.arr = ['Q', 'R', 'S']; + self.repository.title = 'TEST'; + self.repository.tag = 'A'; + + self.repository.optionsEmpty = [{ + name: 'A', + value: 'A' + }, { + name: 'B', + value: 'B' + }]; + + self.repository.options = [{ + k: 'C', + v: 'C' + }, { + k: 'D', + v: 'D' + }]; + self.repository.template = [{ + name: 'A', + price: 10, + B: false + }, { + name: 'B', + price: 10.5, + B: true + }]; + + var output = self.view('a', { + a: 'A', + b: 'B', + arr: ['1', '2', '3'] + }); } function viewDynamic() { - this.view('@{model.name}', { - name: 'Peter' - }); + this.viewcompile('@{model.name}', { name: 'Peter' }); } -function viewIndex() { - - var self = this; - var name = 'controller: '; - - assert.ok(self.path.public('file.txt') === './public/file.txt', name + 'path.public'); - assert.ok(self.path.logs('file.txt') === './logs/file.txt', name + 'path.logs'); - assert.ok(self.path.temp('file.txt') === './tmp/file.txt', name + 'path.temp'); - - self.meta('A', 'B'); - assert.ok(self.repository['$title'] === 'A' && self.repository['$description'] === 'B', name + 'meta() - write'); - - self.sitemap('A', '/'); - assert.ok(self.sitemap()[0].name === 'A', name + 'sitemap() - write'); - - assert.ok(self.module('hatatitla') === null, name + 'module(not exists) - read'); - assert.ok(self.module('test').message() === 'message', name + 'module(exists) - read'); - - self.layout('test'); - assert.ok(self.layoutName === 'test', name + 'layout()'); - - assert.ok(self.functions('share').message() === 'message', name + 'functions()'); - assert.ok(self.model('user').ok === 1, name + 'model()'); - assert.ok(framework.model('user').ok === 1, 'framework: model()'); - - assert.ok(self.isSecure === false, 'controller.isSecure'); - assert.ok(self.config.isDefinition === true, 'definitions()'); +function viewTranslate() { + this.language = this.query.language || ''; + this.view('translate'); +} - assert.ok(!self.xhr, name + 'xhr'); - assert.ok(self.flags.indexOf('get') !== -1, name + 'flags') +function viewIndex() { - 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)'); + var self = this; + var name = 'controller: '; - self.log('test'); + 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'); - assert.ok(self.hash('sha1', '123456', false) === '7c4a8d09ca3762af61e59520943dc26494f8941b', 'controller.hash()'); + self.meta('A', 'B'); + assert.ok(self.repository['$title'] === 'A' && self.repository['$description'] === 'B', name + 'meta() - write'); - self.setModified('123456'); + assert.ok(self.module('hatatitla') === null, name + 'module(not exists) - read'); + assert.ok(self.module('test').message() === 'message', name + 'module(exists) - read'); - var date = new Date(); - date.setFullYear(1984); + self.layout('test'); + assert.ok(self.layoutName === 'test', name + 'layout()'); - self.setModified(date); - self.setExpires(date); + // assert.ok(self.functions('share').message() === 'message', name + 'functions()'); + assert.ok(self.model('user').ok === 1, name + 'model()'); + assert.ok(framework.model('user').ok === 1, 'framework: model() - 1'); + assert.ok(framework.model('other/products').ok === 2, 'framework: model() - 2'); - assert.ok(self.routeJS('p.js') === '/js/p.js', name + 'routeJS()'); - assert.ok(self.routeCSS('p.css') === '/css/p.css', name + 'routeCSS()'); - 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') === '/font/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.secured === false, 'controller.secured'); + assert.ok(CONF.isDefinition === true, 'definitions()'); - self.currentTemplate('current'); + assert.ok(!self.xhr, name + 'xhr'); + assert.ok(self.flags.indexOf('get') !== -1, name + 'flags'); - assert.ok(self.template('test', ['A', 'B'], { - name: '' - }) === '
AB
', name + 'template - no repository'); - assert.ok(self.template('test', ['A', 'B'], '', { - name: 'ABCDEFG' - }) === '
AB
...', name + 'template - repository'); - assert.ok(self.template('test', [], 'test') === 'EMPTY', name + 'template - empty'); + 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)'); - var tmp = self.template('~new', [{ - tag: 'A' - }, { - tag: 'B' - }]); - assert.ok(tmp.indexOf('
<b>B</b>
B
1') !== -1, name + 'template - foreach'); + self.log('test'); + var date = new Date(); + date.setFullYear(1984); - self.layout(''); - assert.ok(self.view('test', null, true) === 'total.js', name + 'view'); - assert.ok(self.content('test', true) === 'EMPTY', name + 'content'); - assert.ok(self.url === '/', name + 'url'); + 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()'); - var error = self.validate({ - A: 'B' - }, ['A']); - assert.ok(error.hasError() && error.read('A') === 'AB', 'framework.onValidation & controller.validation'); + self.layout(''); + assert.ok(self.view('test', null, true) === 'Total.js', name + 'view'); + assert.ok(self.url === '/', name + 'url'); - self.status = 404; - self.plain('OK'); + self.status = 404; + self.plain('OK'); } function viewViews() { - var name = 'views: '; - var self = this; - - self.repository.arr = ['Q', 'R', 'S']; - self.repository.title = 'TEST'; - self.repository.tag = 'A'; - - self.repository.optionsEmpty = [{ - name: 'A', - value: 'A' - }, { - name: 'B', - value: 'B' - }]; - - self.repository.options = [{ - k: 'C', - v: 'C' - }, { - k: 'D', - v: 'D' - }]; - self.repository.template = [{ - name: 'A', - price: 10, - B: false - }, { - name: 'B', - price: 10.5, - B: true - }]; - - var output = self.view('a', { - a: 'A', - b: 'B', - arr: ['1', '2', '3'] - }, true); - - //console.log('###' + output + '###'); - //console.log('\n\n\n'); - //self.framework.stop(); - //return; - - assert.ok(output.contains('10'), name + 'inline helper'); - assert.ok(output.contains('4040'), name + 'inline helper + condition'); - 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('
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('#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'); - assert.ok(output.contains('#helper-fnwithout-A#'), name + 'helper function (without helper keyword)'); - assert.ok(output.contains('#readonly readonly="readonly"#'), name + 'readonly()'); - assert.ok(output.contains('#checked checked="checked"#'), name + 'checked()'); - assert.ok(output.contains('#selected selected="selected"#'), name + 'selected()'); - assert.ok(output.contains('#disabled disabled="disabled"#'), name + 'disabled()'); - assert.ok(output.contains('#resourcedefault#'), name + 'resource()'); - 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('#contentEMPTY#'), name + 'content'); - assert.ok(output.contains('#content-toggle#'), name + 'contentToggle'); - assert.ok(output.contains('#componentCOMPONENT#'), name + 'component'); - assert.ok(output.contains('#titleTITLE#'), name + 'title'); - // template-one
10.00
10
A
other
10.50
10.5
B
other
- // template-one
10.00
10
A
zero
10.50
10.5
B
one
- assert.ok(output.contains('#template-one
10.00
10
A
zero
D
10.50
10.5
B
one
C
#'), name + 'template() - one'); - assert.ok(output.contains('#template-more
  • A
  • B
#'), name + 'template() - more'); - assert.ok(output.contains('#template-emptyEMPTY#'), name + 'template() - empty'); - assert.ok(output.contains('#template-toggle#'), name + 'templateToggle()'); - assert.ok(output.contains('#routejs-/js/p.js#'), name + 'route to static'); - assert.ok(output.contains('#content#'), name + 'download'); - - assert.ok(output.contains(''), name + 'dns'); - assert.ok(output.contains(''), name + 'prefetch'); - assert.ok(output.contains(''), name + 'prerender'); - assert.ok(output.contains(''), name + 'canonical'); - assert.ok(output.contains(''), name + 'next'); - assert.ok(output.contains(''), name + 'prev'); - assert.ok(output.contains(''), name + 'head'); - - assert.ok(output.contains('src="/js/jquery.js"'), name + 'place (routeJS)'); - assert.ok(output.contains('src="//fabricjs.js"'), name + 'place'); - assert.ok(output.contains('#dynamicOK#'), name + 'dynamic view'); - assert.ok(self.repository.INLINE === 6, name + 'INLINE assign 2'); - - self.repository.A = 'A'; - - self.currentView('current'); - output = self.view('c', { - a: 'A', - b: 'B', - c: true, - d: 'hidden' - }, true); - - assert.ok(output.contains(''), name + 'text'); - assert.ok(output.contains(''), name + 'hidden'); - assert.ok(output.contains(''), name + 'checkbox'); - assert.ok(output.contains(''), name + 'textarea'); - assert.ok(output.contains('#ACAXXX#'), name + 'if'); - assert.ok(output.contains(''), name + 'radio'); - assert.ok(output.contains('
NESTED
'), name + 'if - nested'); - - self.json({ - r: true - }); + var name = 'views: '; + var self = this; + + self.repository.arr = ['Q', 'R', 'S']; + self.repository.title = 'TEST'; + self.repository.tag = 'A'; + + self.repository.a = 'a'; + self.repository.b = 'b'; + self.repository.c = 'c'; + + self.repository.optionsEmpty = [{ + name: 'A', + value: 'A' + }, { + name: 'B', + value: 'B' + }]; + + self.repository.options = [{ + k: 'C', + v: 'C' + }, { + k: 'D', + v: 'D' + }]; + self.repository.template = [{ + name: 'A', + price: 10, + B: false + }, { + name: 'B', + price: 10.5, + B: true + }]; + + var output = self.view('a', { + a: 'A', + b: 'B', + arr: ['1', '2', '3'] + }, true); + + //console.log('###' + output + '###'); + //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'); + assert.ok(output.contains('10'), name + 'inline helper'); + assert.ok(output.contains('4040'), name + 'inline helper + condition'); + 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_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(',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'); + assert.ok(output.contains('#helper-fnwithout-A#'), name + 'helper function (without helper keyword)'); + assert.ok(output.contains('#readonly readonly="readonly"#'), name + 'readonly()'); + assert.ok(output.contains('#checked checked="checked"#'), name + 'checked()'); + assert.ok(output.contains('#selected selected="selected"#'), name + 'selected()'); + assert.ok(output.contains('#disabled disabled="disabled"#'), name + 'disabled()'); + assert.ok(output.contains('#resourcedefault#'), name + 'resource()'); + 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('#titleTITLE#'), name + 'title'); + assert.ok(output.contains('#routejs-/js/p.js#'), name + 'route to static'); + assert.ok(output.contains('#content#'), name + 'download'); + + assert.ok(output.contains(''), name + 'dns'); + assert.ok(output.contains(''), name + 'prefetch'); + assert.ok(output.contains(''), name + 'prerender'); + + assert.ok(output.contains(''), name + 'canonical'); + assert.ok(output.contains(''), name + 'next'); + assert.ok(output.contains(''), name + 'prev'); + assert.ok(output.contains(''), name + 'head'); + assert.ok(output.contains('PLACE'), name + 'place'); + assert.ok(output.contains('#dynamicOK#'), name + 'dynamic view'); + assert.ok(self.repository.INLINE === 6, name + 'INLINE assign 2'); + // console.log(output); + assert.ok(output.contains('#RELEASETRANSLATOR1=A=A#'), name + 'INLINE TRANSLATOR FOR RELEASE MODE 1'); + assert.ok(output.contains('#RELEASETRANSLATOR2=A=A#'), name + 'INLINE TRANSLATOR FOR RELEASE MODE 2'); + assert.ok(output.contains('#absolute1=#'), name + 'absolute problem without hostname'); + assert.ok(output.contains('#absolute2=#'), name + 'aboslute problem with hostname'); + assert.ok(output.contains('#absolute3=#'), name + 'aboslute problem array + with hostname'); + assert.ok(output.contains('#CONFIGNAME1=Total.js#'), name + 'inline config value with value'); + assert.ok(output.contains('#CONFIGNAME2=#'), name + 'inline config value without value'); + + assert.ok(output.contains('#d429c9c776604a9e15d04d9bd90dba27e0155965=a+b+c#'), name + 'https://github.com/totaljs/framework/commit/d429c9c776604a9e15d04d9bd90dba27e0155965'); + + self.repository.A = 'A'; + + output = self.view('current/c', { + a: 'A', + b: 'B', + c: true, + d: 'hidden' + }, true); + + assert.ok(output.contains(''), name + 'text'); + assert.ok(output.contains(''), name + 'hidden'); + assert.ok(output.contains(''), name + 'checkbox'); + assert.ok(output.contains(''), name + 'textarea'); + 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!
---'), name + '- "/" view path problem'); + + F.script('next(value.toLowerCase())', 'PETER', function(err, val) { + assert.ok(val ==='peter', 'SCRIPT: lowercase'); + }); + + self.json({ + r: true + }); } function viewViewsIf() { - var self = this; - self.layout(''); - self.repository.A = 'A'; - self.view('current/c', { - a: 'A', - b: 'B' - }); + var self = this; + self.layout(''); + self.repository.A = 'A'; + self.view('current/c', { + a: 'A', + b: 'B' + }); } function viewError() { - var self = this; - var template = self.template('asdljsald', [1, 2, 3]); - assert.ok(template === '', 'template: not found problem'); - assert.ok(self.content('asdasd') === '', 'content: not found problem'); - self.view('asdlkjasl'); + var self = this; + self.view('asdlkjasl'); } function viewRouteA() { - var self = this; - assert.ok(self.url === '/a/', 'routing: viewRouteA'); - self.plain('OK'); + var self = this; + assert.ok(self.url === '/a/', 'routing: viewRouteA'); + self.plain('OK'); } -function viewRouteAB() { - var self = this; - assert.ok(self.url === '/c/b/', 'routing: viewRouteAB'); - self.plain('OK'); +function viewRouteAB(a, b) { + var self = this; + var params = self.params; + assert.ok(params.a === a, params.b === b, 'controller.params'); + assert.ok(self.url === '/c/b/', 'routing: viewRouteAB'); + self.plain('OK'); } function viewRouteAA(a) { - var self = this; - assert.ok(a === 'aaa', 'routing: viewRouteAA'); - assert.ok(self.url === '/a/aaa/', 'routing: viewRouteAA'); - self.plain('OK'); + var self = this; + assert.ok(a === 'aaa', 'routing: viewRouteAA'); + assert.ok(self.url === '/a/aaa/', 'routing: viewRouteAA'); + self.plain('OK'); } function viewRouteABC() { - var self = this; - assert.ok(self.url === '/a/b/c/', 'routing: viewRouteABC'); - self.plain('OK'); + var self = this; + assert.ok(self.url === '/a/b/c/', 'routing: viewRouteABC'); + self.plain('OK'); } function viewJS() { - var self = this; - self.layout(''); - self.view('d'); + var self = this; + self.layout(''); + self.view('d'); } function viewLive() { - var self = this; - - self.mixed.beg(); + var self = this; - self.mixed.send('/users/petersirka/desktop/aaaaa/1.jpg'); + self.mixed.beg(); + self.mixed.send('/users/petersirka/desktop/aaaaa/1.jpg'); - setTimeout(function() { - self.mixed.send('/users/petersirka/desktop/aaaaa/2.jpg', self.mixed.end.bind(self)); - }, 3000); + setTimeout(function() { + self.mixed.send('/users/petersirka/desktop/aaaaa/2.jpg', self.mixed.end.bind(self)); + }, 3000); } function pipe() { - var self = this; - self.pipe('http://www.totaljs.com/'); + var self = this; + self.pipe('http://www.totaljs.com/'); } function view_cookie() { - var self = this; - self.res.cookie('cookie1', '1', new Date().add('d', 1)); - self.res.cookie('cookie2', '2', new Date().add('d', 1)); - self.res.cookie('cookie3', '3', new Date().add('d', 1)); - self.res.cookie('cookie4', '4', new Date().add('d', 1)); - self.plain('cookie'); + var self = this; + + assert.ok(self.req.cookie('a') === '1', 'request cookie problem 1'); + assert.ok(self.req.cookie('b') === '2', 'request cookie problem 2'); + assert.ok(self.req.cookie('c') === '3', 'request cookie problem 3'); + + self.res.cookie('cookieR', 'O', new Date().add('d', 1)); + self.res.cookie('cookie1', '1', new Date().add('d', 1)); + self.res.cookie('cookie2', '2', new Date().add('d', 1)); + self.res.cookie('cookie3', '3', new Date().add('d', 1)); + self.res.cookie('cookie4', '4', new Date().add('d', 1)); + self.res.cookie('cookieR', 'N', new Date().add('d', 1)); + self.plain('cookie'); } function viewHTTP() { - this.plain('HTTP'); + this.plain('HTTP'); } function viewHTTPS() { - this.plain('HTTPS'); + this.plain('HTTPS'); +} + +function view_compressor() { + var self = this; + self.view('compress', { name: 'Peter' }); +} + +function view_nocompress() { + var self = this; + self.view('nocompress'); +} + +function regexp(number) { + this.plain(number); +} + +function binary() { + this.binary(Buffer.from('čťž'), 'text/plain', 'utf8'); +} + +function mobile() { + this.plain('X'); +} + +function mobile_none() { + this.plain('NO-MOBILE'); +} + +function view_theme() { + var self = this; + self.theme('green'); + self.view('index'); } \ No newline at end of file diff --git a/test/controllers/share.js b/test/controllers/share.js index e478332dc..071aa367b 100755 --- a/test/controllers/share.js +++ b/test/controllers/share.js @@ -1,4 +1,4 @@ -exports.install = function(framework) { +exports.install = function() { framework.route('/share/', view_share); framework.route('/router/', view_router); framework.route('/share/a/', view_share_a); diff --git a/test/controllers/subdirectory/share.js b/test/controllers/subdirectory/share.js index 7c4d2f40b..46afdf81c 100644 --- a/test/controllers/subdirectory/share.js +++ b/test/controllers/subdirectory/share.js @@ -1,4 +1,4 @@ -exports.install = function(framework) { +exports.install = function() { framework.route('/sub/share/', view_share); }; diff --git a/test/databases/pages.nosql b/test/databases/pages.nosql new file mode 100644 index 000000000..c9824f399 --- /dev/null +++ b/test/databases/pages.nosql @@ -0,0 +1,3 @@ +{"url":"http://feathers.jrbsystem.com","datecreated":"2017-08-04T12:59:05.007Z"} +{"openfiles":24,"connections":0,"hdd":5340,"url":"http://express.jrbsystem.com","datecreated":"2017-08-04T12:59:05.007Z"} +{"cpu":0,"memory":31186944,"openfiles":25,"connections":2,"url":"http://helpdesk.jrbsystem.com","datecreated":"2017-08-04T12:59:05.042Z"} diff --git a/test/databases/users.nosql b/test/databases/users.nosql index d5bc091f8..f040e97cb 100644 --- a/test/databases/users.nosql +++ b/test/databases/users.nosql @@ -1,6 +1,6 @@ -{"name":"X","index":0} -{"name":"X","index":1} -{"name":"X","index":2} -{"name":"X","index":0} -{"name":"X","index":1} -{"name":"X","index":2} +{"name":"A","email":"1","index":1} +{"name":"B","email":"2","index":2} +{"name":"C","email":"3","index":3} +{"name":"D","email":"4","index":4} +{"name":"E","email":"5","index":5} +{"name":"F","email":"6","index":6} diff --git a/test/default.resource b/test/default.resource new file mode 100644 index 000000000..2d37ed362 --- /dev/null +++ b/test/default.resource @@ -0,0 +1 @@ +name-root : RESOURCE FROM ROOT \ No newline at end of file diff --git a/test/definitions/aaa.js b/test/definitions/aaa.js deleted file mode 100755 index add07eda7..000000000 --- a/test/definitions/aaa.js +++ /dev/null @@ -1,2 +0,0 @@ -framework.config.isDefinition = true; -framework.resize('/*.jpg', '50%'); \ No newline at end of file diff --git a/test/definitions/convertors.js b/test/definitions/convertors.js new file mode 100644 index 000000000..68e06fdc2 --- /dev/null +++ b/test/definitions/convertors.js @@ -0,0 +1,2 @@ +F.convert('page', Number); +F.convert('root.page', Number); \ No newline at end of file diff --git a/test/definitions/initialize.js b/test/definitions/initialize.js new file mode 100644 index 000000000..3bacbf4af --- /dev/null +++ b/test/definitions/initialize.js @@ -0,0 +1,69 @@ +var assert = require('assert'); + +F.register(F.path.root('default.resource')); + +framework.onMeta = function(a,b) { + return a + b; +}; + +framework.onSettings = function(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 }; + } + + 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; + + 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'; +}; + +// Is read from http://www.totaljs.com/framework/include.js +//framework.precompile('precompile.layout', 'http://www.totaljs.com/framework/_layout.html'); +//framework.precompile('precompile.homepage', 'http://www.totaljs.com/framework/homepage.html'); \ No newline at end of file diff --git a/test/definitions/merging-mapping.js b/test/definitions/merging-mapping.js new file mode 100755 index 000000000..035167bda --- /dev/null +++ b/test/definitions/merging-mapping.js @@ -0,0 +1,11 @@ +framework.config.isDefinition = true; +framework.resize('/*.jpg', '50%'); +framework.merge('/merge.js', '/js/test.js', 'file.txt'); +framework.merge('/merge-blocks-a.js', '/js/block.js#a,c,d'); +framework.merge('/merge-blocks-b.js', '/js/block.js#b'); +framework.map('/fet.txt', framework.path.public('file.txt')); +framework.map('/blocks-a.js', '/js/block.js#a,c,d'); +framework.map('/blocks-b.js', '/js/block.js#b'); +framework.map('/blocks-c.js', '/js/block.js#a,b'); +framework.merge('/merge-theme.js', '=green/public/js/default.js', '=green/public/js/merge.js'); +framework.map('/map-theme.js', '=green/public/js/default.js'); \ No newline at end of file diff --git a/test/fulltext.js b/test/fulltext.js deleted file mode 100755 index 495ed10ba..000000000 --- a/test/fulltext.js +++ /dev/null @@ -1,3 +0,0 @@ -var a = ['a', 'b', 'c']; - -console.log(a.join('; ')); \ No newline at end of file diff --git a/test/javascript.js b/test/javascript.js new file mode 100644 index 000000000..169f733d1 --- /dev/null +++ b/test/javascript.js @@ -0,0 +1,11 @@ +// FOR UNITTEST +return '\\' + 2; + +var attributes = "\\[" + a + "*(" + b + ")(?:" + c + + // Operator (capture 2) + "*([*^$|!~]?=)" + d + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + e + "))|)" + f + + "*\\]"; + +var a = 200; \ No newline at end of file diff --git a/test/md.js b/test/md.js deleted file mode 100755 index c2a9ef236..000000000 --- a/test/md.js +++ /dev/null @@ -1,713 +0,0 @@ -var utils = require('../utils'); - -const EMPTY = 0; -const PARAGRAPH = 100; -const EMBEDDED = 101; -const LIST = 102; -const KEYVALUE = 103; -const TMP = '@##'; - -const REG_LINK_1 = /\<.*?\>+/g; -const REG_LINK_2 = /(!)?\[[^\]]+\][\:\s\(]+.*?[^)\s$]+/g; -const REG_LINK_3 = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig; -const REG_LINK_4 = /(^|[^\/])(www\.[\S]+(\b|$))/gim; -const REG_IMAGE = /(\[)?\!\[[^\]]+\][\:\s\(]+.*?[^\s$]+/g; -const REG_FORMAT = /\*{1,2}.*?\*{1,2}|_{1,3}.*?_{1,3}/g; -const REG_KEYWORD = /(\[.*?\]|\{.*?\})/g; - -function Markdown() { - - this.embedded = '==='; - - this.onEmbedded = function(type, lines) { - return lines.join('\n'); - }; - - this.onKeyword = function(type, value) { - return '' + value + ''; - }; - - this.onParagraph = function(type, lines) { - - var self = this; - var cls = ''; - - switch (type) - { - case '>': - case '|': - cls = 'quote'; - break; - - case '//': - cls = 'comment'; - break; - } - - return '

' + lines.join('
') + '

'; - }; - - this.onLine = function(line) { - return '

' + line + '

'; - }; - - this.onFormat = function(type, value) { - - switch (type) { - case '**': - return '' + value + ''; - case '*': - return '' + value + ''; - case '__': - return '' + value + ''; - case '_': - return '' + value + ''; - } - - return value; - }; - - this.onLink = function(text, url) { - - if (url.substring(0, 7) !== 'http://' && url.substring(0, 8) !== 'https://') - url = 'http://' + url; - - return ''; - - if (url) - return '' + tag + ''; - - return img; - }; - - this.onList = function(items) { - - var length = items.length; - var output = ''; - - for (var i = 0; i < length; i++) { - var item = items[i]; - output += '
  • ' + item.value + '
  • '; - } - - return '
      ' + output + '
    '; - - }; - - this.onKeyValue = function(items) { - - var length = items.length; - var output = ''; - - for (var i = 0; i < length; i++) { - var item = items[i]; - output += '
    ' + item.key + '
    ' + item.value + '
    '; - } - - return '
    ' + output + '
    '; - }; - - this.onBreak = function(type) { - - switch (type) { - case '\n': - return '
    '; - case '***': - case '---': - return '
    '; - } - - return '
    '; - }; - - this.onTitle = function(type, text) { - - switch (type) { - case '#': - return '

    ' + text + '

    ' - case '##': - return '

    ' + text + '

    ' - case '###': - return '

    ' + text + '

    ' - case '####': - return '

    ' + text + '

    ' - case '#####': - return '
    ' + text + '
    ' - } - - return type + ' ' + text; - }; - - this.current = []; - this.status = 0; - this.command = ''; - this.skip = false; - this.output = ''; - this.tmp = []; - this.id = ''; -} - -Markdown.prototype.load = function(text, id) { - - var self = this; - var arr = text.split('\n'); - var length = arr.length; - - self.output = ''; - - for (var i = 0; i < length; i++) { - - if (self.skip) { - self.skip = false; - continue; - } - - var line = arr[i]; - - if (self.parseEmbedded(line)) - continue; - - if (self.parseBreak(line)) - continue; - - if (self.parseList(line)) - continue; - - if (self.parseKeyValue(line)) - continue; - - if (self.parseParagraph(line)) - continue; - - if (self.parseTitle(line, arr[i + 1])) - continue; - - if (self.onLine !== null) - self.output += self.onLine(self.parseOther(line)); - } - - if (self.status !== EMPTY) - self.flush(); - - return self.output; -}; - -Markdown.prototype.parseEmbedded = function(line) { - - var self = this; - var status = self.status; - var chars = self.embedded + (status !== EMBEDDED ? ' ' : ''); - var has = line.substring(0, chars.length) === chars; - - if (status !== EMBEDDED && !has) - return false; - - if (status !== EMBEDDED && has) - self.flush(); - - if (status === EMBEDDED && has) { - self.flush(); - self.status = EMPTY; - return true; - } - - if (has) { - self.status = EMBEDDED; - status = EMBEDDED; - self.command = line.substring(chars.length); - return true; - } - - if (status === EMBEDDED) - self.current.push(line); - - return true; -}; - -Markdown.prototype.parseBreak = function(line) { - - var self = this; - - if (line === '' || line === '***' || line === '---') { - - var status = self.status; - - if (status !== EMPTY) - self.flush(); - - self.status = EMPTY; - - if (self.onBreak) - self.output += self.onBreak(line === '' ? '\n' : line) || ''; - - return true; - } - - return false; -}; - -Markdown.prototype.parseList = function(line) { - - var self = this; - - var first = line[0] || ''; - var second = line[1] || ''; - - var has = (first === '-' || first === '+' || first === 'x') && (second === ' '); - - if (!has) - return false; - - var status = self.status; - - if (status !== LIST) { - self.flush(); - self.status = LIST; - } - - self.current.push({ type: first, value: self.parseOther(line.substring(3)) }); - return true; -}; - -Markdown.prototype.parseKeyValue = function(line) { - - var self = this; - var index = line.indexOf(':'); - - if (index === -1) - return false; - - var tmp = line.substring(0, index); - var length = tmp.length; - - var countTab = 0; - var countSpace = 0; - - for (var i = 0; i < length; i++) { - - var c = tmp[i]; - - if (c === '\t') { - countTab++; - break; - } - - if (c === ' ') { - countSpace++; - if (countSpace > 2) - break; - } else - countSpace = 0; - } - - if (countSpace < 3 && countTab <= 0) - return false; - - var status = self.status; - - if (status !== KEYVALUE) { - self.flush(); - self.status = KEYVALUE; - } - - self.current.push({ key: self.parseOther(tmp.trim()), value: self.parseOther(line.substring(index + 1).trim()) }); - return true; -}; - -Markdown.prototype.parseParagraph = function(line) { - - var self = this; - var first = line[0] || ''; - var second = line[1] || ''; - var index = 0; - var has = false; - - switch (first) { - case '>': - case '|': - has = second === ' '; - index = 1; - break; - - case '/': - has == second === '/' && line[3] === ' '; - index = 2; - break; - } - - if (!has) - return false; - - var status = self.status; - - if (has) { - var command = first + (first === '/' ? '/' : ''); - if (self.command !== '' && self.command !== command && status === PARAGRAPH) - self.flush(); - self.command = command; - } - - if (status !== PARAGRAPH) { - self.flush(); - self.status = PARAGRAPH; - status = PARAGRAPH; - } - - self.current.push(self.parseOther(line.substring(index).trim())); - - return true; -}; - -Markdown.prototype.parseTitle = function(line, next) { - - var self = this; - var has = line[0] === '#'; - var type = ''; - - if (!has) { - var first = (next || '')[0] || ''; - has = line[0].charCodeAt(0) > 64 && (first === '=' || first === '-'); - - if (has) - has = line.length === next.length; - - if (has) { - type = first === '=' ? '#' : '##'; - self.skip = true; - } - - } else { - - var index = line.indexOf(' '); - if (index === -1) - return false; - - type = line.substring(0, index).trim(); - } - - if (!has) - return false; - - if (self.status !== EMPTY) - self.flush(); - - if (self.onTitle !== null) - self.output += self.onTitle(type, self.parseOther(self.skip ? line : line.substring(type.length + 1))) || ''; - - return true; -}; - -Markdown.prototype.parseOther = function(line) { - - var self = this; - - if (self.tmp.length > 0) - self.tmp = []; - - // link - line = self.parseLink(line); - - // image - line = self.parseImage(line); - - // other - line = self.parseFormat(line); - - // inline linke - line = self.parseLinkInline(line); - - if (self.tmp.length > 0) { - var length = self.tmp.length; - for (var i = 0; i < length; i++) { - var item = self.tmp[i]; - line = line.replace(item.k, item.v); - } - } - - return line; -}; - -Markdown.prototype.parseFormat = function(text, flush) { - - var matches = text.match(REG_FORMAT); - if (matches === null) - return text; - - var self = this; - var length = matches.length; - - for (var i = 0; i < length; i++) { - - var o = matches[i]; - var isAsterix = o[0] === '*'; - var value = ''; - - if (isAsterix) { - value = self.onFormat(o[1] === '*' ? '**' : '*', o.replace(/^\*{1,2}|\*{1,2}$/g, '')); - text = text.replace(o, flush ? value : self.getReplace(o, value)); - } else { - value = self.onFormat(o[1] === '_' ? '__' : '_', o.replace(/^_{1,2}|_{1,2}$/g, '')); - text = text.replace(o, flush ? value : self.getReplace(o, value)); - } - } - - return text; -}; - -Markdown.prototype.parseLink = function(text) { - - var matches = text.match(REG_LINK_1); - var output = text; - var length = 0; - var self = this; - - if (matches !== null) { - length = matches.length; - for (var i = 0; i < length; i++) { - var o = matches[i]; - var url = o.substring(1, o.length - 1); - output = output.replace(o, self.getReplace(o, self.onLink(url, url))); - } - } - - matches = text.match(REG_LINK_2); - - if (matches === null) - return output; - - length = matches.length; - - for (var i = 0; i < length; i++) { - - var o = matches[i]; - - if (o.substring(0, 3) === '[![') - continue; - - var index = o.indexOf(']'); - if (index === -1) - continue; - - if (o[0] === '!') - continue; - - var text = o.substring(1, index).trim(); - var url = o.substring(index + 1).trim(); - - var first = url[0]; - - if (first === '(' || first === '(' || first === ':') - url = url.substring(1).trim(); - else - continue; - - if (first === '(') - o += ')'; - - var last = url[url.length - 1]; - - if (last === ',' || last === '.' || last === ' ') - url = url.substring(0, url.length - 1); - else - last = ''; - - output = output.replace(o, self.getReplace(o, self.onLink(self.parseFormat(text, true), url) + last)); - } - - return output; -}; - -Markdown.prototype.parseLinkInline = function(text) { - - var matches = text.match(REG_LINK_3); - var length = 0; - var self = this; - - if (matches !== null) { - - length = matches.length; - for (var i = 0; i < length; i++) { - var o = matches[i].trim(); - text = text.replace(o, self.getReplace(o, self.onLink(o, o))); - } - - } - - matches = text.match(REG_LINK_4); - - if (matches !== null) { - - length = matches.length; - for (var i = 0; i < length; i++) { - var o = matches[i].trim(); - text = text.replace(o, self.getReplace(o, self.onLink(o, o))); - } - - } - - return text; -}; - -Markdown.prototype.parseImage = function(text) { - - var output = text; - var matches = text.match(REG_IMAGE); - - if (matches === null) - return output; - - var self = this; - - var length = matches.length; - - for (var i = 0; i < length; i++) { - - var o = matches[i]; - var indexBeg = 2; - - if (o.substring(0, 3) === '[![') - indexBeg = 3; - - var index = o.indexOf(']'); - if (index === -1) - continue; - - var text = o.substring(indexBeg, index).trim(); - var url = o.substring(index + 1).trim(); - - var first = url[0]; - if (first !== '(') - continue; - - index = o.lastIndexOf(')'); - if (index === -1) - continue; - - var find = o.substring(0, index + 1); - - url = url.substring(1, index + 1); - index = url.indexOf('#'); - indexBeg = index; - - var src = ''; - var indexEnd = url.indexOf(')', index); - - var dimension = []; - - if (index > 0) { - dimension = url.substring(indexBeg + 1, indexEnd).split('x'); - src = url.substring(0, index); - } else - src = url.substring(0, indexEnd); - - indexBeg = url.indexOf('(', indexEnd); - indexEnd = url.lastIndexOf(')'); - - if (indexBeg !== -1 && indexBeg > index) - url = url.substring(indexBeg + 1, indexEnd); - else - url = ''; - - output = output.replace(find, self.getReplace(find, self.onImage(text, src, parseInt(dimension[0] || '0', 10), parseInt(dimension[1] || '0', 10), url))); - } - - return output; -} - -Markdown.prototype.parseKeyword = function(text) { - var matches = text.match(REG_KEYWORD); - var length = 0; - var self = this; - - if (matches === null) - return text; - - length = matches.length; - for (var i = 0; i < length; i++) { - var o = matches[i]; - var type = '[]'; - - if (o[0] === '{') - type = '{}'; - - var value = o; - - if (type === '{}') - value = value.replace(/^\{{}|(\}|]){1}$/g, ''); - else - value = value.replace(/^\[{}|(\]|]){1}$/g, ''); - - text = text.replace(o, self.getReplace(o, value)); - } - - return text; -}; - -Markdown.prototype.getReplace = function(find, value) { - var self = this; - var key = TMP + self.tmp.length + ';' - self.tmp.push({ k: key, v: value }); - return key; -}; - -Markdown.prototype.flush = function() { - - var self = this; - - switch (self.status) { - case EMBEDDED: - - if (self.onEmbedded !== null) - self.output += self.onEmbedded(self.command, self.current); - - break; - - case LIST: - - if (self.onList !== null) - self.output += self.onList(self.current); - - break; - - case KEYVALUE: - - if (self.onKeyValue !== null) - self.output += self.onKeyValue(self.current); - - break; - - case PARAGRAPH: - - if (self.onParagraph !== null) - self.output += self.onParagraph(self.command, self.current); - - break; - } - - self.current = []; - self.command = ''; -}; - -// ====================================================== -// EXPORTS -// ====================================================== - -exports.init = function() { - return new Markdown(); -}; - -exports.load = function() { - return new Markdown(); -}; - -exports.markdown = function() { - return new Markdown(); -}; - -exports.md = function() { - return new Markdown(); -}; diff --git a/test/models/other/products.js b/test/models/other/products.js new file mode 100644 index 000000000..3be816b31 --- /dev/null +++ b/test/models/other/products.js @@ -0,0 +1 @@ +exports.ok = 2; \ No newline at end of file diff --git a/test/models/user.js b/test/models/user.js index 91941c84e..8f57573b8 100755 --- a/test/models/user.js +++ b/test/models/user.js @@ -1 +1,15 @@ -exports.ok = 1; \ No newline at end of file +exports.ok = 1; + +var User = NEWSCHEMA('test', 'User'); +User.define('name', 'string(10)', true); +User.setValidate(function(name, value) { + return value.length > 0; +}); + +NEWSCHEMA('filter').make(function(schema) { + schema.define('name', String, true, 'create'); + schema.define('age', Number, true, 'update'); + schema.setValidate(function() { + return false; + }); +}); \ No newline at end of file diff --git a/test/modules/#.js b/test/modules/#.js deleted file mode 100755 index d8fbb04b6..000000000 --- a/test/modules/#.js +++ /dev/null @@ -1,57 +0,0 @@ -var assert = require('assert'); - -exports.onMeta = function(a,b) { - return a + b; -}; - -exports.onSettings = function(a,b) { - return a + b; -}; - -exports.onPictureDimension = function(dimension) { - - switch(dimension) { - case 'small': - return { width: 128, height: 96 }; - case 'middle': - return { width: 320, height: 240 }; - } - - return null; -}; - -exports.onLoad = 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.partial = 0; - self.global.timeout = 0; - - self.partial(function(next) { - self.global.header++; - next(); - }); - - self.partial('partial', function(next) { - self.global.partial++; - next(); - }); -}; - -exports.onPictureUrl = function(dimension, id, width, height, alt) { - return dimension + '-' + id + '.jpg'; -}; - -exports.onValidation = function(name, value) { - return name + value; -}; \ No newline at end of file diff --git a/test/modules/inline-view.js b/test/modules/inline-view.js new file mode 100644 index 000000000..39b6fac8a --- /dev/null +++ b/test/modules/inline-view.js @@ -0,0 +1,11 @@ +var assert = require('assert'); + +exports.installed = false; + +exports.install = function() { + 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/someview.html b/test/modules/someview.html new file mode 100644 index 000000000..062a0d918 --- /dev/null +++ b/test/modules/someview.html @@ -0,0 +1,2 @@ +@{layout('')} +VIEW IN MODULES \ No newline at end of file diff --git a/test/modules/super/index.js b/test/modules/super/index.js new file mode 100644 index 000000000..416b390f9 --- /dev/null +++ b/test/modules/super/index.js @@ -0,0 +1,2 @@ +exports.id = 'supermodule'; +exports.ok = true; \ No newline at end of file diff --git a/test/modules/test.js b/test/modules/test.js index 3680f59c2..0699116ed 100755 --- a/test/modules/test.js +++ b/test/modules/test.js @@ -1,9 +1,13 @@ var assert = require('assert'); var app; -exports.install = function(framework) { +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); }; exports.message = function() { diff --git a/test/my-config.txt b/test/my-config.txt new file mode 100644 index 000000000..f7eaa9d24 --- /dev/null +++ b/test/my-config.txt @@ -0,0 +1 @@ +custom-config1 : 1YES \ No newline at end of file diff --git a/test/packages/testpackage.package b/test/packages/testpackage.package new file mode 100644 index 000000000..e68dd4522 --- /dev/null +++ b/test/packages/testpackage.package @@ -0,0 +1,4 @@ +/index.js :H4sIAAAAAAAAA0utKMgvKinWy8wrLknMyVGwVUgrzUsuyczP09BUqOZSAAI3vaL80pJUDXX9gsTk7MT0VH11HUxVIFCSkVmsV5aZWq6h7lCSWlwCUw9iq2tag9XVanLVWgMA0aObrXYAAAA= +/layout.html :H4sIAAAAAAAAA7NJySyzC3B09nZ0d/VxjPQPDbHRBwk5VCflp1TWAgDiWX2wHwAAAA== +/test.html :H4sIAAAAAAAAA3OozkmszC8t0VB3KEktLilITM5OTE/Vhwiqa9Zy2aRkltkFODp7O7q7hnm6htvogwQA78+CxDcAAAA= +/test.js :H4sIAAAAAAAAA0vOzyvOz0nVy8lP11AvSS0uUde0BgAYM45aFAAAAA== diff --git a/empty-project/databases/empty b/test/public/directory.js/empty similarity index 100% rename from empty-project/databases/empty rename to test/public/directory.js/empty diff --git a/empty-project/source/empty b/test/public/directory.txt/empty similarity index 100% rename from empty-project/source/empty rename to test/public/directory.txt/empty diff --git a/test/public/file.txt b/test/public/file.txt new file mode 100644 index 000000000..3b1246497 --- /dev/null +++ b/test/public/file.txt @@ -0,0 +1 @@ +TEST \ No newline at end of file diff --git a/test/public/js/block.js b/test/public/js/block.js new file mode 100644 index 000000000..178d654bd --- /dev/null +++ b/test/public/js/block.js @@ -0,0 +1,9 @@ +var common = true; + +// @{BLOCK a} +var a = true; +// @{END} + +// @{BLOCK b} +var b = true; +// @{END} \ No newline at end of file diff --git a/test/public/js/test.js b/test/public/js/test.js new file mode 100644 index 000000000..b1471c51b --- /dev/null +++ b/test/public/js/test.js @@ -0,0 +1,2 @@ +var a = 1 + 1 + 1; +console.log( a ); \ No newline at end of file diff --git a/test/public/templates/localization.html b/test/public/templates/localization.html new file mode 100644 index 000000000..1a4907228 --- /dev/null +++ b/test/public/templates/localization.html @@ -0,0 +1 @@ +###@(#T1052832078)### \ No newline at end of file diff --git a/test/resources/default.resource b/test/resources/default.resource index fddbb71e4..572c1c0d4 100755 --- a/test/resources/default.resource +++ b/test/resources/default.resource @@ -1 +1,3 @@ -name : default \ No newline at end of file +name : default +error-name : error-name +T-2032180703 : DEFAULT \ No newline at end of file diff --git a/test/resources/sk.resource b/test/resources/sk.resource new file mode 100644 index 000000000..69ea94688 --- /dev/null +++ b/test/resources/sk.resource @@ -0,0 +1,8 @@ +T1052832078 : preklad +name : default + +July : Júl +Monday : Pondelok +T-2032180703 : SK + +T-251020068 : /abcdefgh-sk/ \ No newline at end of file diff --git a/test/run.sh b/test/run.sh new file mode 100755 index 000000000..b570a3371 --- /dev/null +++ b/test/run.sh @@ -0,0 +1,12 @@ +#!/bin/bash +pwd + +export TZ=CET-1CEST + +node test-builders.js && +node test-javascript.js && +node test-css.js && +node test-utils.js && +node test-framework-debug.js && +node test-framework-release.js + 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/sitemap b/test/sitemap new file mode 100644 index 000000000..afd2ca8c4 --- /dev/null +++ b/test/sitemap @@ -0,0 +1,6 @@ +a : A --> / +b : B --> /b/ --> a +c : C --> /c/ --> b +d : D --> /d/ --> c + +route : ROUTE --> @(/abcdefgh/) --> a \ 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/storage b/test/storage deleted file mode 100755 index 350c7a4ac..000000000 --- a/test/storage +++ /dev/null @@ -1 +0,0 @@ -{"info":{"num_full_gc":5,"num_inc_gc":11,"heap_compactions":5,"usage_trend":0,"estimated_base":7516288,"current_base":7516288,"min":7283200,"max":7516288}} \ No newline at end of file diff --git a/test/templates/current/more.html b/test/templates/current/more.html deleted file mode 100755 index 42f320c09..000000000 --- a/test/templates/current/more.html +++ /dev/null @@ -1,5 +0,0 @@ -
      - @{foreach} -
    • @{model.name}
    • - @{end} -
    \ No newline at end of file diff --git a/test/templates/current/test.html b/test/templates/current/test.html deleted file mode 100755 index 737bd974c..000000000 --- a/test/templates/current/test.html +++ /dev/null @@ -1 +0,0 @@ -
    @{foreach}@{model}@{end}
    @{repository.name.max(1)} \ No newline at end of file diff --git a/test/templates/new.html b/test/templates/new.html deleted file mode 100644 index 568572463..000000000 --- a/test/templates/new.html +++ /dev/null @@ -1,10 +0,0 @@ -@{foreach var m in model} -
    @{m.tag}
    @{!m.tag}
    @{index} -@{end} - -@{foreach m in [0, 1, 2]} -
    @{m}-@{index}
    - @{if index % 2 === 0} - MOD - @{endif} -@{end} \ No newline at end of file diff --git a/test/templates/one.html b/test/templates/one.html deleted file mode 100755 index c33340fdc..000000000 --- a/test/templates/one.html +++ /dev/null @@ -1,5 +0,0 @@ -
    @{model.price.format('##.##')}
    -
    @{model.price}
    -
    @{if index % 2 === 0}A@{else}B@{endif}
    -
    @{index.pluralize('zero', 'one', 'few', 'other')}
    -
    @{if model.B}C@{else}D@{endif}
    \ No newline at end of file diff --git a/test/test-builders.js b/test/test-builders.js new file mode 100755 index 000000000..e140df2ae --- /dev/null +++ b/test/test-builders.js @@ -0,0 +1,686 @@ +require('../index'); +global.utils = require('../utils'); + +var assert = require('assert'); +global.builders = require('../builders'); + +var countW = 0; +var countS = 0; + +function test_PageBuilder() { + + var name = 'Pagination: '; + + builders.Pagination.addTransform('custom', function(argument1) { + assert.ok(argument1 === 1, name + 'addTransform(argument1)'); + return this.count; + }); + + var builder = new builders.Pagination(100, 1, 12); + + assert.ok(builder.isPrev === false, name + 'isPrev (1)'); + assert.ok(builder.isNext === true, name + 'isNext (1)'); + assert.ok(builder.isFirst === true, name + 'isFirst (1)'); + assert.ok(builder.isLast === false, name + 'isLast (1)'); + assert.ok(builder.nextPage === 2, name + 'nextPage (1)'); + assert.ok(builder.prevPage === 1, name + 'prevPage (1)'); + assert.ok(builder.lastPage === 9, name + 'lastPage (1)'); + assert.ok(builder.last().page === 9, name + 'last(1)'); + + var output = builder.render(); + + output = builder.render(6); + builder.refresh(100, 5, 12); + + assert.ok(builder.isPrev, name + 'isPrev (2)'); + assert.ok(builder.isNext, name + 'isNext (2)'); + assert.ok(builder.isFirst === false, name + 'isFirst (2)'); + assert.ok(builder.isLast === false, name + 'isLast (2)'); + assert.ok(builder.nextPage === 6, name + 'nextPage (2)'); + assert.ok(builder.prevPage === 4, name + 'prevPage (2)'); + assert.ok(builder.lastPage === 9, name + 'lastPage (2)'); + assert.ok(builder.last().page === 9, name + 'last(2)'); + + output = builder.render(5); + assert.ok(output[2].selected, name + 'render - max 5 (selected page problem)'); + assert.ok(output[4].url === '?page=7', name + 'render - max 5 (url format)'); + + builder.refresh(1, 1, 12); + assert.ok(builder.isFirst === true, name + 'isFirst (3)'); + assert.ok(builder.isLast === true, name + 'isLast (3)'); + assert.ok(builder.nextPage === 1, name + 'nextPage (3)'); + assert.ok(builder.prevPage === 1, name + 'prevPage (3)'); + assert.ok(builder.lastPage === 1, name + 'lastPage (3)'); + + builder.refresh(10, 1, 5); + assert.ok(builder.isFirst === true, name + 'isFirst (4)'); + assert.ok(builder.isLast === false, name + 'isLast (4)'); + assert.ok(builder.transform('custom', 1) === 2, name + 'transform()'); + assert.ok(builder.nextPage === 2, name + 'nextPage (4)'); + assert.ok(builder.prevPage === 1, name + 'prevPage (4)'); + assert.ok(builder.lastPage === 2, name + 'lastPage (4)'); + + builders.Pagination.setDefaultTransform('custom'); + + var builder = new builders.Pagination(100, 1, 10); + assert.ok(builder.render(1) === 10, name + 'default transform()'); +} + +function test_UrlBuilder() { + var name = 'UrlBuilder: '; + var builder = new builders.UrlBuilder(); + + builder.add('A', '1'); + builder.add('B', '2'); + builder.add('C', ' 3 '); + + assert.ok(builder.read('C') === ' 3 ', name + 'read'); + + builder.remove('B'); + assert.ok(builder.read('B') === null, name + 'remove'); + + builder.add('A', '5'); + assert.ok(builder.read('A') === '5', name + 'update'); + + assert.ok(builder.toString() === 'A=5&C=%203%20', name + 'toString()'); + builder.add('C', '3'); + + assert.ok(builder.toOne(['A', 'B', 'C'], 'X') === '5XX3', name + 'toOne()'); + + builder.clear(); + assert.ok(builder.read('A') === null, name + 'clear()'); + + assert.ok(builder.hasValue(['A', 'B']) === false, name + 'hasValue(empty)'); + builder.add('A', '1'); + builder.add('B', '2'); + assert.ok(builder.hasValue(['A', 'B']) === true, name + 'hasValue()'); +} + +function test_Schema() { + var name = 'Schema: '; + + NEWSCHEMA('tbl_user').make(function(schema) { + schema.define('Id', Number); + schema.define('Name', String); + schema.define('date', Date); + schema.setDefault(function(name) { + if (name === 'date') + return 'OK'; + }); + }); + + //assert.ok(builders.schema('default').get('tbl_user').schema.Id instanceof Function, name + 'schema write & read'); + //assert.ok(JSON.stringify(builders.defaults('tbl_user')) === '{"date":"OK","Name":"","Id":0}', name + 'schema defaults'); + //assert.ok(JSON.stringify(builders.create('tbl_user')) === '{"date":"OK","Name":"","Id":0}', name + 'schema create'); + + NEWSCHEMA('test').make(function(schema) { + schema.define('Id', Number); + schema.define('Name', String); + schema.define('Dt', Date); + schema.define('Male', Boolean); + schema.define('Price', 'decimal'); + }); + + var model = { + Name: 23, + Male: '1', + Dt: 'ADASD', + Price: 1.13 + }; + + var output = builders.prepare('test', model); + + name = 'Schema.prepare: '; + + assert.ok(output.Price === 1.13, name + 'decimal'); + assert.ok(output.Name === '23', name + 'string'); + assert.ok(output.Male, name + 'boolean = true'); + //assert.ok(output.Dt === null, name + 'date (invalid)'); + + assert.ok(builders.prepare('tbl_user', {}).date === 'OK', name + 'defaults'); + + model = { + Dt: '2012-12-12', + Male: false + }; + output = builders.prepare('test', model); + + assert.ok(output.Dt.getDate() === 12 && output.Dt.getMonth() === 11 && output.Dt.getFullYear() === 2012, name + 'date'); + assert.ok(!output.Male, name + 'boolean = false'); + + output = builders.defaults('test'); + + assert.ok(output.Id === 0, name + 'defaults (int)'); + assert.ok(output.Name === '', name + 'defaults (String)'); + assert.ok(output.Male === false, name + 'defaults (Boolean)'); + + NEWSCHEMA('1').make(function(schema) { + schema.define('name', String); + schema.define('join', '[2]'); + }); + + GETSCHEMA('1').define('nums', '[number]'); + + NEWSCHEMA('2').make(function(schema) { + schema.define('age', Number); + schema.setDefault(function(name) { + if (name === 'age') + return -1; + }); + }); + + GETSCHEMA('2').addTransform('xml', function($) { + $.next('OK'); + }).addWorkflow('send', function($) { + countW++; + $.callback('workflow'); + }).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($) { + 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 }})); + output = builders.prepare('1', { + name: 'Peter', + join: [{ + name: 'TEST' + }, { + age: 20, + test: 'KUNDA' + }], + nums: ['1', 'asdas', 2.3] + }); + + assert.ok(output.join[0].age === -1 && output.join[1].age === 20, name + 'schema - joining models'); + assert.ok(output.nums[2] === 2.3 && output.nums[1] === 0, name + 'schema - parse plain array'); + + GETSCHEMA('2').transform('xml', output, function(err, output) { + assert.ok(output === 'OK', 'Builders.transform()'); + }); + + GETSCHEMA('2').workflow('send', output, function(err, output) { + assert.ok(output === 'workflow', 'Builders.workflow()'); + }).get(null, function(err, result) { + assert.ok(result.age === 99, 'schema - get'); + }).save(output, function(err, result) { + assert.ok(result === true, 'schema - save'); + }).remove(output, function(err, result) { + assert.ok(result === true, 'schema - remove'); + }).query(output, function(err, result) { + assert.ok(result.length === 0, 'schema - query'); + }).operation('', function(err, result) { + assert.ok(!result, 'schema - operation - result'); + }); + + 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'); + }); + + NEWSCHEMA('validator').make(function(schema) { + schema.define('name', String, true); + schema.define('age', Number, true); + schema.define('isTerms', Boolean, true); + + schema.setValidate(function(name, value, path, schema) { + assert.ok(name !== 'validator', 'schema validator - problem with schema name in utils.validate()'); + switch (name) { + case 'name': + return value.length > 0; + case 'age': + return value > 10; + case 'isTerms': + return value === true; + } + }); + }); + + var builder = builders.validate('validator', { + name: 'Peter' + }); + + assert.ok(builder.hasError(), name + 'schema validator (error)'); + + builder = builders.validate('validator', { + name: 'Peter', + age: 34, + isTerms: true + }); + + assert.ok(!builder.hasError(), name + 'schema validator (no error)'); + + var obj = GETSCHEMA('default', '2').create(); + + var b = obj.$clone(); + assert.ok(obj.age === b.age, 'schema $clone 1'); + b.age = 10; + assert.ok(obj.age !== b.age, 'schema $clone 2'); + + obj.$async(function(err, result) { + assert.ok(err === null && countW === 2 && countS === 2 && result.length === 2, 'schema $async'); + }).$save().$workflow('send'); + + var q = NEWSCHEMA('test', 'q'); + var x = NEWSCHEMA('test', 'x'); + + q.define('name', String, true); + q.define('arr', '[x]', true); + q.define('ref', x); + q.define('created', Date); + x.define('age', Number, true); + x.define('note', String, true); + + q.setValidate(function(name, value) { + assert.ok((name === 'name' && value.length === 0) || (name === 'arr' && value.length === 2), 'SchemaBuilderEntity.validation() 1'); + }); + + x.setValidate(function(name, value, path, model) { + if (!path.startsWith('x.') && path.indexOf('ref.') === -1) + assert.ok((name === 'age' && value > 22) || (name === 'note' && value.length > 3), 'SchemaBuilderEntity.validation() 2'); + }); + + var qi = q.create(); + assert.ok(qi.created.format('yyyyMMddHHmmss') === F.datetime.format('yyyyMMddHHmmss'), 'A problem with problem a default value of date'); + + var xi = x.create(); + xi.age = 30; + xi.note = 'Peter'; + qi.arr.push(xi); + + xi = x.create(); + xi.age = 23; + xi.note = 'Jano'; + qi.arr.push(xi); + + qi.$validate(); + + // Relations test + qi = q.make({ ref: xi, arr:[xi,xi] }); + xi.note = 'Ivan'; + assert.ok(qi.ref.note === 'Ivan', 'schema relations'); + + var Cat = NEWSCHEMA('test', 'Cat'); + Cat.define('id', Number); + Cat.define('name', String); + Cat.define('age', Number); + + // Performance test + var instanceCount = 80000; + var cats = []; + + //var memwatch = require('memwatch-next'); + //var hd = new memwatch.HeapDiff(); + + var __start = (new Date()).getTime(); + for (var i=0; i obj[n] = 'total fraMEWOrk'); + obj.zip = '83102'; + 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'); + assert.ok(res.upper10 === 'TOTAL FRAM', 'SchemaBuilder: Upper(10)'); + assert.ok(res.zip === '83102', 'SchemaBuilder: Zip'); + 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 = '349393'; + obj.base64 = 'adlajkd'; + + res = schema.make(obj); + 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($) { + $.model.counter = 1; + $.callback(); + }); + + schema.addHook('1', function($) { + $.model.counter++; + $.callback(); + }); + + schema.addHook('1', function($) { + $.model.counter++; + $.callback(); + }); + + schema.addHook('1', function($) { + $.model.counter++; + $.callback(); + }); + + schema.hook('1', null, null, function(err, response) { + assert.ok(response.counter === 4, 'Problem with hooks'); + }); + }); + + 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.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: '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'); + }); + + }); +} + +function test_ErrorBuilder() { + var name = 'ErrorBuilder: '; + + builders.ErrorBuilder.addTransform('custom', function() { + assert.ok(this.hasError(), name + 'transform context'); + return this.items.length; + }); + + var builder = new builders.ErrorBuilder(); + + builder.add('name'); + assert.ok(builder.items[0].name === 'name' && builder.items[0].error === '@', name + 'add'); + builder.add('age', 'only number'); + assert.ok(builder.items[1].name === 'age' && builder.items[1].error === 'only number', name + 'add (custom message)'); + + builder.remove('age'); + assert.ok(typeof(builder.items[1]) === 'undefined', name + 'remove'); + assert.ok(builder.hasError(), name + 'hasError'); + + builder = new builders.ErrorBuilder(function(name) { + return name; + }); + + builder.add('name'); + builder.prepare(); + assert.ok(builder.items[0].error === 'name', name + 'prepare'); + + builder.clear(); + builder.add('name'); + assert.ok(builder.output(true) === '[{"name":"name","error":"name"}]', name + 'json'); + + builder.add(new builders.ErrorBuilder().add('age')); + assert.ok(builder.output(true) === '[{"name":"name","error":"name"},{"name":"age","error":"age"}]', name + 'add(ErrorBuilder)'); + assert.ok(builder.read('name') === 'name', name + 'read()'); + assert.ok(builder.hasError('name'), name + 'hasError(name)'); + + builder.replace('name', 'FET'); + assert.ok(builder.read('name') === 'FET', name + 'replace()'); + + builder.setTransform('default'); + assert.ok(builder.transform('custom') === 2, name + 'transform()'); + + builders.ErrorBuilder.setDefaultTransform('custom'); + builder = new builders.ErrorBuilder(); + + builder.add('error', new Error('some error')); + builder.add('name'); + + assert.ok(builder.errors === 2, name + 'transform()'); + + NEWSCHEMA('default').make(function(schema) { + schema.define('created', Date); + }); + + NEWSCHEMA('Async').make(function(schema) { + + var arr = []; + + schema.define('name', String); + + schema.addWorkflow('1', function($) { + arr.push('workflow1'); + $.model.$next('workflow', '2'); + $.callback(); + }); + + schema.addWorkflow('2', function($) { + arr.push('workflow2'); + $.callback(); + }); + + schema.addWorkflow('3', function($) { + arr.push('workflow3'); + $.callback(); + }); + + schema.addTransform('1', function($) { + arr.push('transform1'); + $.model.$next('transform', '2'); + $.model.$push('transform', '4'); + $.callback(); + }); + + schema.addTransform('2', function($) { + arr.push('transform2'); + $.callback(); + }); + + schema.addTransform('3', function($) { + arr.push('transform3'); + $.callback(); + }); + + schema.addTransform('4', function($) { + arr.push('transform4'); + $.callback(); + }); + + var model = schema.create(); + model.name = 'Peter'; + + var async = model.$async(function() { + assert.ok(arr.indexOf('workflow2') === 1, 'SchemaBuilderEntit.$next()'); + assert.ok(arr.indexOf('transform2') === 4, 'SchemaBuilderEntit.$next()'); + assert.ok(arr.pop() === 'transform4', 'SchemaBuilderEntit.$push()'); + }); + + async.$workflow('1'); + async.$workflow('3'); + async.$transform('1'); + async.$transform('3'); + }); + + NEWSCHEMA('Repository').make(function(schema) { + + schema.addWorkflow('1', function($) { + $.model.$repository('valid', true); + $.callback(); + }); + + schema.addWorkflow('2', function($) { + $.callback(); + }); + + var model = schema.create(); + + model.$async(function(err) { + assert.ok(model.$repository('valid') === true, 'SchemaBuilder.$repository()'); + }).$workflow('1').$workflow('2'); + }); + + + NEWSCHEMA('Output').make(function(schema) { + + schema.addWorkflow('1', function($) { + $.callback(1); + }); + + schema.addWorkflow('2', function($) { + $.model.$output(); + $.callback(2); + }); + + schema.addWorkflow('3', function($) { + $.callback(3); + }); + + var model = schema.create(); + + model.$async(function(err, response) { + assert.ok(response === 2, 'SchemaBuilderEntity.$output()'); + }).$workflow('1').$workflow('2').$workflow('3'); + }); + +} + +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($) { + $.callback(SUCCESS(true, $.value)); + }); + + NEWOPERATION('testB', function($) { + $.error.push('bug'); + $.callback(); + }); + + OPERATION('testA', 123456, function(err, response) { + assert.ok(err === null, 'OPERATIONS: errors'); + assert.ok(response.success && response.value === 123456, 'OPERATIONS: response'); + }); + + OPERATION('testB', function(err, response) { + 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(); +test_UrlBuilder(); +test_Schema(); +test_ErrorBuilder(); +test_Operations(); +test_Convertors(); + +console.log('================================================'); +console.log('success - OK'); +console.log('================================================'); +console.log(''); + +setTimeout(function() {}, 1000); + +process.on('uncaughtException', function(err) { + console.error(err); + process.exit(1); +}); \ No newline at end of file diff --git a/test/test-css.js b/test/test-css.js new file mode 100755 index 000000000..89e7e88ae --- /dev/null +++ b/test/test-css.js @@ -0,0 +1,56 @@ +var assert = require('assert'); +var internal = require('../internal'); + +var buffer = []; +buffer.push('/*auto*/'); +buffer.push('b{border-radius:1px}'); +buffer.push('a{border-radius:1px 2px 3px 4px}'); +buffer.push('a{text-overflow:ellipsis}'); +buffer.push('span{opacity:0;}'); +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,#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'); + +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 !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"!important}div{border-radius:4px}', 'CSS variables'); + +buffer = []; +buffer.push('@import url(\'font.css\');'); +buffer.push('div {'); +buffer.push(' b { color: red; }'); +buffer.push(' span { color: red; }'); +buffer.push(' div { color: red }'); +buffer.push(' div .blue { color: blue; }'); +buffer.push('}'); +buffer.push('@media(max-width:960px){'); +buffer.push(' b { color: red; }'); +buffer.push(' div {'); +buffer.push(' b { color: red; }'); +buffer.push(' span { color: red; }'); +buffer.push(' div { color: red }'); +buffer.push(' div .blue { color: blue; }'); +buffer.push(' }'); +buffer.push('}'); + +assert.ok(internal.compile_css(buffer.join("\n")) === "@import url('font.css');div b{color:red}div span{color:red}div div{color:red}div div .blue{color:blue}@media(max-width:960px){b{color:red}div b{color:red}div span{color:red}div div{color:red}div div .blue{color:blue}}", "CSS nested ordering"); + +console.log('================================================'); +console.log('success - OK'); +console.log('================================================'); +console.log(''); + +process.on('uncaughtException', function(err) { + console.error(err); + process.exit(1); +}); \ No newline at end of file diff --git a/test/test-framework-debug.js b/test/test-framework-debug.js new file mode 100755 index 000000000..e8f80a714 --- /dev/null +++ b/test/test-framework-debug.js @@ -0,0 +1,987 @@ +var utils = require('../utils'); +var assert = require('assert'); +var framework = require('../index'); +var url = 'http://127.0.0.1:8001/'; +var errorStatus = 0; +var max = 100; + +framework.onCompileView = function(name, html) { + return html + 'COMPILED'; +}; + +framework.onLocale = function(req) { + return !req.url.startsWith('/abc') ? 'sk' : ''; +}; + +framework.on('ready', function() { + var t = framework.worker('test'); + var a = false; + t.on('message', function(msg) { + if (msg === 'assert') + a = true; + }); + 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'); +}); + +AUTH(function($) { + $.req.user = { alias: 'Peter Širka' }; + $.req.session = { ready: true }; + $.next($.req.url !== '/unauthorize/'); +}); + +framework.onError = function(error, name, uri) { + + if (errorStatus === 0) { + console.log(error, name, uri); + console.log(error.stack); + framework.stop(); + return; + } + + if (errorStatus === 1) { + assert.ok(error.toString().indexOf('not found') !== -1, 'view: not found problem'); + errorStatus = 0; + return; + } +}; + +function end() { + console.log(''); + console.log('Requests count:', framework.stats.request.request); + console.log(''); + console.log('================================================'); + console.log('success - OK'); + console.log('================================================'); + console.log(''); + framework.stop(); +} + +function test_controller_functions(next) { + 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); + next(); + }); +} + +function test_view_functions(next) { + utils.request(url + 'views/', ['get'], function(error, data, code, headers) { + + if (error) + assert.ok(false, 'test_view_functions: ' + error.toString()); + assert.ok(data === '{"r":true}', 'json'); + next(); + }); +} + +function test_view_error(next) { + errorStatus = 1; + utils.request(url + 'view-notfound/', ['get'], function(error, data, code, headers) { + + if (error) + assert.ok(false, 'test_view_error: ' + error.toString()); + + next(); + }); +} + +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) + throw error; + assert.ok(code === 200, 'CORS, problem with "*" origin'); + complete(); + }, null, { 'origin': 'https://www.totaljs.com' }); + }); + + async.await('cors 2', function(complete) { + utils.request(url + '/cors/origin-not/', ['options'], function(error, data, code, headers) { + if (error) + throw error; + assert.ok(code === 404, 'CORS, problem with origin (origin is not valid)'); + complete(); + }, null, { 'origin': 'https://www.totaljs.com' }); + }); + + async.await('cors 3', function(complete) { + utils.request(url + '/cors/origin-not/', ['options'], function(error, data, code, headers) { + if (error) + throw error; + assert.ok(code === 200, 'CORS, problem with origin (valid origin)'); + complete(); + }, null, { 'origin': 'https://www.totajs.com' }); + }); + + async.await('cors asterix / wildcard', function(complete) { + utils.request(url + '/api/whatever/you/need/', ['options'], function(error, data, code, headers) { + if (error) + throw error; + assert.ok(code === 200, 'CORS, problem with origin (wildcard routing)'); + complete(); + }, null, { 'origin': 'https://www.totajs.com' }); + }); + + async.await('cors headers', function(complete) { + utils.request(url + '/cors/headers/', ['options'], function(error, data, code, headers) { + if (error) + throw error; + + // "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'] === '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': 'https://www.totajs.com' }); + });*/ + + async.await('options', function(complete) { + utils.request(url + 'options/', ['options'], function(error, data, code, headers) { + if (error) + throw error; + assert.ok(data === 'OPTIONS', 'OPTIONS method problem'); + complete(); + }); + }); + + async.await('html compressor', function(complete) { + utils.request(url + 'html-compressor/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === '

    a b c d

    Price 30 €
    Name: Peter
    Name: Peter
    Price: 1000 1 000.00
    1 3
    Name: Peter
    ', 'HTML compressor'); + complete(); + }); + }); + + async.await('html nocompress', function(complete) { + utils.request(url + 'html-nocompress/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data.indexOf('
    \nA\n
    ') !== -1 || data.indexOf('
    \r\nA\r\n
    ') !== -1, 'HTML nocompress'); + complete(); + }); + }); + + async.await('0', function(complete) { + utils.request(url + 'share/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert.ok(data === 'OK', 'controller view directory'); + complete(); + }); + }); + + async.await('a', function(complete) { + utils.request(url + 'a/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + complete(); + }); + }); + + async.await('a/aaa', function(complete) { + utils.request(url + 'a/aaa/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + complete(); + }); + }); + + async.await('sync', function(complete) { + utils.request(url + 'sync/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert.ok(data === 'TEST', 'generator problem'); + complete(); + }); + }); + + async.await('a/b', function(complete) { + utils.request(url + 'c/b/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + complete(); + }); + }); + + async.await('router', function(complete) { + utils.request(url + 'routeto/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'dilino gadzo', 'problem with controller.routeTo()'); + complete(); + }); + }); + + async.await('mobile - 1', function(complete) { + utils.request(url + 'mobile/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(headers['vary'] === 'Accept-Encoding, User-Agent', 'mobile device user-agent problem 1'); + assert(data !== 'X', 'mobile device routing problem 1'); + complete(); + }); + }); + + async.await('mobile - 2', function(complete) { + utils.request(url + 'mobile/?ok=true', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(headers['vary'] === 'Accept-Encoding, User-Agent', 'mobile device user-agent problem 2'); + assert(data === 'X', 'mobile device routing problem 2'); + complete(); + }, null, { 'user-agent': 'bla bla iPad bla' }); + }); + + async.await('robot - 1', function(complete) { + utils.request(url + '', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'ROBOT', 'robot routing problem 1'); + complete(); + }, null, { 'user-agent': 'I am Crawler' }); + }); + + async.await('robot - 2', function(complete) { + utils.request(url + '', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data !== 'ROBOT', 'robot routing problem 2'); + complete(); + }, null, { 'user-agent': 'Chrome' }); + }); + + async.await('binary', function(complete) { + utils.request(url + 'binary/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'čťž', 'binary'); + complete(); + }); + }); + + async.await('localize', function(complete) { + utils.request(url + 'templates/localization.html', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === '###preklad###', 'file localization'); + complete(); + }); + }); + + async.await('rest GET', function(complete) { + utils.request(url + 'rest/', ['get'], null, function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'GET', 'REST - GET'); + complete(); + }); + }); + + async.await('rest HEAD', function(complete) { + utils.request(url + 'rest/', ['head'], null, function(error, data, code, headers) { + if (error) + throw error; + assert(data.connection === 'close', 'REST - HEAD'); + complete(); + }); + }); + + async.await('rest DELETE', function(complete) { + utils.request(url + 'rest/', ['delete'], null, function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'DELETE', 'REST - DELETE'); + complete(); + }); + }); + + async.await('rest POST', function(complete) { + utils.request(url + 'rest/', ['post', 'json'], { success: true }, function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'POST', 'REST - POST (JSON)'); + complete(); + }); + }); + + async.await('rest PUT', function(complete) { + utils.request(url + 'rest/', ['put', 'json'], { success: true }, function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'PUT', 'REST - PUT (JSON)'); + complete(); + }); + }); + + async.await('translate', function(complete) { + utils.request(url + 'translate/?language=', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === '---translate---######', 'translate problem (EN)'); + utils.request(url + 'translate/?language=sk', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === '---preklad---###preklad###', 'translate problem (SK)'); + complete(); + }); + }); + }); + + async.await('custom', function(complete) { + utils.request(url + 'custom/route/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'CUSTOM', 'custom route problem'); + complete(); + }); + }); + + async.await('views in modules', function(complete) { + utils.request(url + 'view-in-modules/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'VIEW IN MODULES', 'Problem with opened path in views.'); + complete(); + }); + }); + + async.await('sitemap routing 1', function(complete) { + utils.request(url + 'abcdefgh/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === '#DEFAULT#', 'Sitemap routing and localization 1'); + complete(); + }); + }); + + async.await('sitemap routing 2', function(complete) { + utils.request(url + 'abcdefgh-sk/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === '#SK#', 'Sitemap routing and localization 2'); + complete(); + }); + }); + + async.await('asterix', function(complete) { + utils.request(url + 'app/a/b/c/d', ['get'], function(error, data, code, headers) { + assert(data === 'ASTERIX', 'asterix routing problem'); + if (error) + throw error; + complete(); + }); + }); + + async.await('a/b/c/', function(complete) { + utils.request(url + 'a/b/c/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + complete(); + }); + }); + + async.await('prefix -- 1', function(complete) { + utils.request(url + 'prefix1/test/', ['get'], function(error, data) { + if (error) + throw error; + assert.ok(data === 'PREFIX1TEST', 'Group + Prefix 1'); + complete(); + }); + }); + + 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 === '
    PACKAGELAYOUT
    PACKAGEVIEW
    ', 'package view problem'); + complete(); + }); + }); + + async.await('subshare', function(complete) { + utils.request(url + 'sub/share/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'SUBSHARE', 'problem with controller in subdirectory.'); + complete(); + }); + }); + + async.await('logged', function(complete) { + utils.request(url + 'logged/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + complete(); + }); + }); + + async.await('unauthorize', function(complete) { + utils.request(url + 'unauthorize/', ['get'], function(error, data, code, headers) { + assert.ok(data === 'UNAUTHORIZED', 'unauthorize flag problem'); + if (error) + throw error; + complete(); + }); + }); + + async.await('timeout', function(complete) { + utils.request(url + 'timeout/', ['get'], function(error, data, code, headers) { + assert(data === '408', 'timeout problem'); + complete(); + }); + }); + + async.await('http', function(complete) { + utils.request(url + 'http/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'HTTP', 'HTTP flag routing problem'); + complete(); + }); + }); + + async.await('get', function(complete) { + utils.request(url + 'get/?name=total&age=30&page=30', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === '{"name":"total","age":"30","page":30}', 'get'); + complete(); + }); + }); + + async.await('post-raw', function(complete) { + utils.request(url + 'post/raw/', ['post', 'raw'], 'SALAMA', function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'SALAMA', 'post-raw'); + complete(); + }); + }); + + async.await('post-schema-filter', function(complete) { + 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":"age","prefix":"age"}]', 'schema filter'); + complete(); + }); + }); + + async.await('post-schema', function(complete) { + utils.request(url + 'post/schema/', ['post'], 'name=Peter123456789012345678901234567890#', function(error, data, code, headers) { + if (error) + throw error; + assert(data === '{"name":"Peter12345","type":"schema"}', 'post-schema'); + complete(); + }); + }); + + async.await('post-schema-error', function(complete) { + utils.request(url + 'post/schema/', ['post'], 'age=Peter123456789012345678901234567890#', function(error, data, code, headers) { + if (error) + throw error; + assert(data === '[{"name":"name","error":"default","path":"name","prefix":"name"}]', 'post-schema 2'); + complete(); + }); + }); + + async.await('post-json', function(complete) { + utils.request(url + 'post/json/', ['json', 'post'], { name: 'total.js' }, function(error, data, code, headers) { + if (error) + throw error; + assert(data === '{"name":"total.js","type":"json"}', 'post-json'); + complete(); + }); + }); + + async.await('post-xml', function(complete) { + utils.request(url + 'post/xml/', ['xml', 'post'], 'total.js20', function(error, data, code, headers) { + if (error) + throw error; + assert(data === '{"root.name":"total.js","root.page":20,"type":"xml"}', 'post-xml'); + complete(); + }); + }); + + async.await('post-parse', function(complete) { + utils.request(url + 'post/parse/?value=query', ['post'], { name: 'total.js' }, function(error, data, code, headers) { + if (error) + throw error; + assert(data === '{"name":"total.js","type":"parse"}', 'post-json'); + complete(); + }); + }); + + async.await('put-raw', function(complete) { + utils.request(url + 'put/raw/', ['put', 'raw'], 'SALAMA', function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'SALAMA', 'put-raw'); + complete(); + }); + }); + + async.await('put-json', function(complete) { + utils.request(url + 'put/json/', ['json', 'put'], { name: 'total.js' }, function(error, data, code, headers) { + if (error) + throw error; + assert(data === '{"name":"total.js","type":"json"}', 'put-json'); + complete(); + }); + }); + + async.await('put-xml', function(complete) { + utils.request(url + 'put/xml/', ['xml', 'put'], 'total.js', function(error, data, code, headers) { + if (error) + throw error; + assert(data === '{"root.name":"total.js","type":"xml"}', 'put-xml'); + complete(); + }); + }); + + async.await('put-parse', function(complete) { + utils.request(url + 'put/parse/', ['put'], { name: 'total.js' }, function(error, data, code, headers) { + if (error) + throw error; + assert(data === '{"name":"total.js","type":"parse"}', 'put-json'); + complete(); + }); + }); + + async.await('multiple GET', function(complete) { + utils.request(url + 'multiple/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'POST-GET-PUT-DELETE', 'multiple (GET)'); + complete(); + }); + }); + + async.await('multiple DELETE', function(complete) { + utils.request(url + 'multiple/', ['delete'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'POST-GET-PUT-DELETE', 'multiple (DELETE)'); + complete(); + }); + }); + + async.await('multiple POST', function(complete) { + utils.request(url + 'multiple/', ['post'], { name: 'total.js' }, function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'POST-GET-PUT-DELETE', 'multiple (POST)'); + complete(); + }); + }); + + async.await('multiple PUT', function(complete) { + utils.request(url + 'multiple/', ['put'], { name: 'total.js' }, function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'POST-GET-PUT-DELETE', 'multiple (PUT)'); + complete(); + }); + }); + + async.await('regexp OK', function(complete) { + utils.request(url + 'reg/exp/12345/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === '12345', 'regexp routing'); + complete(); + }); + }); + + async.await('regexp NO', function(complete) { + utils.request(url + 'reg/exp/a12345/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(code === 404, 'regexp routing (NO)'); + complete(); + }); + }); + + async.await('components routing', function(complete) { + utils.request(url + 'components/contactform/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'CONTACTFORM COMPONENTS', 'components: routing'); + complete(); + }); + }); + + 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) + throw error; + assert(code === 404, 'directory name as filename 1'); + complete(); + }); + }); + + async.await('static-file-notfound-because-directory2', function(complete) { + utils.request(url + 'directory.js', [], function(error, data, code, headers) { + if (error) + throw error; + assert(code === 404, 'directory name as filename 2'); + complete(); + }); + }); + + async.await('static-file', function(complete) { + utils.request(url + 'robots.txt', [], function(error, data, code, headers) { + if (error) + throw error; + assert(data === '/robots.txt', 'static file routing && res.send(STRING)'); + complete(); + }); + }); + + async.await('static-file-middleware', function(complete) { + utils.request(url + 'middleware.txt', [], function(error, data, code, headers) { + if (error) + throw error; + assert(JSON.parse(data).url === '/middleware.txt', 'static file routing with middleware && res.send(OBJECT)'); + complete(); + }); + }); + + async.await('static-file-status', function(complete) { + utils.request(url + 'status.txt', [], function(error, data, code, headers) { + if (error) + throw error; + assert(data === '404: Not Found', 'static file routing && res.send(NUMBER)'); + complete(); + }); + }); + + async.await('upload', function(complete) { + utils.upload([{ name: 'file', filename: ',;-test.txt', buffer: Buffer.from('dG90YWwuanMgaXMga2luZyBvZiB3ZWI=', 'base64') }], url + 'upload/', function(error, data, code, headers) { + assert(data === '{"name":",;-test.txt","length":25,"type":"text/plain"}', 'upload'); + complete(); + }); + }); + + async.await('cookie', function(complete) { + utils.request(url + 'cookie/', ['get'], function(error, data, code, headers) { + if (error) + throw error; + + var cookie = headers['set-cookie'].join(''); + assert(cookie.indexOf('cookie1=1;') !== -1 && cookie.indexOf('cookie2=2;') !== -1 && cookie.indexOf('cookie3=3;') !== -1, 'Cookie problem.'); + assert(cookie.indexOf('cookieR=O;') === -1 && cookie.indexOf('cookieR=N;') !== -1 && cookie.indexOf('cookieR=') === cookie.lastIndexOf('cookieR='), 'Two cookies with same name'); + complete(); + }, { a: 1, b: 2, c: 3 }); + }); + + async.await('Authorize', function(complete) { + utils.request(url + 'a/b/c/d/authorize/', [], function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'authorize', 'Authorize problem.'); + complete(); + }); + }); + + async.await('mapping', function(complete) { + utils.request(url + 'fet.txt', [], function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'TEST', 'static file mapping'); + complete(); + }); + }); + + async.await('merge package', function(complete) { + // mergepackage2 is from versions + utils.request(url + 'mergepackage2.js', [], function(error, data, code, headers) { + if (error) + throw error; + assert(data.indexOf('console.log(\'test\');') !== -1, 'merge package'); + complete(); + }); + }); + + if (DEBUG) { + async.await('merge directory', function(complete) { + // mergepackage2 is from versions + utils.request(url + 'mergedirectory.js', [], function(error, data, code, headers) { + if (error) + throw error; + assert(data.indexOf('block.js') !== -1 && data.indexOf('test.js') !== -1, 'merge directory'); + complete(); + }); + }); + } + + async.await('merge-blocks-a', function(complete) { + utils.request(url + 'merge-blocks-a.js', [], function(error, data, code, headers) { + if (error) + throw error; + assert(data.indexOf('var common=true,a=true;') !== -1, 'merge blocks - A'); + complete(); + }); + }); + + async.await('merge-blocks-b', function(complete) { + utils.request(url + 'merge-blocks-b.js', [], function(error, data, code, headers) { + if (error) + throw error; + assert(data.indexOf('var common=true,b=true;') !== -1, 'merge blocks - B'); + complete(); + }); + }); + + async.await('mapping-blocks-a', function(complete) { + utils.request(url + 'blocks-a.js', [], function(error, data, code, headers) { + if (error) + throw error; + assert(data.indexOf('var common=true,a=true;') !== -1, 'mapping blocks - A'); + complete(); + }); + }); + + async.await('mapping-blocks-b', function(complete) { + utils.request(url + 'blocks-b.js', [], function(error, data, code, headers) { + if (error) + throw error; + assert(data.indexOf('var common=true,b=true;') !== -1, 'mapping blocks - B'); + complete(); + }); + }); + + async.await('mapping-blocks-c', function(complete) { + utils.request(url + 'blocks-c.js', [], function(error, data, code, headers) { + if (error) + throw error; + assert(data.indexOf('var common=true,a=true,b=true;') !== -1, 'mapping blocks - C'); + complete(); + }); + }); + + async.await('virtual', function(complete) { + utils.request(url + 'virtual.txt', [], function(error, data, code, headers) { + if (error) + throw error; + assert(data === 'TEST', 'virtual directory problem'); + complete(); + }); + }); + + async.await('theme-green', function(complete) { + 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.'); + complete(); + }); + }); + + async.await('theme-green-merge', function(complete) { + 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'); + complete(); + }); + }); + + async.await('theme-green-map', function(complete) { + 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.'); + complete(); + }); + }); + + 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; + 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(); + }); + }); + + async.complete(function() { + next && next(); + }); +} + +function run() { + + if (max <= 0) { + + console.timeEnd('TEST'); + + assert.ok(framework.global.middleware > 0, 'middleware - middleware'); + assert.ok(framework.global.theme > 0, 'theme - initialization'); + assert.ok(framework.global.all > 0, 'middleware - global'); + assert.ok(framework.global.file > 0, 'middleware - file'); + assert.ok(framework.global.timeout > 0, 'timeout'); + + UNINSTALL('source', { uninstall: true }); + UNINSTALL('view', 'precompile._layout'); + + //framework.uninstall('precompile', 'precompile.homepage'); + framework.clear(); + + setTimeout(function() { + end(); + }, 2000); + return; + } + + max--; + test_controller_functions(function() { + test_view_functions(function() { + test_view_error(function() { + test_routing(function() { + run(); + }); + }); + }); + }); +} + +/* +var mem = require('memwatch'); + +mem.on('leak', function(info) { + console.log('LEAK ->', info); +}); + +mem.on('stats', function(info) { + console.log('STATS ->', JSON.stringify(info)); +}); +*/ +// framework.fs.create.view('fromURL', 'http://www.totaljs.com/framework/test.html'); + +framework.on('load', function() { + + F.merge('/mergepackage.js', '@testpackage/test.js'); + F.merge('/mergedirectory.js', '~' + F.path.public('js') + '*.js'); + + assert.ok(MODULE('supermodule').ok, 'load module from subdirectory'); + assert.ok(F.config['custom-config1'] === '1YES', 'custom configuration 1'); + assert.ok(F.config['custom-config2'] === '2YES', 'custom configuration 2'); + 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'); console.log('================================================'); console.log('success - OK'); console.log('================================================'); -console.log(''); \ No newline at end of file +console.log(''); + +process.on('uncaughtException', function(err) { + console.error(err); + process.exit(1); +}); \ No newline at end of file diff --git a/test/test-tests.js b/test/test-tests.js new file mode 100644 index 000000000..202ab84e3 --- /dev/null +++ b/test/test-tests.js @@ -0,0 +1 @@ +require('../index').http('debug test'); \ No newline at end of file diff --git a/test/test-tmp.js b/test/test-tmp.js new file mode 100644 index 000000000..717d744ae --- /dev/null +++ b/test/test-tmp.js @@ -0,0 +1,29 @@ +require('../index'); + +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 new file mode 100755 index 000000000..26e811538 --- /dev/null +++ b/test/test-utils.js @@ -0,0 +1,686 @@ +require('../index'); +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.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'); + assert.ok(dt.format('MMMM', 'sk') === 'Júl', 'localized month name 2'); + + dt = new Date(); + dt = dt.add('minute', 1); + dt = dt.add('seconds', 5); + + assert.ok('1 minute 5 seconds'.parseDateExpiration().format('mm:ss') === dt.format('mm:ss'), 'date expiration'); + + dt = '2010-01-01 12:05:10'.parseDate(); + /* + 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, 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)'); + assert.ok(dt.compare('2101-01-01'.parseDate()) === -1, 'date compare (later)'); + assert.ok(dt.compare(dt) === 0, 'date compare (same)'); + assert.ok(Date.compare(dt, dt) === 0, 'date compare (same, static)'); + + dt = '12:00:00'.parseDate(); + assert.ok(dt.compare(dt) === 0, 'time compare (same)'); +} + +// test: number prototype +function prototypeNumber() { + 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'); + assert.ok((10000).format() === '10 000', 'format/decimal: C'); + var number = 10.103435; + assert.ok(number.floor(2) === 10.10, 'floor number: 2 decimals'); + assert.ok(number.floor(4) === 10.1034, 'floor number: 4 decimals'); + assert.ok(number.floor(0) === 10, 'floor number: 0 decimals'); + assert.ok(number.hex() === 'A.1A7AB75643028', 'number to hex'); + assert.ok(number.add('10%', 0) === 1, 'add number: 1'); + assert.ok(number.add('+10%', 0) === 11, 'add number: 2'); + assert.ok(number.add('-10%', 0) === 9, 'add number: 3'); + assert.ok(number.add('*2', 0) === 20, 'add number: 4'); + assert.ok(number.add('*10%', 0) === 10, 'add number: 5'); + + number = 1024; + assert.ok(number.filesize() === '1 KB', 'filesize decimals: auto'); + assert.ok(number.filesize('MB') === '0 MB', 'filesize decimals: MB'); + assert.ok(number.filesize('GB') === '0 GB', 'filesize decimals: GB'); + assert.ok(number.filesize('TB') === '0 TB', 'filesize decimals: TB'); + + number = 1248576; + assert.ok(number.filesize() === '1.19 MB', 'filesize decimals: auto'); + assert.ok(number.filesize('MB') === '1.19 MB', 'filesize decimals: MB'); + assert.ok(number.filesize('TB') === '0 TB', 'filesize decimals: TB'); + assert.ok(number.filesize('KB') === '1 219.31 KB', 'filesize decimals: KB'); + + var num = 5; + var count = 0; + + num.async(function(index, next) { + count += index; + setTimeout(next, 100); + }, function() { + assert.ok(count === 15, 'Number.async() problem'); + }); + +} + +// test: string prototype +function prototypeString() { + var str = ' total.js '; + assert.ok(str.trim() === 'total.js', 'string.trim()'); + assert.ok(str.contains(['t', 'X']), 'string.contains(all=false)'); + assert.ok(str.contains(['t', 'X'], true) === false, 'string.contains(all=true)'); + assert.ok('{0}={1}'.format('name', 'value') === 'name=value', 'string.format()'); + assert.ok('total.js" '.encode() === '<b>total.js</b>"&nbsp;', 'string.encode()'); + assert.ok('<b>total.js</b>&nbsp;'.decode() === 'total.js ', 'string.decode()'); + assert.ok(str.trim().replaceAt(5, ';') === 'total;js', 'string.replaceAt()'); + + str = ' A PeTer Širka Je krááály. '; + + assert.ok(str.toSearch() === 'a peter sirka je krali', 'string.toSearch()'); + + str = 'Great function.'; + + assert.ok(str.startsWith('Great'), 'string.startsWith()'); + assert.ok(str.startsWith('GrEAT', true), 'string.startsWith(ignoreCase)'); + assert.ok(str.startsWith('asdljkaslkdj aslkdjalsdjlasdjlkasdjlasjdlaj') === false, 'string.startsWith() - large string'); + + assert.ok(str.endsWith('ion.'), 'string.endsWith()'); + assert.ok(str.endsWith('ION.', true), 'string.endsWith(ignoreCase)'); + assert.ok(str.endsWith('asdljkaslkdj aslkdjalsdjlasdjlkasdjlasjdlaj') === false, 'string.endsWith() - large string'); + + str = 'abcdefgh ijklmnop'; + assert.ok(str.max(5, '---') === 'ab---', 'string.maxLength(5, "---")'); + assert.ok(str.max(5) === 'ab...', 'string.maxLength(5)'); + + assert.ok(str.isJSON() === false, 'string.isJSON()'); + assert.ok('[]'.isJSON() === true, 'string.isJSON([])'); + assert.ok('{}'.isJSON() === true, 'string.isJSON({})'); + assert.ok(' {} '.isJSON() === true, 'string.isJSON({})'); + assert.ok('"'.isJSON() === false, 'string.isJSON(")'); + assert.ok('""'.isJSON() === true, 'string.isJSON("")'); + assert.ok('12'.isJSON() === true, 'string.isJSON(12)'); + 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()'); + + var dt = new Date(); + var ts = dt.getTime(); + + assert.ok(JSON.stringify({ date: dt }).parseJSON(true).date.getTime() === ts, 'string.parseJSON(true) - problem with Date parsing'); + + str = 'google.sk'; + assert.ok(str.isURL() === false, 'string.isURL(): ' + str); + + str = 'google'; + assert.ok(str.isURL() === false, 'string.isURL(): ' + str); + + str = 'http://www.google.com'; + assert.ok(str.isURL() === true, 'string.isURL(): ' + str); + + str = 'http://127.0.0.1:8000'; + assert.ok(str.isURL() === true, 'string.isURL(): ' + str); + + str = 'https://mail.google.com'; + assert.ok(str.isURL() === true, 'string.isURL(): ' + str); + + str = 'petersirka@gmail.com'; + assert.ok(str.isEmail() === true, 'string.isEmail(): ' + str); + + str = 'petersirka@gmail'; + assert.ok(str.isEmail() === false, 'string.isEmail(): ' + str); + + str = 'anything@addons.business'; + assert.ok(str.isEmail() === true, 'string.isEmail(): ' + str); + + str = 'a@a.a'; + assert.ok(str.isEmail() === false, 'string.isEmail(): ' + str); + + str = '255'; + assert.ok(str.parseInt() === 255, 'string.parseInt(): ' + str); + + str = '-255'; + assert.ok(str.parseInt() === -255, 'string.parseInt(): ' + str); + + str = ' 255 '; + assert.ok(str.parseInt() === 255, 'string.parseInt(): ' + str); + + str = ' a '; + assert.ok(str.parseInt() === 0, 'string.parseInt(): ' + str); + + str = ' a '; + assert.ok(str.parseInt(-1) === -1, 'string.parseInt(): ' + str + ' / default'); + + str = ''; + assert.ok(str.parseInt() === 0, 'string.parseInt(): ' + str); + + str = 'Abc 334'; + assert.ok(str.parseInt2() === 334, 'string.parseInt2(): ' + str); + + str = 'Abc 334.33'; + assert.ok(str.parseFloat2() === 334.33, 'string.parseFloat2(): ' + str); + + str = ''; + assert.ok(str.parseInt2() === 0, 'string.parseInt2(): ' + str); + + str = ''; + assert.ok(str.parseFloat2() === 0, 'string.parseFloat2(): ' + str); + + str = '255.50'; + assert.ok(str.parseFloat() === 255.5, 'string.parseFloat(): ' + str); + + str = ' 255,50 '; + assert.ok(str.parseFloat() === 255.5, 'string.parseFloat(): ' + str); + + str = ' ,50 '; + assert.ok(str.parseFloat() === 0.50, 'string.parseFloat(): ' + str); + + str = '.50'; + assert.ok(str.parseFloat() === 0.50, 'string.parseFloat(): ' + str); + + str = '.'; + assert.ok(str.parseFloat() === 0, 'string.parseFloat(): ' + str); + + str = '123456'; + assert.ok(str.sha1() === '7c4a8d09ca3762af61e59520943dc26494f8941b', 'string.sha1(): ' + str); + assert.ok(str.sha256() === '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', 'string.sha256(): ' + str); + assert.ok(str.md5() === 'e10adc3949ba59abbe56e057f20f883e', 'string.md5(): ' + str); + assert.ok(str.sha512() === 'ba3253876aed6bc22d4a6ff53d8406c6ad864195ed144ab5c87621b6c233b548baeae6956df346ec8c17f5ea10f35ee3cbc514797ed7ddd3145464e2a0bab413', 'string.sha512(): ' + str); + + var value = str.encrypt('key', false); + assert.ok(value.decrypt('key') === str, 'string.encode() & string.decode() = unique=false: ' + str); + + value = str.encrypt('key', true); + assert.ok(value.decrypt('key') === str, 'string.encode() & string.decode() = unique=true: ' + str); + + str = 'data:image/gif;base64,R0lGODdhAQABAIAAAF5eXgAAACwAAAAAAQABAAACAkQBADs='; + assert.ok(str.base64ContentType() === 'image/gif', 'string.base64ContentType(): ' + str); + + str = 'ľščťŽýÁíéäôúáűő'; + assert.ok(str.removeDiacritics() === 'lsctZyAieaouauo', 'string.removeDiacritics(): ' + str); + + str =''; + assert.ok(str.indent(4) === ' ', 'string.indent(4): ' + str); + assert.ok(str.indent(4, '-') === '----', 'string.indent(4, "-"): ' + str); + + str = '12'; + assert.ok(str.isNumber() === true, 'string.isNumber(): ' + str); + str = '13a'; + assert.ok(str.isNumber() === false, 'string.isNumber(): ' + str); + str = '13 '; + assert.ok(str.isNumber() === false, 'string.isNumber(): ' + str); + str = '13.34'; + assert.ok(str.isNumber(true) === true, 'string.isNumber(true): ' + str); + str = '13,34'; + assert.ok(str.isNumber(true) === true, 'string.isNumber(true): ' + str); + + str = '12345'; + assert.ok(str.padLeft(10) === ' 12345', 'string.padLeft(10): ' + str); + assert.ok(str.padLeft(5) === '12345', 'string.padLeft(10): ' + str); + assert.ok(str.padLeft(10, '-') === '-----12345', 'string.padLeft(10, "-"): ' + str); + assert.ok(str.padRight(10) === '12345 ', 'string.padRight(10): ' + str); + assert.ok(str.padRight(5) === '12345', 'string.padRight(10): ' + str); + assert.ok(str.padRight(10, '-') === '12345-----', 'string.padRight(10, "-"): ' + str); + + var num = 12345; + assert.ok(num.padLeft(10) === '0000012345', 'number.padLeft(10): ' + num); + assert.ok(num.padRight(10) === '1234500000', 'number.padRight(10): ' + num); + + str = 'Peter Širka Linker & - you known'; + assert.ok(str.linker() === 'peter-sirka-linker-you-known', 'string.link(): ' + str); + assert.ok(str.linker(11) === 'peter-sirka', 'string.link(): ' + str); + assert.ok(str.slug() === 'peter-sirka-linker-you-known', 'string.slug(): ' + str); + assert.ok(str.slug(11) === 'peter-sirka', 'string.slug(): ' + str); + + assert.ok('total Js'.capitalize() === 'Total Js', 'string.capitalize()'); + assert.ok('totaljs'.isAlphaNumeric(), 'string.isAlphaNumeric(true)'); + assert.ok('total js'.isAlphaNumeric() === false, 'string.isAlphaNumeric(false)'); + + str = '// Configuration\nname : total.js\nage : 29\n// comment1 : comment1\n# comment2 : comment2\ndebug : false'; + assert.ok(JSON.stringify(str.parseConfig({ comment3: 'comment3' })) === '{"comment3":"comment3","name":"total.js","age":"29","debug":"false"}', 'String.parseConfig()'); + + 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() { + + var arr = [ + { name: '1', value: 10 }, + { name: '2', value: 20 }, + { name: '3', value: 30 }, + { name: '4', value: 40 }, + { name: '5', value: 50 } + ]; + + var obj = arr.toObject('name'); + + assert.ok(obj['3'].value === 30, 'array.toObject(with name)'); + + assert.ok(arr.findItem(function(o) { return o.name === '4'; }).value === 40, 'array.find()'); + assert.ok(arr.findItem(function(o) { return o.name === '6'; }) === null, 'array.find(): null'); + assert.ok(arr.findItem('name', '4').value === 40, 'array.find(inline)'); + assert.ok(arr.findItem('name', '6') === null, 'array.find(inline): null'); + + arr = arr.remove(function(o) { + return o.value > 30; + }); + + assert.ok(arr.length === 3, 'array.remove()'); + arr = arr.remove('value', 30); + assert.ok(arr.length === 2, 'array.remove(inline)'); + + arr = [1, 2, 3, 4, 5]; + obj = arr.toObject(); + assert.ok(obj[3] === true, 'array.toObject(without name)'); + assert.ok(arr.skip(3).join('') === '45', 'array.skip()'); + assert.ok(arr.take(3).join('') === '123', 'array.take()'); + + assert.ok(arr.orderBy(false)[0] === 5, 'array.orderBy()'); + + arr = [{}, {}, {}]; + assert.ok(arr.extend({ $index: 1 })[0].$index === 1, 'array.extend(obj)'); + + var c = arr.extend(function(item, index) { + item.$index = index; + return item; + }); + + assert.ok(c[0].$index === 0, 'array.extend(function)'); + + var counter = arr.length; + + arr.wait(function(item, next) { + counter--; + next(); + }, function() { + assert.ok(counter === 0 && counter !== arr.length, 'array.wait(remove = false)'); + + arr.wait(function(item, next) { + next(); + }, function() { + assert.ok(arr.length === 0, 'array.wait(remove = true)'); + }, true); + + }); + + arr = [1, 2, 3, 4, 5, 6]; + arr.limit(3, function(item, next, beg, end) { + if (beg === 0 && end === 3) + 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(); + }); + + //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]; + + assert.ok(arr.unique().join(',') === '1,2,3,4', 'array.unique(2)'); + + var b = [{ name: 'Peter' }, { name: 'Janko' }, { name: 'Peter' }, { name: 'Lucia' }, { name: 'Lucia' }, { name: 'Peter' }]; + assert.ok(JSON.stringify(b.unique('name')) === '[{"name":"Peter"},{"name":"Janko"},{"name":"Lucia"}]', 'array.unique(property)'); + + var asyncarr = []; + var asyncounter = 0; + + asyncarr.push(function(next) { + asyncounter++; + next(); + }); + + asyncarr.push(function(next) { + asyncounter++; + next(); + }); + + asyncarr.async(function() { + assert.ok(asyncounter === 2, 'array.async(classic)'); + }); +} + +function other() { + var obj = {}; + + assert.ok(utils.isEmpty({}), 'utils.isEmpty() - is empty'); + assert.ok(!utils.isEmpty({ a: 1 }), 'utils.isEmpty() - not empty'); + + assert.ok(JSON.stringify(utils.extend({ id: 1 })) === '{"id":1}', 'utils.extend() - undefined'); + + var anonymous = { name: 'Peter', age: 25, arr: [1, 2, 3] }; + assert.ok(!obj.name, 'utils.copy(2)'); + assert.ok(utils.copy({ name: 'Janko' }).name === 'Janko', 'utils.copy(1)'); + + utils.extend(obj, anonymous); + obj.arr.push(4); + assert.ok(obj.arr.length !== anonymous.length, 'utils.copy(2)'); + assert.ok(obj.name === 'Peter' && obj.age === 25, 'utils.extend()'); + + 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); + + assert.ok(typeof(a.age) === 'undefined', 'utils.reduce()'); + assert.ok(typeof(b.age) === 'number', 'utils.reduce() - reverse'); + + assert.ok(utils.reduce([{ name: 'Peter', age: 27 }, { name: 'Lucia', age: 22 }], ['name'])[0].age === undefined, 'utils.reduce() - array'); + assert.ok(utils.reduce([{ name: 'Peter', age: 27 }, { name: 'Lucia', age: 22 }], ['name'], true)[0].name === undefined, 'utils.reduce() - array reverse'); + + var str = 'http://www.google.sk'; + assert.ok(utils.isRelative(str) === false, 'utils.isRelative(): ' + str); + + str = '/img/logo.jpg'; + assert.ok(utils.isRelative(str) === true, 'utils.isRelative(): ' + str); + + assert.ok(utils.isStaticFile(str) === true, 'utils.isStaticFile(): ' + str); + + str = '/logo/'; + assert.ok(utils.isStaticFile(str) === false, 'utils.isStaticFile(): ' + str); + + str = 'gif'; + assert.ok(utils.getContentType(str) === 'image/gif', 'utils.getContentType(): ' + str); + + str = '.jpg'; + assert.ok(utils.getContentType(str) === 'image/jpeg', 'utils.getContentType(): ' + str); + + str = '.xFx'; + assert.ok(utils.getContentType(str) === 'application/octet-stream', 'utils.getContentType(): ' + str); + + str = '/logo'; + assert.ok(utils.path(str) === '/logo/', 'utils.path(): ' + str); + + str = '/logo/'; + assert.ok(utils.path(str) === '/logo/', 'utils.path(): ' + str); + + assert.ok(utils.GUID(40).length === 40, 'utils.GUID(40)'); + + assert.ok(utils.encode('total.js" ') === '<b>total.js</b>"&nbsp;', 'utils.encode()'); + assert.ok(utils.decode('<b>total.js</b>&nbsp;') === 'total.js ', 'utils.decode()'); + + var result = utils.parseXML('
    Peter&JankoIta'lic
    '); + + assert.ok(result['div.b'] === 'Peter&Janko', 'XML Parser 1'); + assert.ok(result['div.i'] === 'Ita\'lic', 'XML Parser 2'); + assert.ok(result['div.i[]'].style === 'color:red', 'XML Parser Attributes'); + + result = utils.parseXML('OK'); + + obj = { a: ' 1 ', b: { a: ' 2 '}, c: [' 1 ', '2', [' 3', ' 5 ']]}; + utils.trim(obj); + assert.ok(JSON.stringify(obj) === '{"a":"1","b":{"a":"2"},"c":["1","2",["3","5"]]}', 'utils.trim()'); + + var async = new utils.Async(); + var value = []; + + async.on('error', function(err, name) { + console.log('ERROR', err, name); + }); + + async.await('0', function(next) { + value.push(9); + next(); + }); + + async.wait('1', '0', function(next) { + value.push(1); + next(); + }); + + async.wait('2', '1', function(next) { + setTimeout(function() { + value.push(2); + next(); + }, 2000); + }); + + async.wait('3', '2', function(next) { + value.push(3); + next(); + }); + + async.wait('4', '5', function(next) { + value.push(4); + next(); + }); + + async.wait('5', '3', function(next) { + value.push(5); + next(); + }); + + async.wait('6', '5', function(next) { + value.push(6); + next(); + }); + + async.wait('7', '6', function(next) { + value.push(7); + next(); + }); + + async.wait('8', '7', function(next) { + value.push(8); + next(); + }); + + async.await(function(next) { + next(); + }); + + async.await(function(next) { + next(); + }); + + async.complete(function() { + + value.sort(function(a, b) { + if (a > b) + return 1; + else + return -1; + }); + + assert.ok(value.join(',') === '1,2,3,4,5,6,7,8,9', 'async'); + }); + + utils.request('http://www.totaljs.com', ['get', 'dnscache'], function(err, data, code) { + assert.ok(code === 200, 'utils.request (success)'); + }).on('data', function(chunk, p) { + assert.ok(p === 0, 'utils.request (events)'); + }); + + utils.request('https://www.totaljs.com', ['get'], function(err, data, code) { + assert.ok(code === 200, 'utils.request (success)'); + }).on('data', function(chunk, p) { + assert.ok(p === 0, 'utils.request (events)'); + }); + + utils.download('http://www.totaljs.com/img/logo.png', ['get'], function(err, res) { + assert.ok(res.statusCode === 200, 'utils.download (success)'); + }).on('data', function(chunk, p) { + assert.ok(p === 100, 'utils.download (events)'); + }); + + utils.request('http://xxxxxxx.yyy', ['get'], null, function(err, data, code) { + assert.ok(err !== null, 'utils.request (error)'); + }); + + 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)'); + assert.ok(utils.getName('\\aaa\\bbb\\ccc\\dddd\\') === 'dddd', 'problem with getName (4)'); + + var indexer = 0; + + utils.wait(function() { + return indexer++ === 3; + }, function(err) { + assert(err === null, 'utils.wait()'); + }); + + utils.wait(NOOP, function(err) { + assert(err !== null, 'utils.wait() - timeout'); + }, 1000); + + var queue = 0; + + utils.queue('file', 2, function(next) { + setTimeout(function() { + queue++; + next(); + }, 300); + }); + + utils.queue('file', 2, function(next) { + setTimeout(function() { + queue--; + next(); + }, 300); + }); + + utils.queue('file', 2, function(next) { + setTimeout(function() { + assert.ok(queue === 0, 'utils.queue()'); + next(); + }, 300); + }); + + var a = { a: 1, b: 2, name: 'Peter', isActive: true, dt: new Date() }; + var b = { a: 1, b: 2, name: 'Peter', isActive: true, dt: new Date() }; + + assert.ok(utils.isEqual(a, b), 'utils.isEqual(1)'); + + b.isActive = false; + assert.ok(utils.isEqual(a, b) === false, 'utils.isEqual(2)'); + + b.name = 'Lucia'; + assert.ok(utils.isEqual(a, b, ['a', 'b']), 'utils.isEqual(3)'); + assert.ok(utils.minifyScript('var a = 1 ;') === 'var a=1;', 'JavaScript minifier'); + assert.ok(utils.minifyStyle('body { margin: 0 0 0 5px }') === 'body{margin:0 0 0 5px}', 'Style minifier'); + assert.ok(utils.minifyHTML('\nTEST\n') === 'TEST', 'HTML minifier'); + + var streamer = utils.streamer('\n', function(value, index) { + assert.ok(value.trim() === index.toString(), 'Streamer problem'); + }); + + streamer(Buffer.from('0')); + streamer(Buffer.from('\n1\n2\n')); + streamer(Buffer.from('3\n')); + streamer(Buffer.from('4\n')); + + streamer = utils.streamer('', '', function(value, index) { + assert.ok(value.trim() === '' + (index + 1) + '', 'Streamer problem 2'); + }); + + streamer(Buffer.from('aaaa 1 adsklasdlajsdlas jd 2')); + streamer(Buffer.from('aaaa 3 adsklasdlajsdlas jd 4')); + + 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('================================================'); +console.log('success - OK'); +console.log('================================================'); +console.log(''); + +process.on('uncaughtException', function(err) { + console.error(err); + process.exit(1); +}); \ No newline at end of file diff --git a/test/test.js b/test/test.js deleted file mode 100755 index 6cfb6c529..000000000 --- a/test/test.js +++ /dev/null @@ -1,226 +0,0 @@ -var utils = require('../utils'); -var assert = require('assert'); -var framework = require('../index'); -var http = require('http'); -var fs = require('fs'); - -var url = 'http://127.0.0.1:8001/'; -var errorStatus = 0; -var max = 100; - -framework.onAuthorization = function(req, res, flags, cb) { - req.user = { alias: 'Peter Širka' }; - req.session = { ready: true }; - cb(true); -}; - -framework.onError = function(error, name, uri) { - - if (errorStatus === 0) { - console.log(error, name, uri); - console.log(error.stack); - framework.stop(); - return; - } - - if (errorStatus === 1) { - assert.ok(error.toString().indexOf('not found') !== -1, 'view: not found problem'); - errorStatus = 2; - return; - } - - if (errorStatus === 2) { - assert.ok(error.toString().indexOf('not found') !== -1, 'template: not found problem'); - errorStatus = 3; - return; - } - - if (errorStatus === 3) { - assert.ok(error.toString().indexOf('not found') !== -1, 'content: not found problem'); - errorStatus = 0; - return; - } -}; - -function end() { - console.log('================================================'); - console.log('success - OK'); - console.log('================================================'); - console.log(''); - framework.stop(); -} - -function test_controller_functions(next) { - utils.request(url, 'GET', null, function(error, data, code, headers) { - - if (error) - assert.ok(false, 'test_controller_functions: ' + error.toString()); - - assert.ok(code === 404, 'controller: statusCode ' + code); - assert.ok(headers['etag'] === '123456:1', 'controller: setModified(etag)'); - assert.ok(headers['last-modified'].toString().indexOf('1984') !== -1, 'controller: setModified(date)'); - assert.ok(headers['expires'].toString().indexOf('1984') !== -1, 'controller: setExpires(date)'); - next(); - }); -} - -function test_view_functions(next) { - utils.request(url + 'views/', 'GET', null, function(error, data, code, headers) { - - if (error) - assert.ok(false, 'test_view_functions: ' + error.toString()); - - assert.ok(data === '{"r":true}', 'json'); - next(); - }); -}; - -function test_view_error(next) { - errorStatus = 1; - utils.request(url + 'view-notfound/', 'GET', null, function(error, data, code, headers) { - - if (error) - assert.ok(false, 'test_view_error: ' + error.toString()); - - next(); - }); -} - -function test_routing(next) { - - var async = new utils.Async(); - - async.await('0', function(complete) { - utils.request(url + 'share/', 'GET', null, function(error, data, code, headers) { - if (error) - throw error; - assert.ok(data === 'OK', 'controller view directory'); - complete(); - }); - }); - - async.await('a', function(complete) { - utils.request(url + 'a/', 'GET', null, function(error, data, code, headers) { - if (error) - throw error; - complete(); - }); - }); - - async.await('a/aaa', function(complete) { - utils.request(url + 'a/aaa/', 'GET', null, function(error, data, code, headers) { - if (error) - throw error; - complete(); - }); - }); - - async.await('a/b', function(complete) { - utils.request(url + 'c/b/', 'GET', null, function(error, data, code, headers) { - if (error) - throw error; - complete(); - }); - }); -/* - async.await('pipe', function(complete) { - utils.request(url + 'pipe/', 'GET', null, function(error, data, code, headers) { - if (error) - throw error; - assert.ok(data.toString('utf8').indexOf('telephone=no') !== -1, 'controller.pipe() / responsePipe() problem'); - complete(); - }); - }); -*/ - async.await('a/b/c/', function(complete) { - utils.request(url + 'a/b/c/', 'GET', null, function(error, data, code, headers) { - if (error) - throw error; - complete(); - }); - }); - - async.await('logged', function(complete) { - utils.request(url + 'logged/', 'GET', null, function(error, data, code, headers) { - if (error) - throw error; - complete(); - }); - }); - - async.await('timeout', function(complete) { - utils.request(url + 'timeout/', 'GET', null, function(error, data, code, headers) { - assert(data === '408', 'timeout problem'); - complete(); - }); - }); - - async.await('http', function(complete) { - utils.request(url + 'http/', 'GET', null, function(error, data, code, headers) { - if (error) - throw error; - assert(data === 'HTTP', 'HTTP flag routing problem'); - complete(); - }); - }); - - async.await('cookie', function(complete) { - utils.request(url + 'cookie/', 'GET', null, function(error, data, code, headers) { - if (error) - throw error; - - var cookie = headers['set-cookie'].join(''); - assert(cookie.indexOf('cookie1=1;') !== -1 && cookie.indexOf('cookie2=2;') !== -1 && cookie.indexOf('cookie3=3;') !== -1, 'Cookie problem.'); - complete(); - }); - }); - -/* - async.await('https', function(complete) { - utils.request(url + 'https/', 'GET', null, function(error, data, code, headers) { - if (error) - throw error; - assert(data === 'HTTPS', 'HTTP flag routing problem'); - complete(); - }); - }); -*/ - - async.complete(function() { - next && next(); - }); -} - -function run() { - - if (max <= 0) { - - framework.fs.rm.view('fromURL'); - - assert.ok(framework.global.header > 0, 'partial - global'); - assert.ok(framework.global.partial > 0, 'partial - partial'); - assert.ok(framework.global.timeout > 0, 'timeout'); - - end(); - return; - } - - max--; - test_controller_functions(function() { - test_view_functions(function() { - test_view_error(function() { - test_routing(function() { - run(); - }); - }); - }); - }); -} - -framework.fs.create.view('fromURL', 'http://www.totaljs.com/framework/test.html'); - -framework.on('ready', function() { - run(); -}); - -framework.run(http, true, 8001); \ No newline at end of file diff --git a/test/tests/controllers/index.test.js b/test/tests/controllers/index.test.js new file mode 100644 index 000000000..f885a26e8 --- /dev/null +++ b/test/tests/controllers/index.test.js @@ -0,0 +1,13 @@ +TEST('plain get', '/get', function(builder) { + builder.exec(function(err, response) { + FAIL(JSON.stringify(response) !== '{}'); + FAIL(JSON.stringify(response) !== '{OK}', 'NEVIEM'); + }); +}); + +TEST('upload', '/upload', function(builder) { + builder.file('file1', '/users/petersirka/desktop/aaa.txt'); + builder.exec(function(err, response) { + OK(response.name === 'aaa.txt'); + }); +}); \ No newline at end of file diff --git a/test/tests/test.js b/test/tests/test.js index 26f48407c..86f5cc9f6 100755 --- a/test/tests/test.js +++ b/test/tests/test.js @@ -1,9 +1,12 @@ -var assert = require('assert'); +exports.priority = 2; -exports.run = function(framework) { - - framework.assert('validation', function(name) { - assert('1' === '2', name); +TEST('validation assert', function() { + FAIL('1' !== '2'); + TEST('validation assert inline', function() { + FAIL('5' !== '5', 'TO JE OK'); }); +}); -}; \ No newline at end of file +TEST('validation assert.ok', function() { + FAIL(false); +}); diff --git a/test/themes/green/index.js b/test/themes/green/index.js new file mode 100644 index 000000000..fbe13d8ee --- /dev/null +++ b/test/themes/green/index.js @@ -0,0 +1,4 @@ +exports.install = function() { + F.global.theme = 1; + F.register('=green/resources/default.resource'); +}; \ No newline at end of file diff --git a/test/themes/green/public/css/default.css b/test/themes/green/public/css/default.css new file mode 100644 index 000000000..3cbc7234a --- /dev/null +++ b/test/themes/green/public/css/default.css @@ -0,0 +1 @@ +body { background-color: green; } \ No newline at end of file diff --git a/test/themes/green/public/js/default.js b/test/themes/green/public/js/default.js new file mode 100644 index 000000000..86b886a82 --- /dev/null +++ b/test/themes/green/public/js/default.js @@ -0,0 +1 @@ +var a = 1 + 1; \ No newline at end of file diff --git a/test/themes/green/public/js/merge.js b/test/themes/green/public/js/merge.js new file mode 100644 index 000000000..eaf5f87f1 --- /dev/null +++ b/test/themes/green/public/js/merge.js @@ -0,0 +1 @@ +var b = 2 + 2; \ No newline at end of file diff --git a/test/themes/green/resources/default.resource b/test/themes/green/resources/default.resource new file mode 100644 index 000000000..f605b550b --- /dev/null +++ b/test/themes/green/resources/default.resource @@ -0,0 +1 @@ +name-theme : ITEM FROM THEME \ No newline at end of file diff --git a/test/themes/green/views/index.html b/test/themes/green/views/index.html new file mode 100644 index 000000000..fcf322388 --- /dev/null +++ b/test/themes/green/views/index.html @@ -0,0 +1 @@ +
    VIEW:@{theme}
    \ No newline at end of file diff --git a/test/themes/green/views/layout.html b/test/themes/green/views/layout.html new file mode 100644 index 000000000..e082c0128 --- /dev/null +++ b/test/themes/green/views/layout.html @@ -0,0 +1,9 @@ + + + @{import('default.js', 'default.css')} + + +
    LAYOUT:@{theme}
    + @{body} + + \ No newline at end of file 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/empty-project/workers/empty b/test/threads/users/something.js similarity index 100% rename from empty-project/workers/empty rename to test/threads/users/something.js diff --git a/test/versions b/test/versions index 137dc42ee..cdde93c30 100755 --- a/test/versions +++ b/test/versions @@ -1 +1,3 @@ -default.js : default101.js \ No newline at end of file +/default.js : default101.js +mergepackage.js : mergepackage2.js +/test.js --> test1.js \ No newline at end of file diff --git a/test/views/a.html b/test/views/a.html index c5353c1c6..ab2b5c586 100755 --- a/test/views/a.html +++ b/test/views/a.html @@ -9,6 +9,8 @@ @{endif} @{end} +#@{component('contactform', { value: 'PETER' })}# +#
    @{{ vue_command }}
    # @{name(10)} @{meta('TITLE')} @{dns('//fonts.googleapis.com')} @@ -18,7 +20,7 @@ @{prev('/a/1/')} @{canonical('/a/a-b-c/')} @{head('//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js')} -#tag-encode@{repository.tag}# +#tag-encode@{R.tag}# #tag-raw@{!repository.tag}# #helper-fn-@{helper('fn', 'A')}# #readonly@{readonly(true)}# @@ -27,32 +29,24 @@ #disabled@{disabled(true)}# #resource@{resource('name')}# #options-empty@{options(repository.optionsEmpty, 'B')}# -#options@{options(repository.options, 'C', 'k', 'v')}# -#view-toggle@{viewToggle(false, 'b')}# -@{currentView('current')} -#view@{view('b', 'model')}# -#content@{content('test')}# -#content-toggle@{contentToggle(false, 'b')}# -#template-one@{template('one', repository.template)}# -#template-empty@{template('more', [], 'test')}# -#template-toggle@{templateToggle(false, 'more', [])}# -@{currentTemplate('current')} -#template-more@{template('more', repository.template)}# -#routejs-@{routeJS('p.js')}# +#options@{options(R.options, 'C', 'k', 'v')}# +#view@{view('current/b', 'model')}# +#routejs-@{public_js('p.js')}# #title@{title}# -#component@{component('products')}# +#mobile@{mobile}# #@{head}# #@{download('test.pdf', 'content', 'test')}# a -@{place('footer', 'jquery.js')} -@{place('footer', '//fabricjs.js')} -#dynamic@{view('OK')}# +@{place('footer', 'PLACE')} +#dynamic@{view_compile('OK')}# - +@{compile Tangular} +COMPILE_TANGULAR +@{end} + @{js('default.js')} @{favicon('favicon.ico')} diff --git a/test/views/nocompress.html b/test/views/nocompress.html new file mode 100644 index 000000000..ebdbae3cb --- /dev/null +++ b/test/views/nocompress.html @@ -0,0 +1,10 @@ +@{nocompress all} +@{layout('')} + +
    +A +
    + + \ No newline at end of file diff --git a/test/views/params.html b/test/views/params.html new file mode 100644 index 000000000..52d79dbdc --- /dev/null +++ b/test/views/params.html @@ -0,0 +1 @@ +--body=@{body.name}--query=@{query.value}-- \ No newline at end of file diff --git a/test/views/parse.html b/test/views/parse.html index 7b53653b4..073827e78 100755 --- a/test/views/parse.html +++ b/test/views/parse.html @@ -9,4 +9,4 @@ @{if model.a !== model.b} FUJ -@{endif} +@{fi} diff --git a/test/views/route.html b/test/views/route.html new file mode 100644 index 000000000..b7ffad0b9 --- /dev/null +++ b/test/views/route.html @@ -0,0 +1,3 @@ +@{layout('')} + +#@(DEFAULT)# \ No newline at end of file diff --git a/test/views/translate.html b/test/views/translate.html new file mode 100644 index 000000000..0f40e6f64 --- /dev/null +++ b/test/views/translate.html @@ -0,0 +1,3 @@ +@{layout('')} +---@(translate)--- +###@(#T1052832078)### \ No newline at end of file diff --git a/test/views/view.html b/test/views/view.html new file mode 100644 index 000000000..0c0773848 --- /dev/null +++ b/test/views/view.html @@ -0,0 +1,6 @@ +
    + @{config['name']} +
    + \ No newline at end of file diff --git a/test/workers/test.js b/test/workers/test.js new file mode 100644 index 000000000..2bbf3b6f3 --- /dev/null +++ b/test/workers/test.js @@ -0,0 +1,8 @@ +require('../../index'); + +F.load(true, ['definitions']); + +F.on('ready', function() { + F.send('assert'); + F.stop(); +}); \ No newline at end of file diff --git a/test/ws.js b/test/ws.js deleted file mode 100755 index ff6277334..000000000 --- a/test/ws.js +++ /dev/null @@ -1,98 +0,0 @@ -var crypto = require('crypto'); -var qs = require('querystring'); -var utils = require('../utils'); - -function TwitterOAuth(apiKey, apiSecret, accessToken, accessSecret) { - this.apiKey = apiKey; - this.apiSecret = apiSecret; - this.accessToken = accessToken; - this.accessSecret = accessSecret; -} - -TwitterOAuth.prototype.create = function(obj) { - var keys = Object.keys(obj); - var length = keys.length; - var builder = []; - - keys.sort(); - - for (var i = 0; i < length; i++) { - var key = keys[i]; - builder.push(key + '="' + escape(obj[key]) + '"'); - } - - return builder.join(', '); -}; - -TwitterOAuth.prototype.signature = function(method, url, params) { - - var self = this; - var keys = Object.keys(params); - var builder = []; - var key = encodeURIComponent(self.apiSecret) + '&' + encodeURIComponent(self.accessSecret); - - keys.sort(); - - var length = keys.length; - for (var i = 0; i < length; i++) - builder.push(keys[i] + '%3D' + encodeURIComponent(params[keys[i]])); - - var signature = method + '&' + encodeURIComponent(url) + '&' + builder.join('%26'); - return crypto.createHmac('sha1', key).update(signature).digest('base64'); -}; - -TwitterOAuth.prototype.request = function(method, url, params, callback, redirect) { - - var headers = {}; - var oauth = {}; - var self = this; - var data = ''; - - oauth['oauth_consumer_key'] = self.apiKey; - - if (redirect) - oauth['oauth_callback'] = redirect; - - oauth['oauth_token'] = self.accessToken; - oauth['oauth_signature_method'] = 'HMAC-SHA1'; - oauth['oauth_timestamp'] = Math.floor(new Date().getTime() / 1000).toString(); - oauth['oauth_nonce'] = utils.GUID(32); - oauth['oauth_version'] = '1.0'; - - if (!params) - params = {}; - else - data = qs.stringify(params); - - var keys = Object.keys(params); - var length = keys.length; - - for (var i = 0; i < length; i++) - params[keys[i]] = encodeURIComponent(params[keys[i]]); - - params['oauth_consumer_key'] = oauth['oauth_consumer_key']; - params['oauth_nonce'] = oauth['oauth_nonce']; - params['oauth_signature_method'] = oauth['oauth_signature_method']; - params['oauth_timestamp'] = oauth['oauth_timestamp']; - params['oauth_version'] = oauth['oauth_version']; - params['oauth_token'] = oauth['oauth_token']; - - oauth['oauth_signature'] = self.signature(method, url, params); - headers['Authorization'] = 'OAuth ' + self.create(oauth); - - utils.request(url, method, data, function(err, data) { - callback(err, JSON.parse(data)); - }, headers); -}; - -var twitter = new TwitterOAuth('Kz3IivBlfIDVrQNC1gqRmvN6r', 'RyOLxTPZc3heMT6T0FU6TGvx1VkzhBwVdchMwENPyL2W4AT6gH', '15876887-ndkuDgi6pqUpVXhqqAPeiTLpWelDKhzm6Q7pZ44l0', '8Fc9pdsnjaWQiXEBVyBaR6B5s2Cl9xGM6yG9jLCnPMHW9'); - -/* -twitter.request('GET', 'https://api.twitter.com/1.1/search/tweets.json', { q: '#nodejs', count: 4 }, function(err, data) { - console.log(data); -}); -*/ - -twitter.request('POST', 'https://api.twitter.com/1.1/statuses/update.json', { status: 'Test tweet.', trim_user: 'true', include_entities: 'true' }, function(err, data) { - console.log(data); -}); \ No newline at end of file diff --git a/tools/beta.sh b/tools/beta.sh new file mode 100644 index 000000000..89a447f27 --- /dev/null +++ b/tools/beta.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +cd "$DIR" +cd .. +npm publish --tag beta \ No newline at end of file diff --git a/tools/release.sh b/tools/release.sh new file mode 100644 index 000000000..6767889d8 --- /dev/null +++ b/tools/release.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +cd "$DIR" +cd .. +npm publish \ No newline at end of file diff --git a/utils.js b/utils.js index ebbb946a2..c384469be 100755 --- a/utils.js +++ b/utils.js @@ -1,347 +1,527 @@ +// 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 FrameworkUtils - * @author Peter Širka - * @copyright Peter Širka 2012-2014 - * @version 1.5.0. + * @version 3.4.4 */ 'use strict'; -var parser = require('url'); -var qs = require('querystring'); -var http = require('http'); -var https = require('https'); -var util = require('util'); -var path = require('path'); -var fs = require('fs'); -var events = require('events'); -var crypto = require('crypto'); -var expressionCache = {}; -var sys = require('sys'); - -var regexpMail = new RegExp('^[a-zA-Z0-9-_.+]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$'); -var regexpUrl = new RegExp('^(http[s]?:\\/\\/(www\\.)?|ftp:\\/\\/(www\\.)?|www\\.){1}([0-9A-Za-z-\\.@:%_\+~#=]+)+((\\.[a-zA-Z]{2,3})+)(/(.)*)?(\\?(.)*)?'); -var regexpTRIM = /^[\s]+|[\s]+$/g; - -var ENCODING = 'utf8'; -var UNDEFINED = 'undefined'; -var STRING = 'string'; -var FUNCTION = 'function'; -var NUMBER = 'number'; -var OBJECT = 'object'; -var BOOLEAN = 'boolean'; -var NEWLINE = '\r\n'; -var VERSION = (typeof(framework) !== UNDEFINED ? ' v' + framework.version_header : ''); - -var contentTypes = { - 'aac': 'audio/aac', - 'ai': 'application/postscript', - 'appcache': 'text/cache-manifest', - 'avi': 'video/avi', - 'bin': 'application/octet-stream', - 'bmp': 'image/bmp', - 'coffee': 'text/coffeescript', - 'css': 'text/css', - 'csv': 'text/csv', - 'doc': 'application/msword', - 'docx': 'application/msword', - 'dtd': 'application/xml-dtd', - 'eps': 'application/postscript', - 'exe': 'application/octet-stream', - 'geojson': 'application/json', - 'gif': 'image/gif', - 'gzip': 'application/x-gzip', - 'htm': 'text/html', - 'html': 'text/html', - 'ico': 'image/x-icon', - 'ics': 'text/calendar', - 'ifb': 'text/calendar', - 'jpe': 'image/jpeg', - 'jpeg': 'image/jpeg', - 'jpg': 'image/jpeg', - 'js': 'text/javascript', - 'json': 'application/json', - 'less': 'text/css', - 'm4a': 'audio/mp4a-latm', - 'm4v': 'video/x-m4v', - 'md': 'text/markdown', - 'mid': 'audio/midi', - 'midi': 'audio/midi', - 'mov': 'video/quicktime', - 'mp3': 'audio/mpeg', - 'mp4': 'video/mp4', - 'mpe': 'video/mpeg', - 'mpeg': 'video/mpeg', - 'mpg': 'video/mpeg', - 'mpga': 'audio/mpeg', - 'mtl': 'text/plain', - 'mv4': 'video/mv4', - 'obj': 'text/plain', - 'ogg': 'application/ogg', - 'package': 'text/plain', - 'pdf': 'application/pdf', - 'png': 'image/png', - 'ppt': 'application/vnd.ms-powerpoint', - 'pptx': 'application/vnd.ms-powerpoint', - 'ps': 'application/postscript', - 'rar': 'application/x-rar-compressed', - 'rtf': 'text/rtf', - 'sass': 'text/css', - 'scss': 'text/css', - 'sh': 'application/x-sh', - 'stl': 'application/sla', - 'svg': 'image/svg+xml', - 'swf': 'application/x-shockwave-flash', - 'tar': 'application/x-tar', - 'tif': 'image/tiff', - 'tiff': 'image/tiff', - 'txt': 'text/plain', - 'wav': 'audio/x-wav', - 'webp': 'image/webp', - 'woff': 'font/woff', - 'xht': 'application/xhtml+xml', - 'xhtml': 'application/xhtml+xml', - 'xls': 'application/vnd.ms-excel', - 'xlsx': 'application/vnd.ms-excel', - 'xml': 'application/xml', - 'xpm': 'image/x-xpixmap', - 'xsl': 'application/xml', - 'xslt': 'application/xslt+xml', - 'zip': 'application/zip' -}; - -if (typeof(setImmediate) === UNDEFINED) { - global.setImmediate = function(cb) { - process.nextTick(cb); - }; -} - -var hasOwnProperty = Object.prototype.hasOwnProperty; - -/* - Expression declaration - @query {String} - @params {String Array} - @values {Object Array} - return {Function} -*/ -function expression(query, params) { - - var name = params.join(','); - var fn = expressionCache[query + '-' + name]; - - if (!fn) { - fn = eval('(function(' + name +'){' + (query.indexOf('return') === -1 ? 'return ' : '') + query + '})'); - expressionCache[query + name] = fn; - } - - var values = []; - - for (var i = 2; i < arguments.length; i++) - values.push(arguments[i]); - - return (function() { - var arr = []; - - for (var i = 0; i < arguments.length; i++) - arr.push(arguments[i]); - - for (var i = 0; i < values.length; i++) - arr.push(values[i]); - - return fn.apply(this, arr); - }); -} +const Dns = require('dns'); +const Url = require('url'); +const Qs = require('querystring'); +const Http = require('http'); +const Https = require('https'); +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()); +}; -global.expression = expression; +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|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 = /\{{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 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'; +const isWindows = require('os').platform().substring(0, 3).toLowerCase() === 'win'; +const DIACRITICSMAP = {}; +const STREAM_READONLY = { flags: 'r' }; +const STREAM_END = { end: false }; +const ALPHA_INDEX = { '<': '<', '>': '>', '"': '"', '&apos': '\'', '&': '&', '<': '<', '>': '>', '"': '"', ''': '\'', '&': '&' }; +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']; + +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 0) - return false; + for (var key in obj) { + if (hasOwnProperty.call(obj, key)) + return false; + } - if (obj.length === 0) - return true; + return true; +}; - for (var key in obj) { - if (hasOwnProperty.call(obj, key)) - return false; - } - return true; +/** + * Compare objects + * @param {Object} obj1 + * @param {Object} obj2 + * @return {Boolean} + */ +exports.isEqual = function(obj1, obj2, properties) { + + var keys = properties ? properties : Object.keys(obj1); + + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i]; + var a = obj1[key]; + var b = obj2[key]; + var ta = typeof(a); + var tb = typeof(b); + + if (ta !== tb) + return false; + + if (a === b) + continue; + + if (a instanceof Date && b instanceof Date) { + if (a.getTime() === b.getTime()) + continue; + return false; + } else if (a instanceof Array && b instanceof Array) { + if (JSON.stringify(a) === JSON.stringify(b)) + continue; + return false; + } + + if (ta === 'object' && tb === 'object') { + if (exports.isEqual(a, b)) + continue; + } + + return false; + } + + return true; }; /** - * Create a request to a specific URL - * @param {String} url URL address. - * @param {String Array} flags Request flags. - * @param {String or Object} data Request data (optional). - * @param {Function(error, content, statusCode, headers)} callback Callback. - * @param {Object} headers Custom cookies (optional, default: null). - * @param {Object} headers Custom headers (optional, default: null). - * @param {String} encoding Encoding (optional, default: UTF8) - * @param {Number} timeout Request timeout. - * return {Boolean} + * Function checks a valid function and waits for it positive result + * @param {Function} fnValid + * @param {Function(err, success)} fnCallback + * @param {Number} timeout Timeout, optional (default: 5000) + * @param {Number} interval Refresh interval, optional (default: 500) */ -exports.request = function(url, flags, data, callback, cookies, headers, encoding, timeout) { +exports.wait = function(fnValid, fnCallback, timeout, interval) { + + if (fnValid() === true) + return fnCallback(null, true); - // No data (data is optinal argument) - if (typeof(data) === FUNCTION) { - timeout = encoding; - encoding = headers; - headers = cookies; - cookies = callback; - callback = data; - data = ''; - } + var id_timeout = null; + var id_interval = setInterval(function() { - var method = 'GET'; - var length = 0; - var isJSON = false; + if (fnValid() === true) { + clearInterval(id_interval); + clearTimeout(id_timeout); + fnCallback && fnCallback(null, true); + } - headers = exports.extend({}, headers || {}); + }, interval || 500); - if (typeof(encoding) !== STRING) - encoding = ENCODING; + id_timeout = setTimeout(function() { + clearInterval(id_interval); + fnCallback && fnCallback(new Error('Timeout.'), false); + }, timeout || 5000); +}; + +exports.$$wait = function(fnValid, timeout, interval) { + return function(callback) { + exports.wait(fnValid, callback, timeout, interval); + }; +}; + +/** + * Resolves an IP from the URL address + * @param {String} url + * @param {Function(err, uri)} callback + */ +exports.resolve = function(url, callback, param) { - if (data === null) - data = ''; + var uri = Url.parse(url); - if (flags instanceof Array) { - length = flags.length; - for (var i = 0; i < length; i++) { - switch (flags[i].toLowerCase()) { + if (!callback) + return dnscache[uri.host]; - case 'xhr': - headers['X-Requested-With'] = 'XMLHttpRequest'; - break; + if (dnscache[uri.host]) { + uri.host = dnscache[uri.host]; + callback(null, uri, param); + return; + } + + Dns.resolve4(uri.hostname, function(e, addresses) { + if (e) + setImmediate(dnsresolve_callback, uri, callback, param); + else { + dnscache[uri.host] = addresses[0]; + uri.host = addresses[0]; + callback(null, uri, param); + } + }); +}; - case 'json': - headers['Content-Type'] = 'application/json'; - isJSON = true; - break; +function dnsresolve_callback(uri, callback, param) { + Dns.resolve4(uri.hostname, function(e, addresses) { + if (addresses && addresses.length) { + dnscache[uri.host] = addresses[0]; + uri.host = addresses[0]; + } + callback(e, uri, param); + }); +} - case 'get': - case 'delete': - case 'options': - method = flags[i].toUpperCase(); - break; +exports.$$resolve = function(url) { + return function(callback) { + return exports.resolve(url, callback); + }; +}; - case 'upload': - headers['Content-Type'] = 'multipart/form-data'; - break; +/** + * Clears DNS cache + */ +exports.clearDNS = function() { + OBSOLETE('U.clearDNS()', 'Use CMD(\'clear_dnscache\')'); + CMD('clear_dnscache'); +}; - case 'post': - case 'put': +setImmediate(function() { + if (global.F) { + F.install('command', 'clear_dnscache', function() { + dnscache = {}; + }); + } +}); - method = flags[i].toUpperCase(); - if (!headers['Content-Type']) - headers['Content-Type'] = 'application/x-www-form-urlencoded'; +exports.keywords = function(content, forSearch, alternative, max_count, max_length, min_length) { - break; - } - } - } + if (forSearch === undefined) + forSearch = true; - var isPOST = method === 'POST' || method === 'PUT'; + min_length = min_length || 2; + max_count = max_count || 200; + max_length = max_length || 20; - if (typeof(data) !== STRING) - data = isJSON ? JSON.stringify(data) : qs.stringify(data); - else if (data[0] === '?') - data = data.substring(1); + var words = []; + var isSoundex = alternative === 'soundex'; - if (!isPOST) { - if (data.length > 0 && url.indexOf('?') === -1) - url += '?' + data; - data = ''; - } + if (content instanceof Array) { + for (var i = 0, length = content.length; i < length; i++) { + if (!content[i]) + continue; + var tmp = (forSearch ? content[i].removeDiacritics().toLowerCase().replace(regexpY, 'i') : content[i].toLowerCase()).replace(regexpN, ' ').split(' '); + if (!tmp || !tmp.length) + continue; + for (var j = 0, jl = tmp.length; j < jl; j++) + words.push(tmp[j]); + } + } else + words = (forSearch ? content.removeDiacritics().toLowerCase().replace(regexpY, 'i') : content.toLowerCase()).replace(regexpN, ' ').split(' '); - var uri = parser.parse(url); - uri.method = method; + if (!words) + words = []; - headers['X-Powered-By'] = 'total.js' + VERSION; + var dic = {}; + var counter = 0; - if (cookies) { - var builder = []; - var keys = Object.keys(cookies); + for (var i = 0, length = words.length; i < length; i++) { - length = keys.length; + var word = words[i].trim().replace(regexpCHARS, keywordscleaner); - for (var i = 0; i < length; i++) - builder.push(keys[i] + '=' + encodeURIComponent(cookies[keys[i]])); + if (regexpCHINA.test(word)) { - if (builder.length > 0) - headers['Cookie'] = builder.join('; '); - } + var tmpw = word.split('', max_count); - if (data.length > 0) - headers['Content-Length'] = data.length; + for (var j = 0; j < tmpw.length; j++) { + word = tmpw[j]; + if (dic[word]) + dic[word]++; + else + dic[word] = 1; + counter++; + } - uri.agent = false; - uri.headers = headers; + if (counter >= max_count) + break; - var onResponse = function(res) { + continue; + } - if (!callback) { - res.resume(); - return; - } + if (word.length < min_length) + continue; - res._buffer = ''; + if (counter >= max_count) + break; - res.on('data', function(chunk) { - this._buffer += chunk.toString(encoding); - }); + // Gets 80% length of word + if (alternative) { + if (isSoundex) + word = word.soundex(); + else { + var size = (word.length / 100) * 80; + if (size > min_length + 1) + word = word.substring(0, size); + } + } - res.on('end', function() { - var self = this; - callback(null, self._buffer, self.statusCode, self.headers); - }); + if (word.length < min_length || word.length > max_length) + continue; - res.resume(); + if (dic[word]) + dic[word]++; + else + dic[word] = 1; - }; + counter++; + } - var connection = uri.protocol === 'https:' ? https : http; + var keys = Object.keys(dic); - try - { - var request = isPOST ? connection.request(uri, onResponse) : connection.get(uri, onResponse); + keys.sort(function(a, b) { + var countA = dic[a]; + var countB = dic[b]; + return countA > countB ? -1 : countA < countB ? 1 : 0; + }); - if (callback) { - request.on('error', function(error) { - callback(error, null, 0, {}); - }); + return keys; +}; - request.setTimeout(timeout || 10000, function() { - callback(new Error(exports.httpStatus(408)), null, 0, {}); - }); - } +function keywordscleaner(c) { + return c.charCodeAt(0) < 200 ? '' : c; +} - if (isPOST) - request.end(data, encoding); - else - request.end(); +function parseProxy(p) { + var key = 'proxy_' + p; + if (F.temporary.other[key]) + return F.temporary.other[key]; - } catch (ex) { + if (p.indexOf('://') === -1) + p = 'http://' + p; - if (callback) - callback(ex, null, 0, {}); + var obj = Url.parse(p); - return false; - } + if (obj.auth) + obj._auth = 'Basic ' + Buffer.from(obj.auth).toString('base64'); - return true; + obj.port = +obj.port; + return F.temporary.other[key] = obj; } /** @@ -349,261 +529,1376 @@ exports.request = function(url, flags, data, callback, cookies, headers, encodin * @param {String} url URL address. * @param {String Array} flags Request flags. * @param {String or Object} data Request data (optional). - * @param {Function(error, response)} callback Callback. + * @param {Function(error, content, statusCode, headers)} callback Callback. * @param {Object} headers Custom cookies (optional, default: null). * @param {Object} headers Custom headers (optional, default: null). * @param {String} encoding Encoding (optional, default: UTF8) * @param {Number} timeout Request timeout. * return {Boolean} */ -exports.download = function(url, flags, data, callback, cookies, headers, encoding, timeout) { - - // No data (data is optinal argument) - if (typeof(data) === FUNCTION) { - timeout = encoding; - encoding = headers; - headers = cookies; - cookies = callback; - callback = data; - data = ''; - } - var method = 'GET'; - var length = 0; - var isJSON = false; +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') { + encoding = headers; + headers = cookies; + cookies = callback; + callback = data; + data = ''; + } else if (!data) + data = ''; + + if (callback === NOOP) + callback = null; + + 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) { + headers = exports.extend({}, headers); + def = headers[CT]; + } else + headers = {}; + + if (flags instanceof Array) { + for (var i = 0, length = flags.length; i < length; i++) { + + // timeout + if (flags[i] > 0) { + options.timeout = flags[i]; + continue; + } + + if (flags[i][0] === '<') { + options.max = flags[i].substring(1).trim().parseInt() * 1024; // kB + 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': + case 'binary': + case 'hex': + options.encoding = flags[i]; + break; + case 'xhr': + headers['X-Requested-With'] = 'XMLHttpRequest'; + break; + case 'plain': + if (!def) + headers[CT] = 'text/plain'; + break; + case 'html': + if (!def) + headers[CT] = 'text/html'; + break; + case 'raw': + type = 3; + if (!def) + headers[CT] = 'application/octet-stream'; + break; + case 'json': + if (!def) + headers[CT] = 'application/json'; + !method && (method = 'POST'); + type = 1; + break; + case 'xml': + if (!def) + headers[CT] = 'text/xml'; + !method && (method = 'POST'); + type = 2; + break; + + case 'get': + case 'options': + case 'head': + method = flags[i].charCodeAt(0) > 96 ? flags[i].toUpperCase() : flags[i]; + break; + + case 'noredirect': + options.noredirect = true; + break; + + case 'upload': + type = 4; + options.upload = true; + options.files = files || EMPTYARRAY; + options.boundary = '----totaljs' + Math.random().toString(16).substring(2); + headers[CT] = 'multipart/form-data; boundary=' + options.boundary; + break; + + case 'post': + case 'put': + case 'delete': + case 'patch': + method = flags[i].toUpperCase(); + !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 = !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) { + 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 : Buffer.from(data, ENCODING); + headers['Content-Length'] = options.data.length; + } else + options.data = data; + + if (cookies) { + if (isCookies) + options.cookies = cookies; + var builder = ''; + for (var m in cookies) + builder += (builder ? '; ' : '') + m + '=' + cookies[m]; + if (builder) + headers['Cookie'] = builder; + } + + 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.headers = headers; + options.uri = uri; + + 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; +}; - headers = exports.extend({}, headers || {}); +function request_resolve(err, uri, options) { + if (!err) + options.uri.host = uri.host; + request_call(options.uri, options); +} - if (typeof(encoding) !== STRING) - encoding = ENCODING; +function ProxyAgent(options) { + var self = this; + self.options = options; + self.maxSockets = Http.Agent.defaultMaxSockets; + self.requests = []; +} - if (data === null) - data = ''; +const PAP = ProxyAgent.prototype; - if (flags instanceof Array) { - length = flags.length; - for (var i = 0; i < length; i++) { +PAP.createConnection = function(pending) { + var self = this; + self.createSocket(pending, function(socket) { + pending.request.onSocket(socket); + }); +}; - switch (flags[i].toLowerCase()) { +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(); +}; - case 'xhr': - headers['X-Requested-With'] = 'XMLHttpRequest'; - break; +function proxyagent_response(res) { + res.upgrade = true; +} - case 'json': - headers['Content-Type'] = 'application/json'; - isJSON = true; - break; +PAP.addRequest = function(req, options) { + this.createConnection({ host: options.host, port: options.port, request: req }); +}; - case 'get': - case 'delete': - case 'options': - method = flags[i].toUpperCase(); - break; +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); + }); +} - case 'upload': - headers['Content-Type'] = 'multipart/form-data'; - break; +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(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', 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', request_assign_res); + + if (options.upload) { + options.first = true; + options.files.wait(function(file, 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 + value.toString()); + if (options.first) + options.first = false; + } + } + req.end(NEWLINE + '--' + options.boundary + '--'); + }); + } else + req.end(options.data); +} - case 'post': - case 'put': +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; + } +} - method = flags[i].toUpperCase(); +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; + } +} - if (!headers['Content-Type']) - headers['Content-Type'] = 'application/x-www-form-urlencoded'; +function request_assign_res(response) { + response.req = this; +} - break; - } - } - } +function request_writefile(req, options, file, next) { - var isPOST = method === 'POST' || method === 'PUT'; + var type = typeof(file.buffer); + var filename = (type === 'string' ? file.buffer : exports.getName(file.filename)); - if (typeof(data) !== STRING) - data = isJSON ? JSON.stringify(data) : qs.stringify(data); - else if (data[0] === '?') - data = data.substring(1); + req.write((options.first ? '' : NEWLINE) + '--' + options.boundary + NEWLINE + 'Content-Disposition: form-data; name="' + file.name + '"; filename="' + filename + '"' + NEWLINE + 'Content-Type: ' + exports.getContentType(exports.getExtension(filename)) + NEWLINE + NEWLINE); - if (!isPOST) { - if (data.length > 0 && url.indexOf('?') === -1) - url += '?' + data; - data = ''; - } + if (options.first) + options.first = false; - var uri = parser.parse(url); - uri.method = method; + // Is Buffer + if (file.buffer && type === 'object') { + req.write(file.buffer); + next(); + } else { + var stream = Fs.createReadStream(file.filename); + stream.once('close', next); + stream.pipe(req, STREAMPIPE); + } +} - headers['X-Powered-By'] = 'total.js' + VERSION; +function request_response(res) { - if (cookies) { - var builder = []; - var keys = Object.keys(cookies); + var options = this.$options; + var uri = this.$uri; - length = keys.length; + res._buffer = null; + res._bufferlength = 0; + + // We have redirect + if (res.statusCode === 301 || res.statusCode === 302) { + + 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.param); + options.callback = null; + } + + if (options.evt) { + options.evt.removeAllListeners(); + options.evt = null; + } + + res.req.removeAllListeners(); + 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.param); + options.callback = null; + } + + if (options.evt) { + options.evt.removeAllListeners(); + options.evt = null; + } + + res.req.removeAllListeners(); + res.removeAllListeners(); + res.req = null; + res = null; + return; + } + + options.redirect++; + + 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.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(tmp, function(err, u) { + if (!err) + tmp.host = u.host; + res.removeAllListeners(); + res = null; + request_call(tmp, options); + }); + + return; + } + + options.length = +res.headers['content-length'] || 0; + options.evt && options.evt.$events.begin && options.evt.emit('begin', options.length); + + // Shared cookies + if (options.cookies) { + var arr = (res.headers['set-cookie'] || ''); + + // Only the one value + if (arr && !(arr instanceof Array)) + arr = [arr]; + + if (arr instanceof Array) { + for (var i = 0, length = arr.length; i < length; i++) { + var line = arr[i]; + var end = line.indexOf(';'); + if (end === -1) + end = line.length; + line = line.substring(0, end); + var index = line.indexOf('='); + if (index !== -1) + options.cookies[line.substring(0, index)] = decodeURIComponent(line.substring(index + 1)); + } + } + } + + if (res.statusCode === 204) { + options.done = true; + request_process_end.call(res); + return; + } + + var encoding = res.headers['content-encoding'] || ''; + if (encoding) + encoding = encoding.split(',')[0]; + + 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); + } + + res.resume(); +} - for (var i = 0; i < length; i++) - builder.push(keys[i] + '=' + encodeURIComponent(cookies[keys[i]])); +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); +} - if (builder.length > 0) - headers['Cookie'] = builder.join('; '); - } +function request_process_end() { - if (data.length > 0) - headers['Content-Length'] = data.length; + var res = this; - uri.agent = false; - uri.headers = headers; + // Is Zlib + if (!res.req) + res = res.res; - var onResponse = function(res) { - callback(null, res); - res.resume(); - }; + var self = res; + var options = self.req.$options; + var uri = self.req.$uri; + var data; - var connection = uri.protocol === 'https:' ? https : http; + options.socket && options.uri.agent.destroy(); + options.timeoutid && clearTimeout(options.timeoutid); - try - { - var request = isPOST ? connection.request(uri, onResponse) : connection.get(uri, onResponse); + if (options.canceled) + return; - if (callback) { - request.on('error', function(error) { - callback(error, null, 0, {}); - }); + var ct = self.headers['content-type']; - request.setTimeout(timeout || 10000, function() { - callback(new Error(exports.httpStatus(408)), null, 0, {}); - }); - } + if (!ct || REG_TEXTAPPLICATION.test(ct)) + data = self._buffer ? (options.encoding === 'binary' ? self._buffer : self._buffer.toString(options.encoding)) : ''; + else + data = self._buffer; - if (isPOST) - request.end(data, encoding); - else - request.end(); + options.canceled = true; - } catch (ex) { + self._buffer = undefined; - if (callback) - callback(ex, null, 0, {}); + 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; + } - return false; - } + 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; + } - return true; + if (res.statusCode !== 204) { + res.req && res.req.removeAllListeners(); + res.removeAllListeners(); + } } -/** - * Send a stream through HTTP - * @param {String} name Filename with extension. - * @param {Stream} stream Stream. - * @param {String} url A valid URL address. - * @param {Function} callback Callback. - * @param {Object} headers Custom headers (optional). - * @param {String} method HTTP method (optional, default POST). - */ -exports.send = function(name, stream, url, callback, headers, method) { +exports.$$request = function(url, flags, data, cookies, headers, encoding, timeout) { + return function(callback) { + exports.request(url, flags, data, callback, cookies, headers, encoding, timeout); + }; +}; - var self = this; +exports.btoa = function(str) { + return (str instanceof Buffer) ? str.toString('base64') : Buffer.from(str.toString(), 'utf8').toString('base64'); +}; + +exports.atob = function(str) { + return Buffer.from(str, 'base64').toString('utf8'); +}; - if (typeof(callback) === OBJECT) { - var tmp = headers; - callback = headers; - headers = tmp; - } +/** + * Create a request to a specific URL + * @param {String} url URL address. + * @param {String Array} flags Request flags. + * @param {String or Object} data Request data (optional). + * @param {Function(error, response)} callback Callback. + * @param {Object} cookies Custom cookies (optional, default: null). + * @param {Object} headers Custom headers (optional, default: null). + * @param {String} encoding Encoding (optional, default: UTF8) + * @param {Number} timeout Request timeout. + * return {Boolean} + */ +exports.download = function(url, flags, data, callback, cookies, headers, encoding, timeout, param) { + + // No data (data is optional argument) + if (typeof(data) === 'function') { + timeout = encoding; + encoding = headers; + headers = cookies; + cookies = callback; + callback = data; + data = ''; + } + + if (typeof(cookies) === 'number') { + cookies = null; + timeout = cookies; + } + + if (typeof(headers) === 'number') { + headers = null; + timeout = headers; + } + + if (typeof(encoding) === 'number') { + encoding = null; + timeout = encoding; + } + + if (typeof(encoding) !== 'string') + encoding = ENCODING; + + var proxy, type = 0; + var method = 'GET'; + var options = { callback: callback, resolve: false, length: 0, evt: new EventEmitter2(), timeout: timeout || 60000, post: false, encoding: encoding }; + + if (headers) + headers = exports.extend({}, headers); + else + headers = {}; + + if (data === null) + data = ''; + + if (flags instanceof Array) { + for (var i = 0, length = flags.length; i < length; i++) { + + // timeout + if (flags[i] > 0) { + options.timeout = flags[i]; + continue; + } + + if (flags[i][0] === '<') { + // max length is not supported + continue; + } + + if (flags[i][0] === 'p' && flags[i][4] === 'y') { + proxy = parseProxy(flags[i].substring(6)); + continue; + } + + switch (flags[i].toLowerCase()) { + + case 'utf8': + case 'ascii': + case 'base64': + case 'binary': + case 'hex': + options.encoding = flags[i]; + break; + + case 'xhr': + headers['X-Requested-With'] = 'XMLHttpRequest'; + break; + + case 'plain': + headers['Content-Type'] = 'text/plain'; + break; + case 'html': + headers['Content-Type'] = 'text/html'; + break; + + case 'json': + headers['Content-Type'] = 'application/json'; + type = 1; + break; + + case 'xml': + headers['Content-Type'] = 'text/xml'; + type = 2; + break; + + case 'get': + case 'head': + case 'options': + method = flags[i].charCodeAt(0) > 96 ? flags[i].toUpperCase() : flags[i]; + break; + + case 'upload': + headers['Content-Type'] = 'multipart/form-data'; + break; + + case 'post': + case 'patch': + case 'delete': + case 'put': + method = flags[i].charCodeAt(0) > 96 ? flags[i].toUpperCase() : flags[i]; + if (!headers['Content-Type']) + headers['Content-Type'] = 'application/x-www-form-urlencoded'; + break; + + 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; + } + } + } + + if (!method) + method = 'GET'; + + options.post = !NOBODY[method]; + + if (typeof(data) !== 'string') + data = type === 1 ? JSON.stringify(data) : Qs.stringify(data); + else if (data[0] === '?') + data = data.substring(1); + + if (!options.post) { + if (data.length && url.indexOf('?') === -1) + url += '?' + data; + data = ''; + } + + if (cookies) { + var builder = ''; + for (var m in cookies) + builder += (builder ? '; ' : '') + m + '=' + cookies[m]; + if (builder) + headers['Cookie'] = builder; + } + + var uri = Url.parse(url); + uri.method = method; + // 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 = Buffer.from(data, ENCODING); + headers['Content-Length'] = options.data.length; + } + + 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; +}; - if (typeof(stream) === STRING) - stream = fs.createReadStream(stream, { flags: 'r' }); +function download_resolve(err, uri, options) { + if (!err) + options.uri.host = uri.host; + download_call(options.uri, options); +} - var BOUNDARY = '----' + Math.random().toString(16).substring(2); - var h = {}; +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(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', 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); +} - if (headers) - util._extend(h, headers); +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); +} - name = path.basename(name); +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; + } +} - h['Cache-Control'] = 'max-age=0'; - h['Content-Type'] = 'multipart/form-data; boundary=' + BOUNDARY; - h['X-Powered-By'] = 'total.js' + VERSION; +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; + options.canceled = true; + } +} - var uri = parser.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 }; +function download_response(res) { + + var options = this.$options; + var uri = this.$uri; + + res._bufferlength = 0; + + // We have redirect + if (res.statusCode === 301 || res.statusCode === 302) { + + if (options.redirect > 3) { + 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(); + res = null; + return; + } + + options.redirect++; + + 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.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(loc, function(err, u) { + if (!err) + tmp.host = u.host; + res.removeAllListeners(); + res = null; + download_call(tmp, options); + }); + + return; + } + + res.on('data', download_process_data); + res.on('end', download_process_end); + + res.resume(); + options.timeoutid && clearTimeout(options.timeoutid); + options.callback && options.callback(null, res, res.statusCode, res.headers, uri.host, options.param); +} - var response = function(res) { +exports.$$download = function(url, flags, data, cookies, headers, encoding, timeout) { + return function(callback) { + exports.download(url, flags, data, callback, cookies, headers, encoding, timeout); + }; +}; - if (!callback) - return; +function download_process_end() { - res.body = ''; - res.on('data', function(chunk) { - this.body += chunk.toString(ENCODING); - }); + var res = this; + var self = this; + var options = self.req.$options; + var uri = self.req.$uri; - res.on('end', function() { - callback(null, res.body); - }); + 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; + } - var connection = options.protocol === 'https:' ? https : http; - var req = connection.request(options, response); + res.req && res.req.removeAllListeners(); + res.removeAllListeners(); +} - if (callback) { - req.on('error', function(err) { - callback(err, null); - }); - } +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.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); + } + } +} - var header = NEWLINE + NEWLINE + '--' + BOUNDARY + NEWLINE + 'Content-Disposition: form-data; name="File"; filename="' + name + '"' + NEWLINE + 'Content-Type: ' + utils.getContentType(path.extname(name)) + NEWLINE + NEWLINE; - req.write(header); +/** + * Upload a stream through HTTP + * @param {String} name Filename with extension. + * @param {Stream} stream Stream. + * @param {String} url A valid URL address. + * @param {Function} callback Callback. + * @param {Object} headers Custom headers (optional). + * @param {String} method HTTP method (optional, default POST). + * @param {Number} timeout Request timeout, default: 60000 (1 minute) + */ +exports.send = function(name, stream, url, callback, cookies, headers, method, timeout) { + + OBSOLETE('U.send()', 'Use U.upload() instead of U.send().'); + + if (typeof(stream) === 'string') + stream = Fs.createReadStream(stream, STREAM_READONLY); + + var BOUNDARY = '----totaljs' + Math.random().toString(16).substring(2); + var h = {}; + + if (headers) + exports.extend(h, headers); + + if (cookies) { + var builder = ''; + for (var m in cookies) + builder += (builder ? '; ' : '') + m + '=' + cookies[m]; + if (builder) + h['Cookie'] = builder; + } + + name = exports.getName(name); + + 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 }; + var responseLength = 0; + + var response = function(res) { + + 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); + }); + + 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; + }); + }; + + var connection = options.protocol === 'https:' ? Https : Http; + var req = connection.request(options, response); + + req.on('response', function(response) { + responseLength = +response.headers['content-length'] || 0; + 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); + }); + + req.on('error', function(err) { + req.removeAllListeners(); + req = null; + e.removeAllListeners(); + e = null; + callback && callback(err, '', 0, undefined, uri.host); + }); + + req.on('close', function() { + req.removeAllListeners(); + req = null; + }); + + var header = NEWLINE + NEWLINE + '--' + BOUNDARY + NEWLINE + 'Content-Disposition: form-data; name="File"; filename="' + name + '"' + NEWLINE + 'Content-Type: ' + exports.getContentType(exports.getExtension(name)) + NEWLINE + NEWLINE; + req.write(header); + + // Is Buffer + if (stream.length) { + req.write(stream); + req.end(NEWLINE + NEWLINE + '--' + BOUNDARY + '--'); + return e; + } + + stream.on('end', () => req.end(NEWLINE + NEWLINE + '--' + BOUNDARY + '--')); + stream.pipe(req, STREAM_END); + return e; +}; - stream.on('end', function() { - req.end(NEWLINE + NEWLINE + '--' + BOUNDARY + '--'); - }); +exports.$$send = function(name, stream, url, cookies, headers, method, timeout) { + return function(callback) { + exports.send(name, stream, url, callback, cookies, headers, method, timeout); + }; +}; - stream.pipe(req, { end: false }); +exports.upload = function(files, url, callback, cookies, headers, method, timeout) { + + var BOUNDARY = '----totaljs' + Math.random().toString(16).substring(2); + var h = {}; + + headers && exports.extend_headers2(h, headers); + + if (cookies) { + var builder = ''; + for (var m in cookies) + builder += (builder ? '; ' : '') + m + '=' + cookies[m]; + 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; + + 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 }; + var responseLength = 0; + var timeoutid; + var done = false; + + var response = function(res) { + + res.body = Buffer.alloc(0); + res._bufferlength = 0; + + res.on('data', function(chunk) { + 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() { + 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; + } + }); + }; + + var connection = options.protocol === 'https:' ? Https : Http; + var req = connection.request(options, response); + + req.on('response', function(response) { + responseLength = +response.headers['content-length'] || 0; + e.$events.begin && e.emit('begin', responseLength); + }); + + 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(); + e.removeAllListeners(); + callback && callback(err, '', 0, undefined, uri.host); + timeoutid && clearTimeout(timeoutid); + req = null; + e = null; + }); + + req.on('close', function() { + req.removeAllListeners(); + req = null; + }); + + var header = NEWLINE + NEWLINE + '--' + BOUNDARY + NEWLINE + 'Content-Disposition: form-data; name="{0}"; filename="{1}"' + NEWLINE + 'Content-Type: {2}' + NEWLINE + NEWLINE; + + files.wait(function(item, next) { + + // item.name; + // item.filename; + // item.stream (optional) or item.buffer (optional) + + req.write(header.format(item.name, U.getName(item.filename), exports.getContentType(exports.getExtension(item.filename)))); + + if (item.buffer) { + req.write(item.buffer); + return next(); + } + + !item.stream && (item.stream = Fs.createReadStream(item.filename)); + item.stream.pipe(req, STREAM_END); + item.stream.on('error', next); + item.stream.on('end', next); + + }, () => req.end(NEWLINE + NEWLINE + '--' + BOUNDARY + '--')); + return e; +}; - return self; +exports.$$upload = function(files, url, cookies, headers, method, timeout) { + return function(callback) { + exports.upload(files, url, callback, cookies, headers, method, timeout); + }; }; /** * Trim string properties - * @param {Object} obj Object. + * @param {Object} obj * @return {Object} */ -exports.trim = function(obj) { - - var type = typeof(obj); - - if (type === STRING) - return obj.trim(); - - if (type !== OBJECT) - return obj; - - Object.keys(obj).forEach(function(name) { - var val = obj[name]; - - if (typeof(val) === OBJECT) { - exports.trim(val); - return; - } - - if (typeof(val) !== STRING) - return; - - obj[name] = val.trim(); - }); - - return obj; +exports.trim = function(obj, clean) { + + if (!obj) + return obj; + + var type = typeof(obj); + if (type === 'string') { + obj = obj.trim(); + return clean && !obj ? undefined : obj; + } + + if (obj instanceof Array) { + for (var i = 0, length = obj.length; i < length; i++) { + + var item = obj[i]; + type = typeof(item); + + if (type === 'object') { + exports.trim(item, clean); + continue; + } + + if (type !== 'string') + continue; + + obj[i] = item.trim(); + if (clean && !obj[i]) + obj[i] = undefined; + } + + return obj; + } + + if (type !== 'object') + return obj; + + var keys = Object.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + var val = obj[keys[i]]; + var type = typeof(val); + if (type === 'object') { + exports.trim(val, clean); + continue; + } else if (type !== 'string') + continue; + obj[keys[i]] = val.trim(); + if (clean && !obj[keys[i]]) + obj[keys[i]] = undefined; + } + + return obj; }; /** * Noop function * @return {Function} Empty function. */ -exports.noop = function() {}; -global.noop = function() {}; +exports.noop = global.noop = global.NOOP = function() {}; /** * Read HTTP status @@ -612,99 +1907,195 @@ global.noop = function() {}; * @return {String} */ exports.httpStatus = function(code, addCode) { - return (addCode || true ? code + ': ' : '') + http.STATUS_CODES[code]; + if (addCode === undefined) + addCode = true; + return (addCode ? code + ': ' : '') + Http.STATUS_CODES[code]; }; /** * Extend object * @param {Object} target Target object. * @param {Object} source Source object. - * @param {Boolean} rewrite Rewrite exists values (optional, default false). + * @param {Boolean} rewrite Rewrite exists values (optional, default true). * @return {Object} Modified object. */ exports.extend = function(target, source, rewrite) { - if (target === null || source === null) - return target; + if (!target || !source) + return target; + + if (typeof(target) !== 'object' || typeof(source) !== 'object') + return target; + + if (rewrite === undefined) + rewrite = true; + + var keys = Object.keys(source); + var i = keys.length; + + while (i--) { + var key = keys[i]; + if (rewrite || target[key] === undefined) + target[key] = exports.clone(source[key]); + } + + return target; +}; - if (typeof(target) !== OBJECT || typeof(source) !== OBJECT) - return target; +exports.extend_headers = function(first, second) { + var keys = Object.keys(first); + var headers = {}; - var keys = Object.keys(source); - var i = keys.length; + var i = keys.length; + while (i--) + headers[keys[i]] = first[keys[i]]; - while (i--) { + keys = Object.keys(second); + i = keys.length; - var key = keys[i]; + while (i--) + headers[keys[i]] = second[keys[i]]; - if (rewrite || typeof(target[key]) === UNDEFINED) - target[key] = source[key]; - } + return headers; +}; - return target; +exports.extend_headers2 = function(first, second) { + var keys = Object.keys(second); + var i = keys.length; + while (i--) + first[keys[i]] = second[keys[i]]; + return first; }; /** - * Copy values from object to object - * @param {Object} source Object source - * @param {Object} target Object target (optional) - * @return {Object} Modified object. + * Clones object + * @param {Object} obj + * @param {Object} skip Optional, can be only object e.g. { name: true, age: true }. + * @param {Boolean} skipFunctions It doesn't clone functions, optional --> default false. + * @return {Object} */ -exports.copy = function(source, target) { +global.CLONE = exports.clone = function(obj, skip, skipFunctions) { + + if (!obj) + return obj; + + var type = typeof(obj); + if (type !== 'object' || obj instanceof Date || obj instanceof Error) + return obj; + + var length; + var o; - if (typeof(target) === UNDEFINED) - return exports.extend({}, source, true); + if (obj instanceof Array) { - if (target === null || source === null) - return target; + length = obj.length; + o = new Array(length); - if (typeof(target) !== OBJECT || typeof(source) !== OBJECT) - return target; + for (var i = 0; i < length; i++) { + type = typeof(obj[i]); + if (type !== 'object' || obj[i] instanceof Date || obj[i] instanceof Error) { + if (skipFunctions && type === 'function') + continue; + o[i] = obj[i]; + continue; + } + o[i] = exports.clone(obj[i], skip, skipFunctions); + } - var keys = Object.keys(source); - var i = keys.length; + return o; + } - while (i--) { + o = {}; - var key = keys[i]; - if (typeof(target[key]) === UNDEFINED) - continue; + for (var m in obj) { - target[key] = source[key]; - } + if (skip && skip[m]) + continue; - return target; + var val = obj[m]; + + if (val instanceof Buffer) { + var copy = Buffer.alloc(val.length); + val.copy(copy); + o[m] = copy; + continue; + } + + var type = typeof(val); + if (type !== 'object' || val instanceof Date || val instanceof Error) { + if (skipFunctions && type === 'function') + continue; + o[m] = val; + continue; + } + + o[m] = exports.clone(obj[m], skip, skipFunctions); + } + + return o; }; /** - * Reduce an object - * @param {Object} source Source object. - * @param {String Array or Object} prop Other properties than these ones will be removed. - * @return {[type]} [description] + * Copy values from object to object + * @param {Object} source Object source + * @param {Object} target Object target (optional) + * @return {Object} Modified object. */ -exports.reduce = function(source, prop) { +exports.copy = function(source, target) { - if (source === null || prop === null) - return source; + if (target === undefined) + return exports.extend({}, source, true); - var type = typeof(prop); + if (!target || !source || typeof(target) !== 'object' || typeof(source) !== 'object') + return target; - if (prop instanceof Array) { - Object.keys(source).forEach(function(o) { - if (prop.indexOf(o) === -1) - delete source[o]; - }); - } + var keys = Object.keys(source); + var i = keys.length; - if (type === OBJECT) { - var obj = Object.keys(prop); - Object.keys(source).forEach(function(o) { - if (obj.indexOf(o) === -1) - delete source[o]; - }); - } + while (i--) { + var key = keys[i]; + target[key] !== undefined && (target[key] = exports.clone(source[key])); + } + + return target; +}; - return source; +/** + * Reduce an object + * @param {Object} source Source object. + * @param {String Array or Object} prop Other properties than these ones will be removed. + * @param {Boolean} reverse Reverse reducing (prop will be removed), default: false. + * @return {Object} + */ +exports.reduce = function(source, prop, reverse) { + + if (!(prop instanceof Array)) { + if (typeof(prop) === 'object') + return exports.reduce(source, Object.keys(prop), reverse); + } + + if (source instanceof Array) { + var arr = []; + for (var i = 0, length = source.length; i < length; i++) + arr.push(exports.reduce(source[i], prop, reverse)); + return arr; + } + + var output = {}; + + 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]; + } else { + if (prop.indexOf(o) !== -1) + output[o] = source[o]; + } + } + + return output; }; /** @@ -714,163 +2105,347 @@ exports.reduce = function(source, prop) { * @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 || typeof(obj) === UNDEFINED) - return obj; + if (obj == null) + return obj; - var arr = path.split('.'); - var model = obj[arr[0]]; + var arr = path.split('.'); + var model = obj[arr[0]]; - for (var i = 1; i < arr.length - 1; i++) - model = model[arr[i]]; + for (var i = 1; i < arr.length - 1; i++) + model = model[arr[i]]; - model[arr[arr.length - 1]] = typeof (fn) === FUNCTION ? fn(model[arr[arr.length - 1]]) : fn; - return obj; + model[arr[arr.length - 1]] = typeof (fn) === 'function' ? fn(model[arr[arr.length - 1]]) : fn; + return obj; }; /** * Checks if is relative url - * @param {String} url + * @param {String} url * @return {Boolean} */ exports.isRelative = function(url) { - return !(url.substring(0, 2) === '//' || url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1); + return !(url.substring(0, 2) === '//' || url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1); +}; + +/** + * Streamer method + * @param {String/Buffer} beg + * @param {String/Buffer} end + * @param {Function(value, index)} callback + */ +exports.streamer = function(beg, end, callback, skip, stream, raw) { + + if (typeof(end) === 'function') { + stream = skip; + skip = callback; + callback = end; + end = undefined; + } + + if (typeof(skip) === 'object') { + stream = skip; + skip = 0; + } + + var indexer = 0; + var buffer = Buffer.alloc(0); + var canceled = false; + var fn; + + if (skip === undefined) + skip = 0; + + 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; + fn = function(chunk) { + + if (!chunk || canceled) + return; + + 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, f); + if (index === -1) + return; + + while (index !== -1) { + + if (skip) + skip--; + else { + if (callback(raw ? buffer.slice(0, index + length) : buffer.toString('utf8', 0, index + length), indexer++) === false) + canceled = true; + } + + if (canceled) + return; + + buffer = buffer.slice(index + length); + index = buffer.indexOf(beg); + if (index === -1) + return; + } + }; + + stream && stream.on('end', () => fn(beg)); + return fn; + } + + var blength = beg.length; + var elength = end.length; + var bi = -1; + var ei = -1; + var is = false; + + fn = function(chunk) { + + if (!chunk || canceled) + return; + + CONCAT[0] = buffer; + CONCAT[1] = chunk; + buffer = Buffer.concat(CONCAT); + + if (!is) { + var f = CONCAT[0].length - beg.length; + if (f < 0) + f = 0; + bi = buffer.indexOf(beg, f); + if (bi === -1) + return; + is = true; + } + + if (is) { + ei = buffer.indexOf(end, bi + blength); + if (ei === -1) + return; + } + + while (bi !== -1) { + + if (skip) + skip--; + else { + if (callback(raw ? buffer.slice(bi, ei + elength) : buffer.toString('utf8', bi, ei + elength), indexer++) === false) + canceled = true; + } + + if (canceled) + return; + + buffer = buffer.slice(ei + elength); + is = false; + bi = buffer.indexOf(beg); + if (bi === -1) + return; + is = true; + ei = buffer.indexOf(end, bi + blength); + if (ei === -1) + return; + } + }; + + stream && stream.on('end', () => fn(end)); + 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 + * @param {String} str * @return {String} */ exports.encode = function(str) { - var type = typeof(str); + if (str == null) + return ''; - if (type === UNDEFINED) - return ''; + var type = typeof(str); + if (type !== 'string') + str = str.toString(); - if (type !== STRING) - str = str.toString(); - - return str.encode(); + return str.encode(); }; /** * HTML decode string - * @param {String} str + * @param {String} str * @return {String} */ exports.decode = function(str) { - var type = typeof(str); - - if (type === UNDEFINED) - return ''; + if (str == null) + return ''; - if (type !== STRING) - str = str.toString(); + var type = typeof(str); + if (type !== 'string') + str = str.toString(); - return str.decode(); + return str.decode(); }; /** * Checks if URL contains file extension. - * @param {String} url + * @param {String} url * @return {Boolean} */ exports.isStaticFile = function(url) { - var pattern = /\.\w{2,8}($|\?)+/g; - return pattern.test(url); -}; - -/** - * Checks if string is null or empty - * @param {String} str - * @return {Boolean} - */ -exports.isNullOrEmpty = function(str) { - - if (typeof(str) !== STRING) - return true; - - return str.length === 0; + return regexpSTATIC.test(url); }; /** * Converts Value to number - * @param {Object} obj Value to convert. - * @param {Number} def Default value (default: 0). + * @param {Object} obj Value to convert. + * @param {Number} def Default value (default: 0). * @return {Number} */ exports.parseInt = function(obj, def) { - var type = typeof(obj); - - if (type === NUMBER) - return obj; - - if (type === UNDEFINED || 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); +}; - var str = type !== STRING ? obj.toString() : obj; - return str.parseInt(def, 10); +exports.parseBool = exports.parseBoolean = function(obj, def) { + if (obj == null) + return def === undefined ? false : def; + var type = typeof(obj); + return type === 'boolean' ? obj : type === 'number' ? obj > 0 : (type !== 'string' ? obj.toString() : obj).parseBool(def); }; /** * Converts Value to float number - * @param {Object} obj Value to convert. - * @param {Number} def Default value (default: 0). + * @param {Object} obj Value to convert. + * @param {Number} def Default value (default: 0). * @return {Number} */ exports.parseFloat = function(obj, def) { - var type = typeof(obj); - - if (type === NUMBER) - return obj; - - if (type === UNDEFINED || obj === null) - return def || 0; - - var str = type !== STRING ? obj.toString() : obj; - return str.parseFloat(def); + if (obj == null || obj === '') + return def === undefined ? 0 : def; + var type = typeof(obj); + return type === 'number' ? obj : (type !== 'string' ? obj.toString() : obj).parseFloat(def); }; /** - * Check if object is Array. - * @param {Object} obj + * Check if the object is Array. + * @param {Object} obj * @return {Boolean} */ exports.isArray = function(obj) { - return obj instanceof Array; + return obj instanceof Array; }; /** - * Check if object is RegExp - * @param {Object} obj + * Check if the object is RegExp + * @param {Object} obj * @return {Boolean} */ exports.isRegExp = function(obj) { - return util.isRegExp(obj); + return obj && typeof(obj.test) === 'function' ? true : false; }; /** - * Check if object is Date - * @param {Object} obj + * Check if the object is Date + * @param {Object} obj * @return {Boolean} */ exports.isDate = function(obj) { - return util.isDate(obj); + return obj instanceof Date && !isNaN(obj.getTime()) ? true : false; +}; + +/** + * Check if the object is Date + * @param {Object} obj + * @return {Boolean} + */ +exports.isError = function(obj) { + return (obj && obj.stack) ? true : false; +}; + +/** + * Check if the value is object + * @param {Object} value + * @return {Boolean} + */ +exports.isObject = function(value) { + try { + return (value && Object.getPrototypeOf(value) === Object.prototype) ? true : false; + } catch (e) { + return false; + } }; /** * Get ContentType from file extension. - * @param {String} ext File extension. + * @param {String} ext File extension. * @return {String} */ exports.getContentType = function(ext) { - if (ext[0] === '.') - ext = ext.substring(1); - return contentTypes[ext.toLowerCase()] || 'application/octet-stream'; + if (ext[0] === '.') + ext = ext.substring(1); + return CONTENTTYPES[ext] || 'application/octet-stream'; +}; + +/** + * Get extension from filename + * @param {String} filename + * @return {String} + */ +exports.getExtension = function(filename, raw) { + var end = filename.length; + for (var i = filename.length - 1; i > 0; i--) { + var c = filename[i]; + if (c === ' ' || c === '?') + end = i; + else if (c === '.') { + c = filename.substring(i + 1, end); + return raw ? c : c.toLowerCase(); + } + else if (c === '/' || c === '\\') + return ''; + } + return ''; +}; + +/** + * Get base name from path + * @param {String} path + * @return {String} + */ +exports.getName = function(path) { + var l = path.length - 1; + var c = path[l]; + if (c === '/' || c === '\\') + path = path.substring(0, l); + var index = path.lastIndexOf('/'); + if (index !== -1) + return path.substring(index + 1); + index = path.lastIndexOf('\\'); + return index === -1 ? path : path.substring(index + 1); }; /** @@ -879,287 +2454,415 @@ exports.getContentType = function(ext) { * @param {String} type Content type (example: application/json). */ exports.setContentType = function(ext, type) { - if (ext[0] === '.') - ext = ext.substring(1); - contentTypes[ext.toLowerCase()] = type; - return true; + if (ext[0] === '.') + ext = ext.substring(1); + + if (ext.length > 8) { + var tmp = regexpSTATIC.toString().replace(/,\d+\}/, ',' + ext.length + '}').substring(1); + regexpSTATIC = new RegExp(tmp.substring(0, tmp.length - 1)); + } + + CONTENTTYPES[ext] = type; + return true; +}; + +exports.path = function(path, delimiter) { + if (!path) + path = ''; + delimiter = delimiter || '/'; + return path[path.length - 1] === delimiter ? path : path + delimiter; +}; + +exports.join = function() { + var path = ['']; + + for (var i = 0; i < arguments.length; i++) { + var current = arguments[i]; + if (!current) + continue; + if (current[0] === '/') + current = current.substring(1); + var l = current.length - 1; + if (current[l] === '/') + current = current.substring(0, l); + path.push(current); + } + + path = path.join('/'); + return !isWindows ? path : path.indexOf(':') > -1 ? path.substring(1) : path; }; /** - * Create eTag hash from text - * @param {String} text - * @param {String} version + * Prepares Windows path to UNIX like format + * @internal + * @param {String} path * @return {String} */ -exports.etag = function(text, version) { - var sum = 0; - var length = text.length; - for (var i = 0; i < length; i++) - sum += text.charCodeAt(i); - return sum.toString() + (version ? ':' + version : ''); +exports.$normalize = function(path) { + return isWindows ? path.replace(regexpPATH, '/') : path; }; -/* - Add @delimiter to end of @path - @path {String} :: filename - @delimiter {String} :: optional, default / - return {String} -*/ -exports.path = function(path, delimiter) { - delimiter = delimiter || '/'; - if (path[path.length - 1] === delimiter) - return path; - return path + delimiter; +exports.random = function(max, min) { + max = (max || 100000); + min = (min || 0); + return Math.floor(Math.random() * (max - min + 1)) + min; }; -/* - Get random number - @max {Number} - @min {Number} - return {Number} -*/ -exports.random = function(max, min) { - max = (max || 100000); - min = (min || 0); - return Math.floor(Math.random() * (max - min + 1)) + min; +function rnd() { + return Math.floor(Math.random() * 65536).toString(36); +} + +global.GUID = exports.GUID = function(max) { + max = max || 40; + var str = ''; + for (var i = 0; i < (max / 3) + 1; i++) + str += rnd(); + return str.substring(0, max); }; -/* - Create unique identifier - @max {Number} :: optional, default 40 - return {String} -*/ -exports.GUID = function(max) { +function validate_builder_default(name, value, entity) { - max = max || 40; + var type = typeof(value); - var rnd = function () { - return Math.floor(Math.random() * 65536).toString(16); - }; + if (entity.type === 12) + return value != null && type === 'object' && !(value instanceof Array); - var str = ''; - for (var i = 0; i < (max / 4) + 1; i++) - str += rnd(); + if (entity.type === 11) + return type === 'number'; - return str.substring(0, max); -}; + // Enum + KeyValue + Custom (8+9+10) + if (entity.type > 7) + return value !== undefined; -/* - Validate - @model {Object} :: object to validate - @properties {String array} - @prepare {Function} : return utils.isValid() OR {Boolean} :: true is valid - @builder {ErrorBuilder} - @resource {Function} :: function(key) return {String} - return {ErrorBuilder} -*/ -exports.validate = function(model, properties, prepare, builder, resource, path) { - - if (typeof(builder) === FUNCTION && typeof(resource) === UNDEFINED) { - resource = builder; - builder = null; - } - - var error = builder; - var current = typeof(path) === UNDEFINED ? '' : path + '.'; - var isSchema = false; - var schemaName = ''; - var definition = null; + switch (entity.subtype) { + case 'uid': + return value.isUID(); + case 'zip': + return value.isZIP(); + case 'email': + return value.isEmail(); + case 'json': + return value.isJSON(); + case 'url': + return value.isURL(); + case 'phone': + return value.isPhone(); + case 'base64': + return value.isBase64(); + } - if (!(error instanceof builders.ErrorBuilder)) - error = new builders.ErrorBuilder(resource); - - if (typeof(properties) === STRING) { - var schema = builders.validation(properties); - if (schema.length !== 0) { - schemaName = properties; - properties = schema; - isSchema = true; - definition = builders.schema(schemaName); - } else - properties = properties.replace(/\s/g, '').split(','); - } - - if (typeof(model) === UNDEFINED || model === null) - model = {}; - - if (typeof(prepare) !== FUNCTION) - throw new Error('Validate hasn\'t any method to validate properties.\nDefine delegate: framework.onValidate ...'); - - for (var i = 0; i < properties.length; i++) { - - var name = properties[i].toString(); - var value = model[name]; - var type = typeof(value); - - if (type === UNDEFINED) { - error.add(name, '@', current + name); - continue; - } else - value = (type === FUNCTION ? model[name]() : model[name]); - - if (type !== OBJECT && isSchema) { - if (builders.isJoin(definition[name])) - type = OBJECT; - } - - if (type === OBJECT && !exports.isDate(value)) { - - if (isSchema) { - var schema = builders.schema(schemaName) || null; - if (schema !== null) { - schema = schema[name] || null; - if (schema !== null) { - var isArray = schema[0] === '['; - - if (isArray) - schema = schema.substring(1, schema.length - 1); - - if (isArray) { - - if (!(value instanceof Array)) { - error.add(name, '@'); - continue; - } - - var sublength = value.length; - for (var j = 0; j < sublength; j++) - exports.validate(value[j], schema, prepare, error, resource, current + name); - - continue; - } - - exports.validate(value, schema, prepare, error, resource, current + name); - continue; - } - } - } - - //exports.validate(value, properties, prepare, error, resource, current + name); - //continue; - } - - var result = prepare(name, value, current + name); - - if (typeof(result) === UNDEFINED) - continue; - - type = typeof(result); - - if (type === STRING) { - error.add(name, result, current + name); - continue; - } - - if (type === BOOLEAN) { - if (!result) - error.add(name, '@', current + name); - continue; - } - - if (result.isValid === false) - error.add(name, result.error, current + name); - } - - return error; -}; + if (type === 'number') + return value > 0; -/* - Validation object - @isValid {Boolean} - @error {String} :: optional, default @ - return {Object} -*/ -exports.isValid = function(valid, error) { - return { isValid: valid, error: error || '@' }; + if (type === 'string' || value instanceof Array) + return value.length > 0; + + if (type === 'boolean') + return value === true; + + if (value == null) + return false; + + if (value instanceof Date) + return value.toString()[0] !== 'I'; // Invalid Date + + return true; +} + +exports.validate_builder = function(model, error, schema, path, index, fields, pluspath) { + + 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 = ''; + + if (model == null) + model = {}; + + for (var i = 0; i < properties.length; i++) { + + var name = properties[i]; + + if (fields && fields.indexOf(name) === -1) + continue; + + var TYPE = schema.schema[name]; + if (!TYPE) + continue; + + if (TYPE.can && !TYPE.can(model, model.$$workflow || EMPTYOBJECT)) + continue; + + var value = model[name]; + var type = typeof(value); + var prefix = schema.resourcePrefix ? (schema.resourcePrefix + name) : name; + + if (value === undefined) { + error.push(pluspath + name, '@', current + name, undefined, prefix); + continue; + } else if (type === 'function') + 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 (!TYPE.required) + continue; + + 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 + 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; + } + + 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 == 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); + } + + return error; }; -/* - Email address validation - @str {String} - return {Boolean} -*/ -exports.isEmail = function(str) { - return (str || '').toString().isEmail(); +/** + * Combine paths + * @return {String} + */ +exports.combine = function() { + + var p = F.directory; + + for (var i = 0, length = arguments.length; i < length; i++) { + var v = arguments[i]; + if (!v) + continue; + if (v[0] === '/') + v = v.substring(1); + + if (v[0] === '~') + p = v.substring(1); + else + p += (p[p.length - 1] !== '/' ? '/' : '') + v; + } + return exports.$normalize(p); }; -/* - URL address validation - @str {String} - return {Boolean} -*/ -exports.isURL = function(str) { - return (str || '').toString().isURL(); +/** + * Remove diacritics + * @param {String} str + * @return {String} + */ +exports.removeDiacritics = function(str) { + return str.replace(regexpDiacritics, c => DIACRITICSMAP[c] || c); }; -/* - Combine path - @arguments {String array} - return {String} -*/ -exports.combine = function() { +/** + * Simple XML parser + * @param {String} xml + * @return {Object} + */ +exports.parseXML = function(xml, replace) { - var self = this; + var beg = -1; + var end = 0; + var tmp = 0; + var current = []; + var obj = {}; + var from = -1; - if (arguments[0][0] === '~') { - arguments[0] = arguments[0].substring(1); - return path.join.apply(self, arguments); - } + while (true) { + beg = xml.indexOf('', beg + 9); + xml = xml.substring(0, beg) + xml.substring(beg + 9, end).trim().encode() + xml.substring(end + 3); + beg += 9; + } - return '.' + path.join.apply(self, arguments); -}; + beg = -1; + end = 0; -/* - @str {String} - return {String} -*/ -exports.removeDiacritics = function(str) { - var dictionaryA = ['á', 'ä', 'č', 'ď', 'é', 'ě', 'ť', 'ž', 'ú', 'ů', 'ü', 'í', 'ï', 'ô', 'ó', 'ö', 'š', 'ľ', 'ĺ', 'ý', 'ÿ', 'č', 'ř']; - var dictionaryB = ['a', 'a', 'c', 'd', 'e', 'e', 't', 'z', 'u', 'u', 'u', 'i', 'i', 'o', 'o', 'o', 's', 'l', 'l', 'y', 'y', 'c', 'r']; - var buf = ''; - var length = str.length; - for (var i = 0; i < length; i++) { - var c = str[i]; - var isUpper = false; + while (true) { + + beg = xml.indexOf('<', beg + 1); + if (beg === -1) + break; + + end = xml.indexOf('>', beg + 1); + if (end === -1) + break; + + var el = xml.substring(beg, end + 1); + var c = el[1]; + + if (c === '?' || c === '/') { + + var o = current.pop(); + + if (from === -1 || o !== el.substring(2, el.length - 1)) + continue; + + var path = (current.length ? current.join('.') + '.' : '') + o; + var value = xml.substring(from, beg).decode(); + + if (replace) + path = path.replace(REG_XMLKEY, '_'); - var index = dictionaryA.indexOf(c); - if (index === -1) { - index = dictionaryA.indexOf(c.toLowerCase()); - isUpper = true; - } + if (obj[path] === undefined) + obj[path] = value; + else if (obj[path] instanceof Array) + obj[path].push(value); + else + obj[path] = [obj[path], value]; - if (index === -1) { - buf += c; - continue; - } + from = -1; + continue; + } - c = dictionaryB[index]; - if (isUpper) - c = c.toUpperCase(); + tmp = el.indexOf(' '); + var hasAttributes = true; - buf += c; - } - return buf; + if (tmp === -1) { + tmp = el.length - 1; + hasAttributes = false; + } + + from = beg + el.length; + + var isSingle = el[el.length - 2] === '/'; + var name = el.substring(1, tmp); + + if (!isSingle) + current.push(name); + + if (!hasAttributes) + continue; + + var match = el.match(regexpXML); + if (!match) + continue; + + var attr = {}; + var length = match.length; + + for (var i = 0; i < length; i++) { + var index = match[i].indexOf('"'); + attr[match[i].substring(0, index - 1)] = match[i].substring(index + 1, match[i].length - 1).decode(); + } + + var k = current.join('.') + (isSingle ? '.' + name : '') + '[]'; + if (replace) + k = k.replace(REG_XMLKEY, '_'); + obj[k] = attr; + } + + return obj; +}; + +exports.parseJSON = function(value, date) { + try { + return JSON.parse(value, date ? jsonparser : undefined); + } catch(e) { + } +}; + +exports.parseQuery = function(value) { + return F.onParseQuery(value); }; +function jsonparser(key, value) { + return typeof(value) === 'string' && value.isJSONDate() ? new Date(value) : value; +} + /** * Get WebSocket frame * @author Jozef Gula - * @param {Number} code - * @param {Buffer or String} message - * @param {Hexa} type + * @param {Number} code + * @param {Buffer or String} message + * @param {Hexa} type * @return {Buffer} */ -exports.getWebSocketFrame = function(code, message, type) { - var messageBuffer = getWebSocketFrameMessageBytes(code, message); - var messageLength = messageBuffer.length; - var lengthBuffer = getWebSocketFrameLengthBytes(messageLength); - var frameBuffer = new Buffer(1 + lengthBuffer.length + messageLength); - frameBuffer[0] = 0x80 | type; - lengthBuffer.copy(frameBuffer, 1, 0, lengthBuffer.length); - messageBuffer.copy(frameBuffer, lengthBuffer.length + 1, 0, messageLength); - return frameBuffer; -} +exports.getWebSocketFrame = function(code, message, type, compress) { + var messageBuffer = getWebSocketFrameMessageBytes(code, message); + var lengthBuffer = getWebSocketFrameLengthBytes(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); + messageBuffer.copy(frameBuffer, lengthBuffer.length + 1, 0, messageBuffer.length); + return frameBuffer; +}; /** * Get bytes of WebSocket frame message @@ -1169,25 +2872,26 @@ exports.getWebSocketFrame = function(code, message, type) { * @return {Buffer} */ function getWebSocketFrameMessageBytes(code, message) { - var index = code === 0 ? 0 : 2; - var binary = typeof(message.readUInt8) !== UNDEFINED; - var length = message.length; - var messageBuffer = new Buffer(length + index); - for (var i = 0; i < length; i++) { - if (binary) - messageBuffer[i + index] = message[i]; - else - messageBuffer[i + index] = message.charCodeAt(i); - } + var index = code ? 2 : 0; + var binary = message instanceof Int8Array || message instanceof Buffer; + var length = message.length; + + var messageBuffer = Buffer.alloc(length + index); - if (code === 0) - return messageBuffer; + for (var i = 0; i < length; i++) { + if (binary) + messageBuffer[i + index] = message[i]; + else + messageBuffer[i + index] = message.charCodeAt(i); + } - messageBuffer[0] = (code >> 8); - messageBuffer[1] = (code); + if (code) { + messageBuffer[0] = code >> 8; + messageBuffer[1] = code; + } - return messageBuffer; + return messageBuffer; } /** @@ -1197,45 +2901,37 @@ function getWebSocketFrameMessageBytes(code, message) { * @return {Number} */ function getWebSocketFrameLengthBytes(length) { - var lengthBuffer = null; - - if (length <= 125) { - lengthBuffer = new Buffer(1); - lengthBuffer[0] = length; - return lengthBuffer; - } - - if (length <= 65535) { - lengthBuffer = new Buffer(3); - lengthBuffer[0] = 126; - lengthBuffer[1] = (length >> 8) & 255; - lengthBuffer[2] = (length) & 255; - return lengthBuffer; - } - - lengthBuffer = new Buffer(9); - - lengthBuffer[0] = 127; - lengthBuffer[1] = (length >> 56) & 255; - lengthBuffer[2] = (length >> 48) & 255; - lengthBuffer[3] = (length >> 40) & 255; - lengthBuffer[4] = (length >> 32) & 255; - lengthBuffer[5] = (length >> 24) & 255; - lengthBuffer[6] = (length >> 16) & 255; - lengthBuffer[7] = (length >> 8) & 255; - lengthBuffer[8] = (length) & 255; - - return lengthBuffer; + var lengthBuffer = null; + + if (length <= 125) { + lengthBuffer = Buffer.alloc(1); + lengthBuffer[0] = length; + return lengthBuffer; + } + + if (length <= 65535) { + lengthBuffer = Buffer.alloc(3); + lengthBuffer[0] = 126; + lengthBuffer[1] = (length >> 8) & 255; + lengthBuffer[2] = (length) & 255; + return lengthBuffer; + } + + lengthBuffer = Buffer.alloc(9); + + lengthBuffer[0] = 127; + lengthBuffer[1] = 0x00; + lengthBuffer[2] = 0x00; + lengthBuffer[3] = 0x00; + lengthBuffer[4] = 0x00; + lengthBuffer[5] = (length >> 24) & 255; + lengthBuffer[6] = (length >> 16) & 255; + lengthBuffer[7] = (length >> 8) & 255; + lengthBuffer[8] = (length) & 255; + + return lengthBuffer; } -/* - Get distance (KM) between two coordinates - @lat1 {Number} - @lot1 {Number} - @lat2 {Number} - @lot1 {Number} - return {Number} -*/ /** * GPS distance in KM * @param {Number} lat1 @@ -1245,1734 +2941,4129 @@ function getWebSocketFrameLengthBytes(length) { * @return {Number} */ exports.distance = function(lat1, lon1, lat2, lon2) { - var R = 6371; - var dLat = (lat2 - lat1).toRad(); - var dLon = (lon2 - lon1).toRad(); - var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1.toRad()) * Math.cos(lat2.toRad()) * Math.sin(dLon / 2) * Math.sin(dLon / 2); - var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return (R * c).floor(3); + var R = 6371; + var dLat = (lat2 - lat1).toRad(); + var dLon = (lon2 - lon1).toRad(); + var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1.toRad()) * Math.cos(lat2.toRad()) * Math.sin(dLon / 2) * Math.sin(dLon / 2); + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return (R * c).floor(3); }; +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(); + filelist.onFilter = function(filename, is) { + return is ? true : filename.toLowerCase().indexOf(tmp) !== -1; + }; + } else if (exports.isRegExp(filter)) { + tmp = filter; + 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} callback Callback - * @param {Function} filter Custom filter (optional). + * @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) { - var filelist = new FileList(); - filelist.onComplete = callback; - filelist.onFilter = filter || null; - filelist.walk(path); + ls(path, callback, false, filter); }; -/* - @type {String} - @value {Number} - return {Date} -*/ -Date.prototype.add = function(type, value) { - var self = this; - var dt = new Date(self.getTime()); - - switch(type) { - case 's': - case 'ss': - case 'second': - case 'seconds': - dt.setSeconds(dt.getSeconds() + value); - return dt; - case 'm': - case 'mm': - case 'minute': - case 'minutes': - dt.setMinutes(dt.getMinutes() + value); - return dt; - case 'h': - case 'hh': - case 'hour': - case 'hours': - dt.setHours(dt.getHours() + value); - return dt; - case 'd': - case 'dd': - case 'day': - case 'days': - dt.setDate(dt.getDate() + value); - return dt; - case 'M': - case 'MM': - case 'month': - case 'months': - dt.setMonth(dt.getMonth() + value); - return dt; - case 'y': - case 'yyyy': - case 'year': - case 'years': - dt.setFullYear(dt.getFullYear() + value); - return dt; - } - return dt; -}; - -/* - Format date to string - @format {String} - return {String} -*/ -Date.prototype.format = function(format) { - var self = this; - - var h = self.getHours(); - var m = self.getMinutes().toString(); - var s = self.getSeconds().toString(); - var M = (self.getMonth() + 1).toString(); - var yyyy = self.getFullYear().toString(); - var d = self.getDate().toString(); - - var a = 'AM'; - var H = h.toString(); +/** + * 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). + */ +exports.ls2 = function(path, callback, filter) { + ls(path, callback, true, filter); +}; +DP.setTimeZone = function(timezone) { + var dt = this.toLocaleString('en-US', { timeZone: timezone, hour12: false, dateStyle: 'short', timeStyle: 'short' }); + return new Date(Date.parse(dt)); +}; - if (h >= 12) { - h -= 12; - a = 'PM'; - } +DP.add = function(type, value) { + + var self = this; + + if (type.constructor === Number) + return new Date(self.getTime() + (type - type % 1)); + + if (value === undefined) { + var arr = type.split(' '); + type = arr[1]; + value = exports.parseInt(arr[0]); + } + + var dt = new Date(self.getTime()); + + switch(type) { + case 's': + case 'ss': + case 'sec': + case 'second': + case 'seconds': + dt.setUTCSeconds(dt.getUTCSeconds() + value); + return dt; + case 'm': + case 'mm': + case 'minute': + case 'min': + case 'minutes': + dt.setUTCMinutes(dt.getUTCMinutes() + value); + return dt; + case 'h': + case 'hh': + case 'hour': + case 'hours': + dt.setUTCHours(dt.getUTCHours() + value); + return dt; + case 'd': + case 'dd': + case 'day': + case 'days': + dt.setUTCDate(dt.getUTCDate() + value); + return dt; + case 'w': + case 'ww': + case 'week': + case 'weeks': + dt.setUTCDate(dt.getUTCDate() + (value * 7)); + return dt; + case 'M': + case 'MM': + case 'month': + case 'months': + dt.setUTCMonth(dt.getUTCMonth() + value); + return dt; + case 'y': + case 'yyyy': + case 'year': + case 'years': + dt.setUTCFullYear(dt.getUTCFullYear() + value); + return dt; + } + return dt; +}; - if (h === 0) - h = 12; +/** + * Date difference + * @param {Date/Number/String} date Optional. + * @param {String} type Date type: minutes, seconds, hours, days, months, years + * @return {Number} + */ +DP.diff = function(date, type) { + + if (arguments.length === 1) { + type = date; + date = Date.now(); + } else { + var to = typeof(date); + if (to === 'string') + date = Date.parse(date); + else if (exports.isDate(date)) + date = date.getTime(); + } + + var r = this.getTime() - date; + + switch (type) { + case 's': + case 'ss': + case 'second': + case 'seconds': + return Math.ceil(r / 1000); + case 'm': + case 'mm': + case 'minute': + case 'minutes': + return Math.ceil((r / 1000) / 60); + case 'h': + case 'hh': + case 'hour': + case 'hours': + return Math.ceil(((r / 1000) / 60) / 60); + case 'd': + case 'dd': + case 'day': + case 'days': + return Math.ceil((((r / 1000) / 60) / 60) / 24); + case 'M': + case 'MM': + case 'month': + case 'months': + // avg: 28 days per month + return Math.ceil((((r / 1000) / 60) / 60) / (24 * 28)); + + case 'y': + case 'yyyy': + case 'year': + case 'years': + // avg: 28 days per month + return Math.ceil((((r / 1000) / 60) / 60) / (24 * 28 * 12)); + } + + return NaN; +}; - h = h.toString(); +DP.extend = function(date) { + var dt = new Date(this); + var match = date.match(regexpDATE); - var hh = h.padLeft(2, '0'); - var HH = H.padLeft(2, '0'); - var mm = m.padLeft(2, '0'); - var ss = s.padLeft(2, '0'); - var MM = M.padLeft(2, '0'); - var dd = d.padLeft(2, '0'); - var yy = yyyy.substring(2); + if (!match) + return dt; - return format.replace(/yyyy/g, yyyy).replace(/yy/g, yy).replace(/MM/g, MM).replace(/M/g, M).replace(/dd/g, dd).replace(/d/g, d).replace(/HH/g, HH).replace(/H/g, H).replace(/hh/g, hh).replace(/h/g, h).replace(/mm/g, mm).replace(/m/g, m).replace(/ss/g, ss).replace(/s/g, ss).replace(/a/g, a); -}; + for (var i = 0, length = match.length; i < length; i++) { + var m = match[i]; + var arr, tmp; -if (!String.prototype.trim) { - String.prototype.trim = function() { - return this.replace(regexpTRIM, ''); - }; -} + if (m.indexOf(':') !== -1) { + + arr = m.split(':'); + tmp = +arr[0]; + tmp >= 0 && dt.setUTCHours(tmp); + + if (arr[1]) { + tmp = +arr[1]; + tmp >= 0 && dt.setUTCMinutes(tmp); + } + + if (arr[2]) { + tmp = +arr[2]; + tmp >= 0 && dt.setUTCSeconds(tmp); + } + + continue; + } + + if (m.indexOf('-') !== -1) { + arr = m.split('-'); + + tmp = +arr[0]; + tmp && dt.setUTCFullYear(tmp); + + if (arr[1]) { + tmp = +arr[1]; + tmp >= 0 && dt.setUTCMonth(tmp - 1); + } + + if (arr[2]) { + tmp = +arr[2]; + tmp >= 0 && dt.setUTCDate(tmp); + } + + continue; + } + + if (m.indexOf('.') !== -1) { + arr = m.split('.'); + + if (arr[2]) { + tmp = +arr[2]; + !isNaN(tmp) && dt.setUTCFullYear(tmp); + } + + if (arr[1]) { + tmp = +arr[1]; + !isNaN(tmp) && dt.setUTCMonth(tmp - 1); + } + + tmp = +arr[0]; + !isNaN(tmp) && dt.setUTCDate(tmp); + + continue; + } + } + + return dt; +}; /** - * Checks if string starts with the text - * @see {@link http://docs.totaljs.com/String.prototype/#String.prototype.startsWith|Documentation} - * @param {String} text Text to find. - * @param {Boolean} ignoreCase Ingore case sensitive. - * @return {Boolean} + * Compare dates + * @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 */ -String.prototype.startsWith = function(text, ignoreCase) { +DP.compare = function(date) { + + var self = this; + var r = self.getTime() - date.getTime(); - var self = this; - var length = text.length; - var tmp = self.substring(0, length); + if (r === 0) + return 0; - if (ignoreCase) - return tmp.length === length && tmp.toLowerCase() === text.toLowerCase(); + if (r < 0) + return -1; - return tmp.length === length && tmp === text; + return 1; }; /** - * Checks if string ends with the text - * @see {@link http://docs.totaljs.com/String.prototype/#String.prototype.endsWith|Documentation} - * @param {String} text Text to find. - * @param {Boolean} ignoreCase Ingore case sensitive. - * @return {Boolean} + * Compare two dates + * @param {String or Date} d1 + * @param {String or Date} d2 + * @return {Number} Results: -1 = @d1 is earlier than @d2, 0 = @d1 is same as @d2, 1 = @d1 is later than @d2 */ -String.prototype.endsWith = function(text, ignoreCase) { - var self = this; - var length = text.length; - var tmp = self.substring(self.length - length); +Date.compare = function(d1, d2) { - if (ignoreCase) - return tmp.length === length && tmp.toLowerCase() === text.toLowerCase(); + if (typeof(d1) === 'string') + d1 = d1.parseDate(); - return tmp.length === length && tmp === text; + if (typeof(d2) === 'string') + d2 = d2.parseDate(); + return d1.compare(d2); }; -String.prototype.replacer = function(find, text) { - var self = this; - var beg = self.indexOf(find); - if (beg === -1) - return self; - return self.substring(0, beg) + text + self.substring(beg + find.length); +/** + * Format datetime + * @param {String} format + * @return {String} + */ +DP.format = function(format, resource) { + + if (!format) + 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); + + var key = format; + var half = false; + + if (format && format[0] === '!') { + half = true; + format = format.substring(1); + } + + var beg = '\'+'; + var end = '+\''; + var before = []; + + var ismm = false; + var isdd = false; + var isww = false; + + 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; + return beg + '(F.resource(resource, mm) || mm).substring(0, 3)' + end; + case 'MMMM': + ismm = true; + return beg + '(F.resource(resource, mm) || mm)' + end; + case 'MM': + return beg + '(d.getMonth() + 1).toString().padLeft(2, \'0\')' + end; + 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': + return beg + (half ? 'framework_utils.$pmam(d.getHours()).toString().padLeft(2, \'0\')' : 'd.getHours().toString().padLeft(2, \'0\')') + end; + case 'H': + case 'h': + return beg + (half ? 'framework_utils(d.getHours())' : 'd.getHours()') + end; + case 'mm': + return beg + 'd.getMinutes().toString().padLeft(2, \'0\')' + end; + case 'm': + return beg + 'd.getMinutes()' + end; + case 'ss': + return beg + 'd.getSeconds().toString().padLeft(2, \'0\')' + end; + case 's': + return beg + 'd.getSeconds()' + end; + case 'w': + case 'ww': + isww = true; + return beg + (key === 'ww' ? 'ww.toString().padLeft(2, \'0\')' : 'ww') + end; + case 'a': + var b = "'PM':'AM'"; + return beg + '(d.getHours() >= 12 ? ' + b + ')' + end; + } + }); + + 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,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); }; -/* - Hash string - @type {String} :: optional, default empty - return {String} -*/ -String.prototype.hash = function(type) { - var str = this; - switch ((type || '').toLowerCase()) { - case 'md5': - return str.md5(); - case 'sha1': - return str.sha1(); - case 'sha256': - return str.sha256(); - case 'sha512': - return str.sha512(); - default: - return str.split('').reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0); - } -}; - -/* - Count text in string - @text {String} - return {Number} -*/ -String.prototype.count = function(text) { - var index = 0; - var count = 0; - do { +exports.$pmam = function(value) { + return value >= 12 ? value - 12 : value; +}; - index = this.indexOf(text, index + text.length); +DP.toUTC = function(ticks) { + var dt = this.getTime() + this.getTimezoneOffset() * 60000; + return ticks ? dt : new Date(dt); +}; - if (index > 0) - count++; +// +v2.2.0 parses JSON dates as dates and this is the fallback for backward compatibility +DP.parseDate = function() { + return this; +}; - } while (index > 0); - return count; +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] === ':'; }; -String.prototype.parseDate = function() { - return new Date(this); +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; }; -/* - Contain string a array values? - @value {String or String Array} - @mustAll {Boolean} :: optional (default false), String must contains all items in String array - return {Boolean} -*/ -String.prototype.contains = function(value, mustAll) { +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)); +} - var str = this; +function $urlajax(text) { + return text.substring(0, text.length - 1) + CONF.default_root; +} - if (typeof(value) === STRING) - return str.indexOf(value, typeof(mustAll) === NUMBER ? mustAll : 0) !== -1; +function $urlmaker(text) { + var c = text[4]; + return CONF.default_root ? CONF.default_root : (c || ''); +} - var length = value.length; +if (!SP.trim) { + SP.trim = function() { + return this.replace(regexpTRIM, ''); + }; +} - for (var i = 0; i < length; i++) { - var exists = str.indexOf(value[i]) !== -1; - if (mustAll) { - if (!exists) - return false; - } else if (exists) - return true; - } +if (!SP.replaceAt) { + SP.replaceAt = function(index, character) { + return this.substr(0, index) + character + this.substr(index + character.length); + }; +} - return mustAll; +/** + * Checks if the string starts with the text + * @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} + */ +SP.startsWith = function(text, ignoreCase) { + var self = this; + var length = text.length; + var tmp; + + if (ignoreCase === true) { + tmp = self.substring(0, length); + return tmp.length === length && tmp.toLowerCase() === text.toLowerCase(); + } + + if (ignoreCase) + tmp = self.substr(ignoreCase, length); + else + tmp = self.substring(0, length); + + return tmp.length === length && tmp === text; }; -/* - Parse configuration from string - return {Object} -*/ -String.prototype.configuration = function() { +/** + * Checks if the string ends with the text + * @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} + */ +SP.endsWith = function(text, ignoreCase) { + var self = this; + var length = text.length; + var tmp; + + if (ignoreCase === true) { + tmp = self.substring(self.length - length); + return tmp.length === length && tmp.toLowerCase() === text.toLowerCase(); + } + + if (ignoreCase) + tmp = self.substr((self.length - ignoreCase) - length, length); + else + tmp = self.substring(self.length - length); + + return tmp.length === length && tmp === text; +}; - var arr = this.split('\n'); - var length = arr.length; - var obj = {}; +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)); +}; - for (var i = 0; i < length; i++) { +/** + * Hash string + * @param {String} type Hash type. + * @param {String} salt Optional, salt. + * @return {String} + */ +SP.hash = function(type, salt) { + var str = salt ? this + salt : this; + switch (type) { + case 'md5': + return str.md5(); + case 'sha1': + return str.sha1(); + case 'sha256': + return str.sha256(); + case 'sha512': + return str.sha512(); + case 'crc32': + return str.crc32(); + case 'crc32unsigned': + return str.crc32(true); + default: + var val = string_hash(str); + return type === true ? val >>> 0 : val; + } +}; - var str = arr[i]; +global.HASH = function(value, type) { + return value.hash(type ? type : true); +}; - if (str === '' || str[0] === '#') - continue; +SP.makeid = function() { + return this.hash(true).toString(16); +}; - if (str.substring(0, 2) === '//') - continue; +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; +}; - var index = str.indexOf(':'); - if (index === -1) - continue; +function string_hash(s, convert) { + var hash = 0; + if (s.length === 0) + return convert ? '' : hash; + for (var i = 0, l = s.length; i < l; i++) { + var char = s.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash |= 0; + } + return hash; +} - obj[str.substring(0, index).trim()] = str.substring(index + 1).trim(); - } +SP.count = function(text) { + var index = 0; + var count = 0; + do { + index = this.indexOf(text, index + text.length); + if (index > 0) + count++; + } while (index > 0); + return count; +}; - return obj; +SP.parseXML = function(replace) { + return F.onParseXML(this, replace); }; -/* - @arguments {Object array} - return {String} -*/ -String.prototype.format = function() { - var formatted = this; - var length = arguments.length; - for (var i = 0; i < length; i++) { - var regexp = new RegExp('\\{' + i + '\\}', 'gi'); - formatted = formatted.replace(regexp, arguments[i]); - } - return formatted; +SP.parseJSON = function(date) { + return exports.parseJSON(this, date); }; -String.prototype.encode = function() { - return this.replace(/\&/g, '&').replace(/\>/g, '>').replace(/\').replace(/\</g, '<').replace(/\"/g, '"').replace(/&/g, '&'); +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; }; -String.prototype.urlEncode = function() { - return encodeURIComponent(this); +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; }; -String.prototype.urlDecode = function() { - return decodeURIComponent(this); +SP.parseTerminal = function(fields, fn, skip, take) { + + var lines = this.split('\n'); + + if (typeof(fields) === 'function') { + take = skip; + skip = fn; + fn = fields; + parseTerminal2(lines, fn, skip, take); + return this; + } + + if (skip === undefined) + skip = 0; + if (take === undefined) + take = lines.length; + + var headers = []; + var indexer = 0; + var line = lines[0]; + + if (!line) { + line = lines[1]; + skip++; + } + + if (!line) { + line = lines[2]; + skip++; + } + + if (!line) + return this; + + var fieldslength = fields.length; + var tmp; + + for (var i = 0, length = fieldslength; i < length; i++) { + var field = fields[i]; + + var beg = -1; + var end = -1; + var type = typeof(field); + + if (type === 'object' && field.test) { + tmp = line.match(field); + if (tmp) { + beg = tmp.index; + end = beg + tmp.toString().length; + } else { + beg = -1; + end = -1; + } + } else if (type === 'string') { + tmp = line.indexOf(field); + if (tmp === -1) { + beg = -1; + end = -1; + } else { + beg = tmp; + end = line.indexOf(' ', beg + field.length); + } + } + + headers.push({ beg: beg, end: end }); + } + + for (var i = skip + 1, length = skip + 1 + take; i < length; i++) { + + var line = lines[i]; + if (!line) + continue; + + var arr = []; + var is = false; + var beg; + + for (var j = 0; j < fieldslength; j++) { + var header = headers[j]; + if (header.beg !== -1) { + is = true; + beg = 0; + + for (var k = header.beg; k > -1; k--) { + if (line[k] === ' ') { + beg = k + 1; + break; + } + } + + arr.push(line.substring(beg, header.end === -1 ? undefined : header.end).trim()); + } else + arr.push(''); + } + + is && fn(arr, indexer++, length, i); + } + + return this; }; -/* - Simple templating :: Hello {name}, your score: {score}, your price: {price | ### ###.##}, date: {date | dd.MM.yyyy} - @obj {Object} - return {String} -*/ -String.prototype.params = function(obj) { - var formatted = this; +function parseTerminal2(lines, fn, skip, take) { + var indexer = 0; + + if (skip === undefined) + skip = 0; + if (take === undefined) + take = lines.length; + + for (var i = skip, length = skip + take; i < length; i++) { + var line = lines[i]; + if (!line) + continue; + var m = line.match(regexpTERMINAL); + m && fn(m, indexer++, length, i); + } +} - if (typeof(obj) === UNDEFINED || obj === null) - return formatted; +function parseDateFormat(format, val) { - var reg = /\{[^}\n]*\}/g; - var match = formatted.match(reg); + 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 }; - if (match === null) - return formatted; + for (var i = 0; i < format.length; i++) { - var length = match.length; + var c = format[i]; - for (var i = 0; i < length; i++) { - var prop = match[i]; + if (!allowed[c]) + continue; - var isEncode = false; - var name = prop.substring(1, prop.length - 1).trim(); + if (prev !== c) { + prevformat && tmpformat.push(prevformat); + prevformat = c; + prev = c; + } else + prevformat += c; + } - var format = ''; - var index = name.indexOf('|'); + prev = ''; - if (index !== -1) { - format = name.substring(index + 1, name.length).trim(); - name = name.substring(0, index).trim(); - } + for (var i = 0; i < val.length; i++) { + var code = val.charCodeAt(i); + if (code >= 48 && code <= 57) + prev += val[i]; + } - if (prop.substring(0, 2) === '{!') - name = name.substring(1); - else - isEncode = true; + prevformat && tmpformat.push(prevformat); - var val; + 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; + } - if (name.indexOf('.') !== -1) { - var arr = name.split('.'); + var dt = {}; - if (arr.length === 2) - val = obj[arr[0]][arr[1]]; - else if (arr.length === 3) - val = obj[arr[0]][arr[1]][arr[3]]; - else if (arr.length === 4) - val = obj[arr[0]][arr[1]][arr[3]][arr[4]]; - else if (arr.length === 5) - val = obj[arr[0]][arr[1]][arr[3]][arr[4]][arr[5]]; + for (var i = 0; i < tmpformat.length; i++) { + var type = tmpformat[i]; + if (tmp[i]) + dt[type[0]] = +tmp[i]; + } - } else - val = name.length === 0 ? obj : obj[name]; + var h = dt.h || dt.H; - if (typeof(val) === FUNCTION) - val = val(index); + if (h != null) { + var ampm = val.match(REG_TIME); + if (ampm && ampm[0].toLowerCase() === 'pm') + h += 12; + } - if (typeof(val) === UNDEFINED) - continue; + 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); +} - if (format.length > 0) { +SP.parseDate = function(format) { + + if (format) + return parseDateFormat(format, this); + + var self = this.trim(); + var lc = self.charCodeAt(self.length - 1); + + // Classic date + if (lc === 41) + return new Date(self); + + // JSON format + if (lc === 90) + return new Date(Date.parse(self)); + + var arr = self.indexOf(' ') === -1 ? self.split('T') : self.split(' '); + var index = arr[0].indexOf(':'); + var length = arr[0].length; + + if (index !== -1) { + var tmp = arr[1]; + arr[1] = arr[0]; + arr[0] = tmp; + } + + if (arr[0] === undefined) + arr[0] = ''; + + var noTime = arr[1] === undefined ? true : arr[1].length === 0; + + for (var i = 0; i < length; i++) { + var c = arr[0].charCodeAt(i); + if (c === 45 || c === 46 || (c > 47 && c < 58)) + continue; + if (noTime) + return new Date(self); + } + + if (arr[1] === undefined) + arr[1] = '00:00:00'; + + var firstDay = arr[0].indexOf('-') === -1; + + var date = (arr[0] || '').split(firstDay ? '.' : '-'); + var time = (arr[1] || '').split(':'); + var parsed = []; + + if (date.length < 4 && time.length < 2) + return new Date(self); + + index = (time[2] || '').indexOf('.'); + + // milliseconds + if (index !== -1) { + time[3] = time[2].substring(index + 1); + time[2] = time[2].substring(0, index); + } else + time[3] = '0'; + + parsed.push(+date[firstDay ? 2 : 0]); // year + parsed.push(+date[1]); // month + parsed.push(+date[firstDay ? 0 : 2]); // day + parsed.push(+time[0]); // hours + parsed.push(+time[1]); // minutes + parsed.push(+time[2]); // seconds + parsed.push(+time[3]); // miliseconds + + var def = new Date(); + + for (var i = 0, length = parsed.length; i < length; i++) { + if (isNaN(parsed[i])) + parsed[i] = 0; + + var value = parsed[i]; + if (value !== 0) + continue; + + switch (i) { + case 0: + if (value <= 0) + parsed[i] = def.getFullYear(); + break; + case 1: + if (value <= 0) + parsed[i] = def.getMonth() + 1; + break; + case 2: + if (value <= 0) + parsed[i] = def.getDate(); + break; + } + } + + return new Date(parsed[0], parsed[1] - 1, parsed[2], parsed[3], parsed[4] - NOW.getTimezoneOffset(), parsed[5]); +}; - var type = typeof(val); - if (type === STRING) { - var max = parseInt(format, 10); - if (!isNaN(max)) - val = val.max(max + 3, '...'); +SP.parseDateExpiration = function() { + var self = this; - } else if (type === NUMBER || util.isDate(val)) - val = val.format(format); - } + var arr = self.split(' '); + var dt = new Date(); + var length = arr.length; - val = val.toString().dollar(); - formatted = formatted.replace(prop, isEncode ? exports.encode(val) : val); - } + for (var i = 0; i < length; i += 2) { + var num = arr[i].parseInt(); + if (num === 0) + continue; + var type = arr[i + 1]; + if (type) + dt = dt.add(type, num); + } - return formatted; + return dt; }; -/* - Set max length of string - @length {Number} - @chars {String} :: optional, default ... - return {String} -*/ -String.prototype.max = function(length, chars) { - var str = this; - chars = chars || '...'; - return str.length > length ? str.substring(0, length - chars.length) + chars : str; +SP.contains = function(value, mustAll) { + var str = this; + + if (typeof(value) === 'string') + return str.indexOf(value, typeof(mustAll) === 'number' ? mustAll : 0) !== -1; + + for (var i = 0, length = value.length; i < length; i++) { + var exists = str.indexOf(value[i]) !== -1; + if (mustAll) { + if (!exists) + return false; + } else if (exists) + return true; + } + + return mustAll; }; -String.prototype.isJSON = function() { - var self = this; - if (self.length <= 1) - return false; - var a = self[0]; - var b = self[self.length - 1]; - return (a === '"' && b === '"') || (a === '[' && b === ']') || (a === '{' && b === '}'); +/** + * Same functionality as as String.localeCompare() but this method works with latin. + * @param {String} value + * @return {Number} + */ +SP.localeCompare2 = function(value) { + return COMPARER(this, value); }; -String.prototype.isURL = function() { - var str = this; - if (str.length <= 7) - return false; - return regexpUrl.test(str); +var configurereplace = function(text) { + var val = CONF[text.substring(1, text.length - 1)]; + return val == null ? '' : val; }; -String.prototype.isEmail = function() { - var str = this; - if (str.length <= 4) - return false; +SP.env = function() { + return this.replace(regexpCONFIGURE, configurereplace); +}; - if (str[0] === '.' || str[str.length - 1] === '.') - return false; +/** + * Parse configuration from a string + * @param {Object} def + * @onerr {Function} error handling + * @return {Object} + */ +SP.parseConfig = function(def, onerr) { + + if (typeof(def) === 'function') { + onerr = def; + def = null; + } + + var arr = this.split('\n'); + var length = arr.length; + var obj = def ? exports.extend({}, def) : {}; + var subtype; + var name; + var index; + var value; + + for (var i = 0; i < length; i++) { + + var str = arr[i]; + if (!str || str[0] === '#' || str.substring(0, 2) === '//') + continue; + + index = str.indexOf(':'); + if (index === -1) { + index = str.indexOf('\t:'); + if (index === -1) + continue; + } + + name = str.substring(0, index).trim(); + value = str.substring(index + 2).trim(); + + index = name.indexOf('('); + if (index !== -1) { + subtype = name.substring(index + 1, name.indexOf(')')).trim().toLowerCase(); + name = name.substring(0, index).trim(); + } else + subtype = ''; + + switch (subtype) { + case 'string': + obj[name] = value; + break; + case 'number': + case 'float': + case 'double': + case 'currency': + obj[name] = value.isNumber(true) ? value.parseFloat2() : value.parseInt2(); + break; + case 'boolean': + case 'bool': + obj[name] = (/true|on|1|enabled/i).test(value); + break; + case 'config': + obj[name] = CONF[value]; + break; + case 'eval': + case 'object': + case 'array': + 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); + break; + case 'env': + case 'environment': + obj[name] = process.env[value]; + break; + case 'date': + case 'time': + case 'datetime': + obj[name] = value.parseDate(); + break; + case 'random': + obj[name] = GUID((value || '0').parseInt() || 10); + break; + default: + obj[name] = value; + break; + } + } + + return obj; +}; - return regexpMail.test(str); +SP.format = function() { + var arg = arguments; + return this.replace(regexpSTRINGFORMAT, function(text) { + var value = arg[+text.substring(1, text.length - 1)]; + return value == null ? '' : value; + }); }; -/* - @def {Number} :: optional, default 0 - return {Number} -*/ -String.prototype.parseInt = function(def) { - var num = 0; - var str = this; +SP.encryptUID = function(key) { + return exports.encryptUID(this, key); +}; - if (str.substring(0, 1) === '0') - num = parseInt(str.replace(/\s/g, '').substring(1), 10); - else - num = parseInt(str.replace(/\s/g, ''), 10); +SP.decryptUID = function(key) { + return exports.decryptUID(this, key); +}; - if (isNaN(num)) - return def || 0; +SP.encode = function() { + var output = ''; + for (var i = 0, length = this.length; i < length; i++) { + var c = this[i]; + switch (c) { + case '<': + output += '<'; + break; + case '>': + output += '>'; + break; + case '"': + output += '"'; + break; + case '\'': + output += '''; + break; + case '&': + output += '&'; + break; + default: + output += c; + break; + } + } + return output; +}; - return num; +SP.decode = function() { + return this.replace(regexpDECODE, function(s) { + if (s.charAt(1) !== '#') + return ALPHA_INDEX[s] || s; + var code = s[2].toLowerCase() === 'x' ? parseInt(s.substr(3), 16) : parseInt(s.substr(2)); + return !code || code < -32768 || code > 65535 ? '' : String.fromCharCode(code); + }); }; -/* - @def {Number} :: optional, default 0 - return {Number} -*/ -String.prototype.parseFloat = function(def) { - var num = 0; - var str = this; +SP.urlEncode = function() { + return encodeURIComponent(this); +}; - if (str.substring(0, 1) === '0') - num = parseFloat(str.replace(/\s/g, '').substring(1).replace(',', '.')); - else - num = parseFloat(str.replace(/\s/g, '').replace(',', '.')); +SP.urlDecode = function() { + return decodeURIComponent(this); +}; - if (isNaN(num)) - return def || 0; +SP.arg = function(obj, encode, def) { + if (typeof(encode) === 'string') + def = encode; + return this.replace(regexpARG, function(text) { + // 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; + }); +}; - return num; +SP.params = function(obj) { + + OBSOLETE('String.params()', 'The method is deprecated instead of it use F.viewCompile() or String.format().'); + + var formatted = this; + if (obj == null) + return formatted; + + return formatted.replace(regexpPARAM, function(prop) { + + var isEncode = false; + var name = prop.substring(2, prop.length - 2).trim(); + + var format = ''; + var index = name.indexOf('|'); + + if (index !== -1) { + format = name.substring(index + 1, name.length).trim(); + name = name.substring(0, index).trim(); + } + + if (name[0] === '!') + name = name.substring(1); + else + isEncode = true; + + var val; + + if (name.indexOf('.') !== -1) { + var arr = name.split('.'); + if (arr.length === 2) { + if (obj[arr[0]]) + val = obj[arr[0]][arr[1]]; + } else if (arr.length === 3) { + if (obj[arr[0]] && obj[arr[0]][arr[1]]) + val = obj[arr[0]][arr[1]][arr[2]]; + } else if (arr.length === 4) { + if (obj[arr[0]] && obj[arr[0]][arr[1]] && obj[arr[0]][arr[1]][arr[2]]) + val = obj[arr[0]][arr[1]][arr[2]][arr[3]]; + } else if (arr.length === 5) { + if (obj[arr[0]] && obj[arr[0]][arr[1]] && obj[arr[0]][arr[1]][arr[2]] && obj[arr[0]][arr[1]][arr[2]][arr[3]]) + val = obj[arr[0]][arr[1]][arr[2]][arr[3]][arr[4]]; + } + } else + val = name.length ? obj[name] : obj; + + if (typeof(val) === 'function') + val = val(index); + + if (val === undefined) + return prop; + + if (format.length) { + var type = typeof(val); + if (type === 'string') { + var max = +format; + if (!isNaN(max)) + val = val.max(max + 3, '...'); + + } else if (type === 'number' || exports.isDate(val)) { + if (format.isNumber()) + format = +format; + val = val.format(format); + } + } + + val = val.toString(); + return isEncode ? exports.encode(val) : val; + }); }; -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); - else - result += self[i]; - } - return result; +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.fromUnicode = function() { +SP.isJSON = function() { + var self = this; + if (self.length <= 1) + return false; + + var l = self.length - 1; + var a; + var b; + var i = 0; + + while (true) { + a = self[i++]; + if (a === ' ' || a === '\n' || a === '\r' || a === '\t') + continue; + break; + } + + while (true) { + b = self[l--]; + if (b === ' ' || b === '\n' || b === '\r' || b === '\t') + continue; + break; + } + + return (a === '"' && b === '"') || (a === '[' && b === ']') || (a === '{' && b === '}') || (a.charCodeAt(0) > 47 && b.charCodeAt(0) < 57); +}; - var str = this.replace(/\\u([\d\w]{4})/gi, function (match, v) { - return String.fromCharCode(parseInt(v, 16)); - }); +SP.isURL = function() { + return this.length <= 7 ? false : F.validators.url.test(this); +}; - return unescape(str); +SP.isZIP = function() { + return F.validators.zip.test(this); }; -String.prototype.sha1 = function(salt) { - var hash = crypto.createHash('sha1'); - hash.update(this + (salt || ''), ENCODING); - return hash.digest('hex'); +SP.isEmail = function() { + return this.length <= 4 ? false : F.validators.email.test(this); }; -String.prototype.sha256 = function(salt) { - var hash = crypto.createHash('sha256'); - hash.update(this + (salt || ''), ENCODING); - return hash.digest('hex'); +SP.isPhone = function() { + return this.length < 6 ? false : F.validators.phone.test(this); }; -String.prototype.sha512 = function(salt) { - var hash = crypto.createHash('sha512'); - hash.update(this + (salt || ''), ENCODING); - return hash.digest('hex'); +SP.isBase64 = function() { + var str = this; + return str.length % 4 === 0 && regexpBASE64.test(str); }; -String.prototype.md5 = function(salt) { - var hash = crypto.createHash('md5'); - hash.update(this + (salt || ''), ENCODING); - return hash.digest('hex'); +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; }; -/* - @key {String} - @isUnique {Boolean} - return {String} -*/ -String.prototype.encrypt = function(key, isUnique) { - var str = '0' + this; - var data_count = str.length; - var key_count = key.length; - var change = str[data_count - 1]; - var random = isUnique ? exports.random(120) + 40 : 65; - var count = data_count + (random % key_count); - var values = []; - var index = 0; - - values[0] = String.fromCharCode(random); - var counter = this.length + key.length; - - for (var i = count - 1; i > 0; i--) { - index = str.charCodeAt(i % data_count); - values[i] = String.fromCharCode(index ^ (key.charCodeAt(i % key_count) ^ random)); - } - - var hash = new Buffer(counter + '=' + values.join(''), ENCODING).toString('base64').replace(/\//g, '-').replace(/\+/g, '_'); - index = hash.indexOf('='); - if (index > 0) - return hash.substring(0, index); - - return hash; -}; - -/* - @key {String} - return {String} -*/ -String.prototype.decrypt = function(key) { +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 values = this.replace(/\-/g, '/').replace(/\_/g, '+'); - var mod = values.length % 4; + var arr = this.split(regexpLINES); + var obj = {}; - if (mod > 0) { - for (var i = 0; i < mod; i++) - values += '='; - } + for (var i = 0; i < arr.length; i++) { + var line = arr[i]; + if (!line || line.substring(0, 2) === '//' || line[0] === '#') + continue; - values = new Buffer(values, 'base64').toString(ENCODING); + var index = line.indexOf('='); + if (index === -1) + continue; - var index = values.indexOf('='); - if (index === -1) - return null; + var key = line.substring(0, index); + var val = line.substring(index + 1).replace(/\\n/g, '\n'); + var end = val.length - 1; - var counter = parseInt(values.substring(0, index), 10); - if (isNaN(counter)) - return null; + if ((val[0] === '"' && val[end] === '"') || (val[0] === '\'' && val[end] === '\'')) + val = val.substring(1, end); + else + val = val.trim(); - values = values.substring(index + 1); + obj[key] = val; + } - var count = values.length; - var random = values.charCodeAt(0); + return obj; +}; - var key_count = key.length; - var data_count = count - (random % key_count); - var decrypt_data = []; +SP.parseInt = function(def) { + var str = this.trim(); + var num = +str; + return isNaN(num) ? (def === undefined ? 0 : def) : num; +}; - for (var i = data_count - 1; i > 0; i--) { - index = values.charCodeAt(i) ^ (random ^ key.charCodeAt(i % key_count)); - decrypt_data[i] = String.fromCharCode(index); - } +SP.parseInt2 = function(def) { + var num = this.match(regexpINTEGER); + return num ? +num[0] : (def === undefined ? 0 : def); +}; - var val = decrypt_data.join(''); +SP.parseFloat2 = function(def) { + var num = this.match(regexpFLOAT); + return num ? +num[0].toString().replace(/,/g, '.') : (def === undefined ? 0 : def); +}; - if (counter !== val.length + key.length) - return null; +SP.parseBool = SP.parseBoolean = function() { + var self = this.toLowerCase(); + return self === 'true' || self === '1' || self === 'on'; +}; - return val; +SP.parseFloat = function(def) { + var str = this.trim(); + if (str.indexOf(',') !== -1) + str = str.replace(',', '.'); + var num = +str; + return isNaN(num) ? (def === undefined ? 0 : def) : num; }; -/* - Convert value from base64 and save to file - @filename {String} - @callback {Function} :: optional - return {String} -*/ -String.prototype.base64ToFile = function(filename, callback) { - var self = this; +SP.capitalize = function(first) { + + if (first) + return (this[0] || '').toUpperCase() + this.substring(1); - var index = self.indexOf(','); - if (index === -1) - index = 0; - else - index++; + var builder = ''; + var c; - if (callback) - fs.writeFile(filename, self.substring(index), 'base64', callback); - else - fs.writeFile(filename, self.substring(index), 'base64', exports.noop); + for (var i = 0, length = this.length; i < length; i++) { + var c = this[i - 1]; + if (!c || (c === ' ' || c === '\t' || c === '\n')) + c = this[i].toUpperCase(); + else + c = this[i]; + builder += c; + } - return this; + return builder; }; -/* - Get content type from base64 - return {String} -*/ -String.prototype.base64ContentType = function() { - var self = this; +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 + output += this[i]; + } + return output; +}; - var index = self.indexOf(';'); - if (index === -1) - return ''; +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; +}; - return self.substring(5, index); +SP.sha1 = function(salt) { + var hash = Crypto.createHash('sha1'); + hash.update(this + (salt || ''), ENCODING); + return hash.digest('hex'); }; -String.prototype.removeDiacritics = function() { - return exports.removeDiacritics(this); +SP.sha256 = function(salt) { + var hash = Crypto.createHash('sha256'); + hash.update(this + (salt || ''), ENCODING); + return hash.digest('hex'); }; -/* - Indent - @max {Number} - @c {String} : optional, default SPACE - return {String} -*/ -String.prototype.indent = function(max, c) { - var self = this; - return new Array(max + 1).join(c || ' ') + self; +SP.sha512 = function(salt) { + var hash = Crypto.createHash('sha512'); + hash.update(this + (salt || ''), ENCODING); + return hash.digest('hex'); }; -/* - isNumber? - @isDecimal {Boolean} :: optional, default false - return {Boolean} -*/ -String.prototype.isNumber = function(isDecimal) { +SP.md5 = function(salt) { + var hash = Crypto.createHash('md5'); + hash.update(this + (salt || ''), ENCODING); + return hash.digest('hex'); +}; - var self = this; - var length = self.length; +SP.toSearch = function() { + var str = this.replace(regexpSEARCH, '').trim().toLowerCase().removeDiacritics(); + var buf = []; + var prev = ''; + for (var i = 0, length = str.length; i < length; i++) { + var c = str[i]; + if (c === 'y') + c = 'i'; + if (c === prev) + continue; + prev = c; + buf.push(c); + } + + return buf.join(''); +}; - if (length === 0) - return false; +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); +}; + +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; + var random = isUnique ? exports.random(120) + 40 : 65; + var count = data_count + (random % key_count); + var values = []; + var index = 0; - isDecimal = isDecimal || false; + values[0] = String.fromCharCode(random); - for (var i = 0; i < length; i++) { - var ascii = self.charCodeAt(i); + var counter = this.length + key.length; - if (isDecimal) { - if (ascii === 44 || ascii === 46) { - isDecimal = false; - continue; - } - } + for (var i = count - 1; i > 0; i--) { + index = str.charCodeAt(i % data_count); + values[i] = String.fromCharCode(index ^ (key.charCodeAt(i % key_count) ^ random)); + } - if (ascii < 48 || ascii > 57) - return false; - } + str = Buffer.from(counter + '=' + values.join(''), ENCODING).toString('hex'); + var sum = 0; - return true; + for (var i = 0; i < str.length; i++) + sum += str.charCodeAt(i); + + return (sum + checksum((secret || CONF.secret) + key)) + '-' + str; }; -/* - @max {Number} - @c {String} :: optional - return {String} -*/ -String.prototype.padLeft = function(max, c) { - var self = this; - return new Array(Math.max(0, max - self.length + 1)).join(c || ' ') + self; +SP.decrypt = function(key, secret) { + + var index = this.indexOf('-'); + if (index === -1) + return null; + + var cs = +this.substring(0, index); + if (!cs || isNaN(cs)) + return null; + + 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); + + if (sum !== cs) + return null; + + var values = Buffer.from(hash, 'hex').toString(ENCODING); + var index = values.indexOf('='); + if (index === -1) + return null; + + var counter = +values.substring(0, index); + if (isNaN(counter)) + return null; + + values = values.substring(index + 1); + + var count = values.length; + var random = values.charCodeAt(0); + var key_count = key.length; + var data_count = count - (random % key_count); + var decrypt_data = []; + + for (var i = data_count - 1; i > 0; i--) { + index = values.charCodeAt(i) ^ (random ^ key.charCodeAt(i % key_count)); + decrypt_data[i] = String.fromCharCode(index); + } + + var val = decrypt_data.join(''); + return counter !== (val.length + key.length) ? null : val; }; -/* - @max {Number} - @c {String} :: optional - return {String} -*/ -String.prototype.padRight = function(max, c) { - var self = this; - return self + new Array(Math.max(0, max - self.length + 1)).join(c || ' '); +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; }; -/* - index {Number} - value {String} - return {String} -*/ -String.prototype.insert = function(index, value) { - var str = this; - var a = str.substring(0, index); - var b = value.toString() + str.substring(index); - return a + b; +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; }; -/* - Prepare string for replacing double dollar -*/ -String.prototype.dollar = function() { - var str = this; - var index = str.indexOf('$', 0); - - while (index !== -1) { - if (str[index + 1] === '$') - str = str.insert(index, '$'); - index = str.indexOf('$', index + 2); - } - return str.toString(); -}; - -/* - Create link - @max {Number} :: optional default 60 chars - return {String} -*/ -String.prototype.linker = function(max) { - max = max || 60; +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; +}; - var self = this.trim().toLowerCase().removeDiacritics(); - var builder = ''; - var length = self.length; +SP.base64ToBuffer = function() { + var self = this; - for (var i = 0; i < length; i++) { - var c = self[i]; - var code = self.charCodeAt(i); + var index = self.indexOf(','); + if (index === -1) + index = 0; + else + index++; - if (builder.length >= max) - break; + return Buffer.from(self.substring(index), 'base64'); +}; - if (code > 31 && code < 48) { +SP.base64ContentType = function() { + var self = this; + var index = self.indexOf(';'); + return index === -1 ? '' : self.substring(5, index); +}; - if (builder[builder.length - 1] === '-') - continue; +SP.removeDiacritics = function() { + return exports.removeDiacritics(this); +}; + +SP.indent = function(max, c) { + var plus = ''; + if (c === undefined) + c = ' '; + while (max--) + plus += c; + return plus + this; +}; - builder += '-'; - continue; - } +SP.isNumber = function(isDecimal) { - if (code > 47 && code < 58) { - builder += c; - continue; - } + var self = this; + var length = self.length; - if (code > 94 && code < 123) { - builder += c; - continue; - } + if (!length) + return false; - } + isDecimal = isDecimal || false; - return builder; + for (var i = 0; i < length; i++) { + var ascii = self.charCodeAt(i); + + if (isDecimal) { + if (ascii === 44 || ascii === 46) { + isDecimal = false; + continue; + } + } + + if (ascii < 48 || ascii > 57) + return false; + } + + return true; }; -String.prototype.link = function(max) { - console.log('String.prototype.link: OBSOLETE - Use String.prototype.Linker()'); - return this.linker(max); +if (!SP.padLeft) { + SP.padLeft = function(max, c) { + var self = this; + var len = max - self.length; + if (len < 0) + return self; + if (c === undefined) + c = ' '; + while (len--) + self = c + self; + return self; + }; +} + + +if (!SP.padRight) { + SP.padRight = function(max, c) { + var self = this; + var len = max - self.length; + if (len < 0) + return self; + if (c === undefined) + c = ' '; + while (len--) + self += c; + return self; + }; +} + +SP.insert = function(index, value) { + var str = this; + var a = str.substring(0, index); + var b = value.toString() + str.substring(index); + return a + b; }; -String.prototype.pluralize = function(zero, one, few, other) { - var str = this; - return str.parseInt().pluralize(zero, one, few, other); +/** + * Create a link from String + * @param {Number} max A maximum length, default: 60 and optional. + * @return {String} + */ +SP.slug = SP.toSlug = SP.toLinker = SP.linker = function(max) { + max = max || 60; + + var self = this.trim().toLowerCase().removeDiacritics(); + var builder = ''; + var length = self.length; + + for (var i = 0; i < length; i++) { + var c = self[i]; + var code = self.charCodeAt(i); + + if (code > 540){ + builder = ''; + break; + } + + if (builder.length >= max) + break; + + if (code > 31 && code < 48) { + if (builder[builder.length - 1] !== '-') + builder += '-'; + continue; + } + + if ((code > 47 && code < 58) || (code > 94 && code < 123)) + builder += c; + } + + 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.isBoolean = function() { - var self = this.toLowerCase(); - return (self === 'true' || self === 'false') ? true : false; +SP.pluralize = function(zero, one, few, other) { + return this.parseInt().pluralize(zero, one, few, other); }; -/* - @decimals {Number} - return {Number} -*/ -Number.prototype.floor = function(decimals) { - return Math.floor(this * Math.pow(10, decimals)) / Math.pow(10, decimals); +SP.isBoolean = function() { + var self = this.toLowerCase(); + return (self === 'true' || self === 'false') ? true : false; }; -/* - @max {Number} - @c {String} :: optional - return {String} -*/ -Number.prototype.padLeft = function(max, c) { - return this.toString().padLeft(max, c || '0'); +/** + * Check if the string contains only letters and numbers. + * @return {Boolean} + */ +SP.isAlphaNumeric = function() { + return regexpALPHA.test(this); }; -/* - @max {Number} - @c {String} :: optional - return {String} -*/ -Number.prototype.padRight = function(max, c) { - return this.toString().padRight(max, c || '0'); +SP.soundex = function() { + + var arr = this.toLowerCase().split(''); + var first = arr.shift(); + var builder = first.toUpperCase(); + + for (var i = 0, length = arr.length; i < length; i++) { + var v = SOUNDEX[arr[i]]; + if (v === undefined) + continue; + if (i) { + if (v !== arr[i - 1]) + builder += v; + } else if (v !== SOUNDEX[first]) + builder += v; + } + + return (builder + '000').substring(0, 4); }; -/* - Format number :: 10000 = 10 000 - @format {Number or String} :: number is decimal and string is specified format, example: ## ###.## - return {String} +/** +* Remove all Html Tags from a string +* @return {string} */ -Number.prototype.format = function(format) { +SP.removeTags = function() { + return this.replace(regexpTags, ''); +}; - var index = 0; - var num = this.toString(); - var beg = 0; - var end = 0; - var max = 0; - var output = ''; - var length = 0; +NP.floor = function(decimals) { + return Math.floor(this * Math.pow(10, decimals)) / Math.pow(10, decimals); +}; - if (typeof(format) === STRING) { +NP.fixed = function(decimals) { + return +this.toFixed(decimals); +}; - var d = false; - length = format.length; +NP.padLeft = function(max, c) { + return this.toString().padLeft(max, c || '0'); +}; - for (var i = 0; i < length; i++) { - var c = format[i]; - if (c === '#') { - if (d) - end++; - else - beg++; - } +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} + */ +NP.async = function(fn, callback) { + var number = this; + if (number) + fn(number--, () => setImmediate(() => number.async(fn, callback))); + else + callback && callback(); + return number; +}; - if (c === '.') - d = true; - } +/** + * Format number + * @param {Number} decimals Maximum decimal numbers + * @param {String} separator Number separator, default ' ' + * @param {String} separatorDecimal Decimal separator, default '.' if number separator is ',' or ' '. + * @return {String} + */ +NP.format = function(decimals, separator, separatorDecimal) { + + var self = this; + + if (typeof(decimals) === 'string') + return self.format2(decimals); + + var num = self.toString(); + var dec = ''; + var output = ''; + var minus = num[0] === '-' ? '-' : ''; + if (minus) + num = num.substring(1); + + var index = num.indexOf('.'); + + if (typeof(decimals) === 'string') { + var tmp = separator; + separator = decimals; + decimals = tmp; + } + + if (separator === undefined) + separator = ' '; + + if (index !== -1) { + dec = num.substring(index + 1); + num = num.substring(0, index); + } + + index = -1; + for (var i = num.length - 1; i >= 0; i--) { + index++; + if (index > 0 && index % 3 === 0) + output = separator + output; + output = num[i] + output; + } + + if (decimals || dec.length) { + if (dec.length > decimals) + dec = dec.substring(0, decimals || 0); + else + dec = dec.padRight(decimals || 0, '0'); + } + + if (dec.length && separatorDecimal === undefined) + separatorDecimal = separator === '.' ? ',' : '.'; + + return minus + output + (dec.length ? separatorDecimal + dec : ''); +}; - var strBeg = num; - var strEnd = ''; +NP.add = function(value, decimals) { + + if (value == null) + return this; + + if (typeof(value) === 'number') + return this + value; + + var first = value.charCodeAt(0); + var is = false; + + if (first < 48 || first > 57) { + is = true; + value = value.substring(1); + } + + var length = value.length; + var num; + + if (value[length - 1] === '%') { + value = value.substring(0, length - 1); + if (is) { + var val = value.parseFloat(); + switch (first) { + case 42: + num = this * ((this / 100) * val); + break; + case 43: + num = this + ((this / 100) * val); + break; + case 45: + num = this - ((this / 100) * val); + break; + case 47: + num = this / ((this / 100) * val); + break; + } + return decimals !== undefined ? num.floor(decimals) : num; + } else { + num = (this / 100) * value.parseFloat(); + return decimals !== undefined ? num.floor(decimals) : num; + } + + } else + num = value.parseFloat(); + + switch (first) { + case 42: + num = this * num; + break; + case 43: + num = this + num; + break; + case 45: + num = this - num; + break; + case 47: + num = this / num; + break; + default: + num = this; + break; + } + + if (decimals !== undefined) + return num.floor(decimals); + + return num; +}; - index = num.indexOf('.'); +NP.format2 = function(format) { + var index = 0; + var num = this.toString(); + var beg = 0; + var end = 0; + var max = 0; + var output = ''; + var length = 0; - if (index !== -1) { - strBeg = num.substring(0, index); - strEnd = num.substring(index + 1); - } + if (typeof(format) === 'string') { - if (strBeg.length > beg) { - max = strBeg.length - beg; - var tmp = ''; - for (var i = 0; i < max; i++) - tmp += '#'; + var d = false; + length = format.length; - format = tmp + format; - } + for (var i = 0; i < length; i++) { + var c = format[i]; + if (c === '#') { + if (d) + end++; + else + beg++; + } - if (strBeg.length < beg) - strBeg = strBeg.padLeft(beg, ' '); + if (c === '.') + d = true; + } - if (strEnd.length < end) - strEnd = strEnd.padRight(end, '0'); + var strBeg = num; + var strEnd = ''; - if (strEnd.length > end) - strEnd = strEnd.substring(0, end); + index = num.indexOf('.'); - d = false; - index = 0; + if (index !== -1) { + strBeg = num.substring(0, index); + strEnd = num.substring(index + 1); + } - var skip = true; - length = format.length; + if (strBeg.length > beg) { + max = strBeg.length - beg; + var tmp = ''; + for (var i = 0; i < max; i++) + tmp += '#'; - for (var i = 0; i < length; i++) { + format = tmp + format; + } - var c = format[i]; + if (strBeg.length < beg) + strBeg = strBeg.padLeft(beg, ' '); - if (c !== '#') { + if (strEnd.length < end) + strEnd = strEnd.padRight(end, '0'); - if (skip) - continue; + if (strEnd.length > end) + strEnd = strEnd.substring(0, end); - if (c === '.') { - d = true; - index = 0; - } + d = false; + index = 0; - output += c; - continue; - } + var skip = true; + length = format.length; - var value = d ? strEnd[index] : strBeg[index]; + for (var i = 0; i < length; i++) { - if (skip) - skip = [',', ' '].indexOf(value) !== -1; + var c = format[i]; - if (!skip) - output += value; + if (c !== '#') { - index++; - } + if (skip) + continue; - return output; - } + if (c === '.') { + d = true; + index = 0; + } - output = '### ### ###'; - beg = num.indexOf('.'); - max = format || 0; + output += c; + continue; + } - if (max === 0 && beg !== -1) - max = num.length - (beg + 1); + var value = d ? strEnd[index] : strBeg[index]; - if (max > 0) { - output += '.'; - for (var i = 0; i < max; i++) - output += '#'; - } + if (skip) + skip = [',', ' '].indexOf(value) !== -1; - return this.format(output); -}; + if (!skip) + output += value; -/* - Pluralize number - zero {String} - one {String} - few {String} - other {String} - return {String} -*/ -Number.prototype.pluralize = function(zero, one, few, other) { + index++; + } - var num = this; - var value = ''; + return output; + } - if (num === 0) - value = zero || ''; - else if (num === 1) - value = one || ''; - else if (num > 1 && num < 5) - value = few || ''; - else - value = other; + output = '### ### ###'; + beg = num.indexOf('.'); + max = format || 0; - var beg = value.indexOf('#'); - var end = value.lastIndexOf('#'); + if (max === 0 && beg !== -1) + max = num.length - (beg + 1); - if (beg === -1) - return value; + if (max > 0) { + output += '.'; + for (var i = 0; i < max; i++) + output += '#'; + } - var format = value.substring(beg, end + 1); - return num.format(format) + value.replace(format, ''); + return this.format(output); }; -/* - @length {Number} - return {String} -*/ -Number.prototype.hex = function(length) { - var str = this.toString(16).toUpperCase(); - while(str.length < length) - str = '0' + str; - return str; +NP.pluralize = function(zero, one, few, other) { + + var num = this; + var value = ''; + + if (num == 0) + value = zero || ''; + else if (num == 1) + value = one || ''; + else if (num > 1 && num < 5) + value = few || ''; + else + value = other; + + var beg = value.indexOf('#'); + if (beg === -1) + return value; + + var end = value.lastIndexOf('#'); + var format = value.substring(beg, end + 1); + return num.format(format) + value.replace(format, ''); }; -/* - Internal function -*/ -Number.prototype.condition = function(ifTrue, ifFalse) { - return (this % 2 === 0 ? ifTrue : ifFalse) || ''; +NP.hex = function(length) { + var str = this.toString(16).toUpperCase(); + while(str.length < length) + str = '0' + str; + return str; }; -/* - VAT - @percentage {Number} - @decimals {Number}, optional, default 2, - @includedVAT {Boolean}, optional, default true - return {Number} -*/ -Number.prototype.VAT = function(percentage, decimals, includedVAT) { - var num = this; - var type = typeof(decimals); +NP.VAT = function(percentage, decimals, includedVAT) { + var num = this; + var type = typeof(decimals); - if (type === BOOLEAN) { - var tmp = includedVAT; - includedVAT = decimals; - decimals = tmp; - type = typeof(decimals); - } + if (type === 'boolean') { + var tmp = includedVAT; + includedVAT = decimals; + decimals = tmp; + type = typeof(decimals); + } - if (type === UNDEFINED) - decimals = 2; + if (type === 'undefined') + decimals = 2; - if (typeof(includedVAT) === UNDEFINED) - includedVAT = true; + if (includedVAT === undefined) + includedVAT = true; - if (percentage === 0 || num === 0) - return num; + if (!percentage || !num) + return num; + return includedVAT ? (num / ((percentage / 100) + 1)).round(decimals) : (num * ((percentage / 100) + 1)).round(decimals); +}; - return includedVAT ? (num / ((percentage / 100) + 1)).floor(decimals) : (num * ((percentage / 100) + 1)).floor(decimals); +NP.discount = function(percentage, decimals) { + var num = this; + if (decimals === undefined) + decimals = 2; + return (num - (num / 100) * percentage).floor(decimals); }; -/* - Discount - @percentage {Number} - @decimals {Number}, optional, default 2 - return {Number} -*/ -Number.prototype.discount = function(percentage, decimals) { - var num = this; - var type = typeof(decimals); +NP.parseDate = function(plus) { + return new Date(this + (plus || 0)); +}; - if (type === UNDEFINED) - decimals = 2; +if (!NP.toRad) { + NP.toRad = function () { + return this * Math.PI / 180; + }; +} - return (num - (num / 100) * percentage).floor(decimals); -}; -Number.prototype.parseDate = function(plus) { - return new Date(this + (plus || 0)); +NP.filesize = function(decimals, type) { + + if (typeof(decimals) === 'string') { + var tmp = type; + type = decimals; + decimals = tmp; + } + + var value; + + // this === bytes + switch (type) { + case 'bytes': + value = this; + break; + case 'KB': + value = this / 1024; + break; + case 'MB': + value = filesizehelper(this, 2); + break; + case 'GB': + value = filesizehelper(this, 3); + break; + case 'TB': + value = filesizehelper(this, 4); + break; + default: + + type = 'bytes'; + value = this; + + if (value > 1023) { + value = value / 1024; + type = 'KB'; + } + + if (value > 1023) { + value = value / 1024; + type = 'MB'; + } + + if (value > 1023) { + value = value / 1024; + type = 'GB'; + } + + if (value > 1023) { + value = value / 1024; + type = 'TB'; + } + + break; + } + + type = ' ' + type; + return (decimals === undefined ? value.format(2).replace('.00', '') : value.format(decimals)) + type; }; -if (typeof (Number.prototype.toRad) === UNDEFINED) { - Number.prototype.toRad = function () { - return this * Math.PI / 180; - }; +function filesizehelper(number, count) { + while (count--) { + number = number / 1024; + if (number.toFixed(3) === '0.000') + return 0; + } + return number; } -Boolean.prototype.condition = function(ifTrue, ifFalse) { - return (this ? ifTrue : ifFalse) || ''; +var AP = Array.prototype; + +/** + * Take items from array + * @param {Number} count + * @return {Array} + */ +AP.take = function(count) { + var arr = []; + var self = this; + var length = self.length; + for (var i = 0; i < length; i++) { + arr.push(self[i]); + if (arr.length >= count) + return arr; + } + return arr; }; -/* - @count {Number} - return {Array} -*/ -Array.prototype.take = function(count) { - var arr = []; - var self = this; - var length = self.length; - for (var i = 0; i < length; i++) { - arr.push(self[i]); - if (arr.length >= count) - return arr; - } - return arr; -}; - -/* - Trim values -*/ -Array.prototype.trim = function() { - var self = this; - var length = self.length; - for (var i = 0; i < length; i++) { - if (typeof(self[i]) === STRING) - self[i] = self[i].trim(); - } - return self; -}; - -/* - @count {Number} - return {Array} -*/ -Array.prototype.skip = function(count) { - var arr = []; - var self = this; - var length = self.length; - for (var i = 0; i < length; i++) { - if (i >= count) - arr.push(self[i]); - } - return arr; -}; - -/* - @cb {Function} :: return true / false - return {Array} -*/ -Array.prototype.where = function(cb) { +/** + * Extend objects in Array + * @param {Object} obj + * @param {Boolean} rewrite Default: false. + * @return {Array} Returns self + */ +AP.extend = function(obj, rewrite) { + var isFn = typeof(obj) === 'function'; + for (var i = 0, length = this.length; i < length; i++) { + if (isFn) + this[i] = obj(this[i], i); + else + this[i] = exports.extend(this[i], obj, rewrite); + } + return this; +}; + +/** + * First item in array + * @param {Object} def Default value. + * @return {Object} + */ +AP.first = function(def) { + var item = this[0]; + return item === undefined ? def : item; +}; + +/** + * Create object from Array + * @param {String} name Optional, property name. + * @return {Object} + */ +AP.toObject = function(name) { - var self = this; - var selected = []; - var length = self.length; + var self = this; + var obj = {}; - for (var i = 0; i < length; i++) { - if (cb.call(self, self[i], i)) - selected.push(self[i]); - } + for (var i = 0, length = self.length; i < length; i++) { + var item = self[i]; + if (name) + obj[item[name]] = item; + else + obj[item] = true; + } - return selected; + return obj; }; -/* - @cb {Function} :: return true if is finded - return {Array item} -*/ -Array.prototype.find = function(cb) { - var self = this; - var length = self.length; - for (var i = 0; i < length; i++) { - if (cb.call(self, self[i], i)) - return self[i]; - } - return null; -}; - -/* - @cb {Function} :: return true if is removed - return {Array} -*/ -Array.prototype.remove = function(cb) { - var self = this; - var arr = []; - var length = self.length; - for (var i = 0; i < length; i++) { - if (!cb.call(self, self[i], i)) - arr.push(self[i]); - } - return arr; -}; - -/* - Random return item from array - Return {Object} -*/ -Array.prototype.random = function() { - var self = this; - return self[exports.random(self.length - 1)]; +/** + * Compare two arrays + * @param {String} id An identificator. + * @param {Array} b Second array. + * @param {Function(itemA, itemB, indexA, indexB)} executor + */ +AP.compare = function(id, b, executor) { + + var a = this; + var ak = {}; + var bk = {}; + var al = a.length; + var bl = b.length; + var tl = Math.max(al, bl); + var processed = {}; + + for (var i = 0; i < tl; i++) { + var av = a[i]; + if (av) + ak[av[id]] = i; + var bv = b[i]; + if (bv) + bk[bv[id]] = i; + } + + var index = -1; + + for (var i = 0; i < tl; i++) { + + var av = a[i]; + var bv = b[i]; + var akk; + var bkk; + + if (av) { + akk = av[id]; + if (processed[akk]) + continue; + processed[akk] = true; + index = bk[akk]; + if (index === undefined) + executor(av, undefined, i, -1); + else + executor(av, b[index], i, index); + } + + if (bv) { + bkk = bv[id]; + if (processed[bkk]) + continue; + processed[bkk] = true; + index = ak[bkk]; + if (index === undefined) + executor(undefined, bv, -1, i); + else + executor(a[index], bv, index, i); + } + } + + OBSOLETE('Array.compare()', 'Use U.diff() insteadof Array.compare()'); }; -/* - Waiting list - function remove each item - @callback {Function} :: function(next) {} - @complete {Function} :: optional -*/ -Array.prototype.waiting = function(onItem, callback) { - console.log('Array.prototype.waiting: OBSOLETE. Use Array.prototype.wait'); - return this.wait(onItem, callback); +/** + * Pair arrays + * @param {Array} arr + * @param {String} property + * @param {Function(itemA, itemB)} fn Paired items (itemA == this, itemB == arr) + * @param {Boolean} remove Optional, remove item from this array if the item doesn't exist int arr (default: false). + * @return {Array} + */ +AP.pair = function(property, arr, fn, remove) { + + if (property instanceof Array) { + var tmp = property; + property = arr; + arr = tmp; + } + + if (!arr) + arr = new Array(0); + + var length = arr.length; + var index = 0; + + while (true) { + var item = this[index++]; + if (!item) + break; + + var is = false; + + for (var i = 0; i < length; i++) { + if (item[property] !== arr[i][property]) + continue; + fn(item, arr[i]); + is = true; + break; + } + + if (is || !remove) + continue; + + index--; + this.splice(index, 1); + } + + OBSOLETE('Array.pair()', 'The method will be removed in Total.js v4'); + return this; }; -/* - Waiting list - function remove each item - @callback {Function} :: function(next) {} - @complete {Function} :: optional -*/ -Array.prototype.wait = function(onItem, callback) { +/** + * Last item in array + * @param {Object} def Default value. + * @return {Object} + */ +AP.last = function(def) { + var item = this[this.length - 1]; + return item === undefined ? def : item; +}; - var self = this; - var item = self.shift(); +AP.quicksort = AP.orderBy = function(name, asc) { + + var length = this.length; + if (!length || length === 1) + return this; + + if (typeof(name) === 'boolean') { + asc = name; + name = 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; + var field = name ? self[0][name] : self[0]; + + switch (typeof(field)) { + case 'string': + if (field.isJSONDate()) + type = 4; + else + type = 1; + break; + case 'number': + type = 2; + break; + case 'boolean': + type = 3; + break; + default: + if (!exports.isDate(field)) + return self; + type = 4; + break; + } + + shellsort(self, function(a, b) { + + var va = name ? a[name] : a; + var vb = name ? b[name] : b; + + // String + if (type === 1) { + return va && vb ? (asc ? COMPARER(va, vb) : COMPARER(vb, va)) : 0; + } else if (type === 2) { + return va > vb ? (asc ? 1 : -1) : va < vb ? (asc ? -1 : 1) : 0; + } else if (type === 3) { + return va === true && vb === false ? (asc ? 1 : -1) : va === false && vb === true ? (asc ? -1 : 1) : 0; + } else if (type === 4) { + if (!va || !vb) + return 0; + if (!va.getTime) + va = new Date(va); + if (!vb.getTime) + vb = new Date(vb); + var at = va.getTime(); + var bt = vb.getTime(); + return at > bt ? (asc ? 1 : -1) : at < bt ? (asc ? -1 : 1) : 0; + } + return 0; + }); + + return self; +}; - if (typeof(item) === UNDEFINED) { - if (callback) - callback(); - return self; - } +AP.trim = function() { + var self = this; + var output = []; + for (var i = 0, length = self.length; i < length; i++) { + if (typeof(self[i]) === 'string') + self[i] = self[i].trim(); + self[i] && output.push(self[i]); + } + return output; +}; - onItem.call(self, item, function() { - setImmediate(function() { - self.wait(onItem, callback); - }); - }); +/** + * Skip items from array + * @param {Number} count + * @return {Array} + */ +AP.skip = function(count) { + var arr = []; + var self = this; + var length = self.length; + for (var i = 0; i < length; i++) + i >= count && arr.push(self[i]); + return arr; +}; + +/** + * Find items in Array + * @param {Function(item, index) or String/Object} cb + * @param {Object} value Optional. + * @return {Array} + */ +AP.where = AP.findAll = function(cb, value) { + + var self = this; + var selected = []; + var isFN = typeof(cb) === 'function'; + var isV = value !== undefined; + + for (var i = 0, length = self.length; i < length; i++) { - return self; + if (isFN) { + cb.call(self, self[i], i) && selected.push(self[i]); + continue; + } + + if (isV) { + self[i] && self[i][cb] === value && selected.push(self[i]); + continue; + } + + self[i] === cb && selected.push(self[i]); + } + + return selected; }; -Array.prototype.async = function(callback) { +/** + * Find item in Array + * @param {Function(item, index) or String/Object} cb + * @param {Object} value Optional. + * @return {Array} + */ +AP.findItem = function(cb, value) { + var self = this; + var index = self.findIndex(cb, value); + if (index === -1) + return null; + return self[index]; +}; - var self = this; - var item = self.shift(); +var arrfindobsolete; - if (typeof(item) === UNDEFINED) { - if (callback) - callback(); - return self; - } +AP.find = function(cb, value) { - item(function() { - setImmediate(function() { - self.async(callback); - }); - }); + if (!arrfindobsolete) { + arrfindobsolete = true; + OBSOLETE('Array.prototype.find()', 'will be removed in v4, use alternative "Array.prototype.findItem()"'); + } - return self; + var self = this; + var index = self.findIndex(cb, value); + if (index === -1) + return null; + return self[index]; }; -/* - Randomize array - Return {Array} -*/ -Array.prototype.randomize = function() { +AP.findIndex = function(cb, value) { + + var self = this; + var isFN = typeof(cb) === 'function'; + var isV = value !== undefined; - var self = this; - var random = (Math.floor(Math.random() * 100000000) * 10).toString(); - var index = 0; - var old = 0; + for (var i = 0, length = self.length; i < length; i++) { - self.sort(function(a, b) { + if (isFN) { + if (cb.call(self, self[i], i)) + return i; + continue; + } - var c = random[index++]; + if (isV) { + if (self[i] && self[i][cb] === value) + return i; + continue; + } - if (typeof(c) === UNDEFINED) { - c = random[0]; - index = 0; - } + if (self[i] === cb) + return i; + } + + return -1; +}; + +/** + * Remove items from Array + * @param {Function(item, index) or Object} cb + * @param {Object} value Optional. + * @return {Array} + */ +AP.remove = function(cb, value) { - if (old > c) { - old = c; - return -1; - } + var self = this; + var arr = []; + var isFN = typeof(cb) === 'function'; + var isV = value !== undefined; - if (old === c) { - old = c; - return 0; - } + for (var i = 0, length = self.length; i < length; i++) { - old = c; - return 1; - }); + if (isFN) { + !cb.call(self, self[i], i) && arr.push(self[i]); + continue; + } - return self; + if (isV) { + self[i] && self[i][cb] !== value && arr.push(self[i]); + continue; + } + + self[i] !== cb && arr.push(self[i]); + } + return arr; }; -/* - Async class -*/ -function AsyncTask(owner, name, fn, cb, waiting) { +AP.wait = AP.waitFor = function(onItem, callback, thread, tmp) { + + var self = this; + var init = false; + + // INIT + if (!tmp) { + + if (typeof(callback) !== 'function') { + thread = callback; + callback = null; + } - this.handlers = { - oncomplete: this.complete.bind(this) - }; - - this.isRunning = 0; - this.owner = owner; - this.name = name; - this.fn = fn; - this.cb = cb; - this.waiting = waiting; - this.interval = null; - this.isCanceled = false; + tmp = {}; + tmp.pending = 0; + tmp.index = 0; + tmp.thread = thread; + + // thread === Boolean then array has to be removed item by item + + init = true; + } + + var item = thread === true ? self.shift() : self[tmp.index++]; + if (item === undefined) { + if (!tmp.pending) { + callback && callback(); + tmp.cancel = true; + } + return self; + } + + tmp.pending++; + onItem.call(self, item, () => setImmediate(next_wait, self, onItem, callback, thread, tmp), tmp.index); + + if (!init || tmp.thread === 1) + return self; + + for (var i = 1; i < tmp.thread; i++) + self.wait(onItem, callback, 1, tmp); + + return self; +}; + +function next_wait(self, onItem, callback, thread, tmp) { + tmp.pending--; + self.wait(onItem, callback, thread, tmp); } -AsyncTask.prototype.run = function() { - var self = this; - try - { - self.isRunning = 1; - self.owner.tasksWaiting[self.name] = true; - self.owner.emit('begin', self.name); +/** + * Creates a function async list + * @param {Function} callback Optional + * @return {Array} + */ +AP.async = function(thread, callback, pending) { + + var self = this; + + if (typeof(thread) === 'function') { + callback = thread; + thread = 1; + } else if (thread === undefined) + thread = 1; + + if (pending === undefined) + pending = 0; + + var item = self.shift(); + if (item === undefined) { + if (!pending) { + pending = undefined; + callback && callback(); + } + return self; + } + + for (var i = 0; i < thread; i++) { + + if (i) + item = self.shift(); + + pending++; + item(function() { + setImmediate(function() { + pending--; + self.async(1, callback, pending); + }); + }); + } + + return self; +}; - var timeout = self.owner.tasksTimeout[self.name]; - if (timeout > 0) - self.interval = setTimeout(self.timeout.bind(self), timeout); +AP.randomize = function() { + OBSOLETE('Array.randomize()', 'Use Array.random().'); + return this.random(); +}; - self.fn(self.handlers.oncomplete); - } catch (ex) { - self.owner.emit('error', self.name, ex); - self.complete(); - } - return self; +// Fisher-Yates shuffle +AP.random = function() { + for (var i = this.length - 1; i > 0; i--) { + var j = Math.floor(Math.random() * (i + 1)); + var temp = this[i]; + this[i] = this[j]; + this[j] = temp; + } + return this; +}; + +AP.limit = function(max, fn, callback, index) { + + if (index === undefined) + index = 0; + + var current = []; + var self = this; + var length = index + max; + + for (var i = index; i < length; i++) { + var item = self[i]; + + if (item !== undefined) { + current.push(item); + continue; + } + + if (!current.length) { + callback && callback(); + return self; + } + + fn(current, () => callback && callback(), index, index + max); + return self; + } + + if (!current.length) { + callback && callback(); + return self; + } + + fn(current, function() { + if (length < self.length) + self.limit(max, fn, callback, length); + else + callback && callback(); + }, index, index + max); + + return self; +}; + +/** + * Get unique elements from Array + * @return {[type]} [description] + */ +AP.unique = function(property) { + + var self = this; + var result = []; + var sublength = 0; + + for (var i = 0, length = self.length; i < length; i++) { + var value = self[i]; + + if (!property) { + result.indexOf(value) === -1 && result.push(value); + continue; + } + + if (sublength === 0) { + result.push(value); + sublength++; + continue; + } + + var is = true; + for (var j = 0; j < sublength; j++) { + if (result[j][property] === value[property]) { + is = false; + break; + } + } + + if (is) { + result.push(value); + sublength++; + } + } + + return result; +}; + +ArrayBuffer.prototype.toBuffer = function() { + var buf = new Buffer(this.byteLength); + var view = new Uint8Array(this); + for (var i = 0, length = buf.length; i < length; ++i) + buf[i] = view[i]; + return buf; +}; + +function AsyncTask(owner, name, fn, cb, waiting) { + this.isRunning = 0; + this.owner = owner; + this.name = name; + this.fn = fn; + this.cb = cb; + this.waiting = waiting; + this.interval = null; + this.isCanceled = false; +} + +AsyncTask.prototype.run = function() { + var self = this; + try + { + + if (self.isCanceled) { + self.complete(); + return self; + } + + self.isRunning = 1; + self.owner.tasksWaiting[self.name] = true; + self.owner.emit('begin', self.name); + + var timeout = self.owner.tasksTimeout[self.name]; + if (timeout > 0) + self.interval = setTimeout(function() { self.timeout(); }, timeout); + + self.fn(function() { + setImmediate(() => self.complete()); + }); + + } catch (ex) { + self.owner.emit('error', self.name, ex); + self.complete(); + } + return self; }; AsyncTask.prototype.timeout = function(timeout) { - var self = this; - if (timeout > 0) { - clearTimeout(self.interval); - setTimeout(self.timeout.bind(self), timeout); - return self; - } + var self = this; - if (timeout <= 0) { - clearTimeout(self.interval); - setTimeout(self.timeout.bind(self), timeout); - return self; - } + if (timeout > 0) { + clearTimeout(self.interval); + setTimeout(function() { self.timeout(); }, timeout); + return self; + } - self.cancel(true); - return self; + if (timeout <= 0) { + clearTimeout(self.interval); + setTimeout(function() { self.timeout(); }, timeout); + return self; + } + + setImmediate(() => self.cancel(true)); + return self; }; AsyncTask.prototype.cancel = function(isTimeout) { - var self = this; + var self = this; - self.isCanceled = true; + self.isCanceled = true; - if (isTimeout) - self.owner.emit('timeout', self.name); - else - self.owner.emit('cancel', self.name); + if (isTimeout) + self.owner.emit('timeout', self.name); + else + self.owner.emit('cancel', self.name); - self.fn = null; - self.cb = null; + self.fn = null; + self.cb = null; + self.complete(); + return self; }; AsyncTask.prototype.complete = function() { - var item = this; - var self = item.owner; - - item.isRunning = 2; - - delete self.tasksPending[item.name]; - delete self.tasksWaiting[item.name]; + var item = this; + var self = item.owner; - if (!item.isCanceled) { - try - { - self.emit('end', item.name); + item.isRunning = 2; - if (item.cb) - item.cb(); + delete self.tasksPending[item.name]; + delete self.tasksWaiting[item.name]; - } catch (ex) { - self.emit('error', ex, item.name); - } - } + if (!item.isCanceled) { + try + { + self.emit('end', item.name); + item.cb && item.cb(); + } catch (ex) { + self.emit('error', ex, item.name); + } + } - self.reload(); - self.refresh(); + setImmediate(function() { + self.reload(); + self.refresh(); + }); - return self; + return self; }; function Async(owner) { - this._max = 0; - this._count = 0; - this._isRunning = false; + this._max = 0; + this._count = 0; + this._isRunning = false; + this._isEnd = false; - this.owner = owner; - this.onComplete = []; + this.owner = owner; + this.onComplete = []; - this.tasksPending = {}; - this.tasksWaiting = {}; - this.tasksAll = []; - this.tasksTimeout = {}; + this.tasksPending = {}; + this.tasksWaiting = {}; + this.tasksAll = []; + this.tasksTimeout = {}; + this.isCanceled = false; - events.EventEmitter.call(this); + Events.EventEmitter.call(this); } Async.prototype = { - get count() { - return this._count; - }, - - get percentage() { - var self = this; - return 100 - Math.floor((self._count * 100) / self._max); - } -} + get count() { + return this._count; + }, + + get percentage() { + var p = 100 - Math.floor((this._count * 100) / this._max); + return p ? p : 0; + } +}; -Async.prototype.__proto__ = Object.create(events.EventEmitter.prototype, { - constructor: { - value: Async, - enumberable: false - } +const ACP = Async.prototype; + +ACP.__proto__ = Object.create(Events.EventEmitter.prototype, { + constructor: { + value: Async, + enumberable: false + } }); -Async.prototype.reload = function() { - var self = this; - self.tasksAll = Object.keys(self.tasksPending); - self.emit('percentage', self.percentage); - return self; +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; - var self = this; + if (name === undefined) { + self.isCanceled = true; + for (var i = 0; i < self._count; i++) + self.cancel(self.tasksAll[i]); + return true; + } - if (typeof(name) === UNDEFINED) { + var task = self.tasksPending[name]; + if (!task) + return false; - for (var i = 0; i < self._count; i++) - self.cancel(tasksAll[i]); + delete self.tasksPending[name]; + delete self.tasksWaiting[name]; + + task.cancel(); + task = null; + self.reload(); + self.refresh(); + + return true; +}; - return true; - } +ACP.await = function(name, fn, cb) { - var task = self.tasksPending[name]; + var self = this; - if (!task) - return false; + if (self.isCanceled) + return false; - delete self.tasksPending[name]; - delete self.tasksWaiting[name]; + if (typeof(name) === 'function') { + cb = fn; + fn = name; + name = exports.GUID(6); + } - task.cancel(); - self.reload(); - self.refresh(); + if (self.tasksPending[name]) + return false; - return true; + self.tasksPending[name] = new AsyncTask(self, name, fn, cb, null); + self._max++; + self.reload(); + self.refresh(); + return true; }; -Async.prototype.await = function(name, fn, cb) { +ACP.wait = function(name, waitingFor, fn, cb) { - var self = this; + var self = this; - if (typeof(name) === FUNCTION) { - cb = fn; - fn = name; - name = exports.GUID(6); - } + if (self.isCanceled) + return false; - if (typeof(self.tasksPending[name]) !== UNDEFINED) - return false; + if (typeof(waitingFor) === 'function') { + cb = fn; + fn = waitingFor; + waitingFor = null; + } - self.tasksPending[name] = new AsyncTask(self, name, fn, cb, null); - self._max++; - self.reload(); - self.refresh(); + if (self.tasksPending[name]) + return false; - return true; + self.tasksPending[name] = new AsyncTask(self, name, fn, cb, waitingFor); + self._max++; + self.reload(); + self.refresh(); + return true; }; -Async.prototype.wait = function(name, waitingFor, fn, cb) { +ACP.complete = function(fn) { + return this.run(fn); +}; - var self = this; +ACP.run = function(fn) { + this._isRunning = true; + fn && this.onComplete.push(fn); + this.refresh(); + return this; +}; - if (typeof(waitingFor) === FUNCTION) { - cb = fn; - fn = waitingFor; - waitingFor = name; - name = exports.GUID(6); - } +ACP.isRunning = function(name) { + if (!name) + return this._isRunning; + var task = this.tasksPending[name]; + return task ? task.isRunning === 1 : false; +}; - if (typeof(self.tasksPending[name]) !== UNDEFINED) - return false; +ACP.isWaiting = function(name) { + var task = this.tasksPending[name]; + return task ? task.isRunning === 0 : false; +}; - self.tasksPending[name] = new AsyncTask(self, name, fn, cb, waitingFor); - self._max++; - self.reload(); - self.refresh(); +ACP.isPending = function(name) { + return this.tasksPending[name] ? true : false; +}; - return true; +ACP.timeout = function(name, timeout) { + if (timeout) + this.tasksTimeout[name] = timeout; + else + this.tasksTimeout[name] = undefined; + return this; +}; +ACP.refresh = function(name) { + + var self = this; + + if (!self._isRunning || self._isEnd) + return self; + + self._count = self.tasksAll.length; + var index = 0; + + while (true) { + var name = self.tasksAll[index++]; + if (!name) + break; + + var task = self.tasksPending[name]; + if (!task) + break; + + if (self.isCanceled || task.isCanceled) { + delete self.tasksPending[name]; + delete self.tasksWaiting[name]; + self.tasksAll.splice(index, 1); + self._count = self.tasksAll.length; + index--; + continue; + } + + if (task.isRunning !== 0 || (task.waiting && self.tasksPending[task.waiting])) + continue; + + task.run(); + } + + if (self._count === 0) { + self._isRunning = false; + self._isEnd = true; + self.emit('complete'); + self.emit('percentage', 100); + self._max = 0; + var complete = self.onComplete; + var length = complete.length; + self.onComplete = []; + for (var i = 0; i < length; i++) { + try + { + complete[i](); + } catch (ex) { + self.emit('error', ex); + } + } + setImmediate(() => self._isEnd = false); + } + + return self; }; -Async.prototype.complete = function(fn) { - return this.run(fn); +function FileList() { + this.pending = []; + this.pendingDirectory = []; + this.directory = []; + this.file = []; + this.onComplete = null; + this.onFilter = null; + this.advanced = false; +} + +const FLP = FileList.prototype; + +FLP.reset = function() { + this.file.length = 0; + this.directory.length = 0; + this.pendingDirectory.length = 0; + return this; }; -Async.prototype.run = function(fn) { - var self = this; - self._isRunning = true; +FLP.walk = function(directory) { + + var self = this; + + if (directory instanceof Array) { + var length = directory.length; + for (var i = 0; i < length; i++) + self.pendingDirectory.push(directory[i]); + self.next(); + return; + } + + Fs.readdir(directory, function(err, arr) { + if (err) + return self.next(); + var length = arr.length; + for (var i = 0; i < length; i++) + self.pending.push(Path.join(directory, arr[i])); + self.next(); + }); +}; + +FLP.stat = function(path) { + var self = this; + + Fs.stat(path, function(err, stats) { - if (fn) - self.onComplete.push(fn); + if (err) + return self.next(); - self.refresh(); - return self; + if (stats.isDirectory()) { + path = self.clean(path); + if (!self.onFilter || self.onFilter(path, true)) { + self.directory.push(path); + self.pendingDirectory.push(path); + } + } else if (!self.onFilter || self.onFilter(path, false)) + self.file.push(self.advanced ? { filename: path, stats: stats } : path); + + self.next(); + }); }; -Async.prototype.isRunning = function(name) { +FLP.clean = function(path) { + return path[path.length - 1] === Path.sep ? path : path + Path.sep; +}; - var self = this; +FLP.next = function() { + var self = this; - if (!name) - return self._isRunning; + if (self.pending.length) { + var item = self.pending.shift(); + self.stat(item); + return; + } - var task = self.tasksPending[name]; - if (!task) - return false; + if (self.pendingDirectory.length) { + var directory = self.pendingDirectory.shift(); + self.walk(directory); + return; + } - return task.isRunning === 1; + self.onComplete(self.file, self.directory); }; -Async.prototype.isWaiting = function(name) { - var self = this; +exports.Async = Async; - var task = self.tasksPending[name]; - if (!task) - return false; +exports.sync = function(fn, owner) { + return function() { + + var args = [].slice.call(arguments); + var params; + var callback; + var executed = false; + var self = owner || this; + + args.push(function() { + params = arguments; + if (!executed && callback) { + executed = true; + callback.apply(self, params); + } + }); + + fn.apply(self, args); + + return function(cb) { + callback = cb; + if (!executed && params) { + executed = true; + callback.apply(self, params); + } + }; + }; +}; - return task.isRunning === 0; +exports.sync2 = function(fn, owner) { + return (function() { + + var params; + var callback; + var executed = false; + var self = owner || this; + var args = [].slice.call(arguments); + + args.push(function() { + params = arguments; + if (!executed && callback) { + executed = true; + callback.apply(self, params); + } + }); + + fn.apply(self, args); + + return function(cb) { + callback = cb; + if (!executed && params) { + executed = true; + callback.apply(self, params); + } + }; + })(); }; -Async.prototype.isPending = function(name) { - var self = this; - var task = self.tasksPending[name]; - if (!task) - return false; - return true; +exports.async = function(fn, isApply) { + var context = this; + return function(complete) { + + var self = this; + var argv; + + if (arguments.length) { + + if (isApply) { + // index.js/Subscribe.prototype.doExecute + argv = arguments[1]; + } else { + argv = []; + for (var i = 1; i < arguments.length; i++) + argv.push(arguments[i]); + } + } else + argv = new Array(0); + + var generator = fn.apply(context, argv); + next(null); + + function next(err, result) { + + var g, type; + + try + { + var can = err ? false : true; + switch (can) { + case true: + g = generator.next(result); + break; + case false: + g = generator.throw(err); + break; + } + + } catch (e) { + + if (!complete) + return; + + type = typeof(complete); + + if (type === 'object' && complete.isController) { + if (e instanceof ErrorBuilder) + complete.content(e); + else + complete.view500(e); + return; + } + + type === 'function' && setImmediate(() => complete(e)); + return; + } + + if (g.done) { + typeof(complete) === 'function' && complete(null, g.value); + return; + } + + var promise = g.value instanceof Promise; + + if (typeof(g.value) !== 'function' && !promise) { + next.call(self, null, g.value); + return; + } + + try + { + if (promise) { + g.value.then((value) => next.call(self, null, value)); + return; + } + + g.value.call(self, function() { + next.apply(self, arguments); + }); + + } catch (e) { + setImmediate(() => next.call(self, e)); + } + } + + return generator.value; + }; }; -Async.prototype.timeout = function(name, timeout) { +// MIT +// Written by Jozef Gula +// Optimized by Peter Sirka +const CACHE_GML1 = [null, null, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; +const CACHE_GML2 = [null, null, null, null, null, null, null, null]; +exports.getMessageLength = function(data, isLE) { + + var length = data[1] & 0x7f; + + if (length === 126) { + if (data.length < 4) + return -1; + CACHE_GML1[0] = data[3]; + CACHE_GML1[1] = data[2]; + return converBytesToInt64(CACHE_GML1, 0, isLE); + } + + if (length === 127) { + if (data.Length < 10) + return -1; + CACHE_GML2[0] = data[9]; + CACHE_GML2[1] = data[8]; + CACHE_GML2[2] = data[7]; + CACHE_GML2[3] = data[6]; + CACHE_GML2[4] = data[5]; + CACHE_GML2[5] = data[4]; + CACHE_GML2[6] = data[3]; + CACHE_GML2[7] = data[2]; + return converBytesToInt64(CACHE_GML2, 0, isLE); + } + + return length; +}; - var self = this; +// MIT +// Written by Jozef Gula +function converBytesToInt64(data, startIndex, isLE) { + return isLE ? (data[startIndex] | (data[startIndex + 1] << 0x08) | (data[startIndex + 2] << 0x10) | (data[startIndex + 3] << 0x18) | (data[startIndex + 4] << 0x20) | (data[startIndex + 5] << 0x28) | (data[startIndex + 6] << 0x30) | (data[startIndex + 7] << 0x38)) : ((data[startIndex + 7] << 0x20) | (data[startIndex + 6] << 0x28) | (data[startIndex + 5] << 0x30) | (data[startIndex + 4] << 0x38) | (data[startIndex + 3]) | (data[startIndex + 2] << 0x08) | (data[startIndex + 1] << 0x10) | (data[startIndex] << 0x18)); +} - if (timeout <= 0 || typeof(timeout) === UNDEFINED) { - delete self.tasksTimeout[name]; - return self; - } +exports.queuecache = {}; + +function queue_next(name) { + + var item = exports.queuecache[name]; + if (!item) + return; + + item.running--; + + if (item.running < 0) + item.running = 0; + + if (item.pending.length) { + var fn = item.pending.shift(); + if (fn) { + item.running++; + setImmediate(queue_next_callback, fn, name); + } else + item.running = 0; + } +} - self.tasksTimeout[name] = timeout; - return self; +function queue_next_callback(fn, name) { + fn(() => queue_next(name)); +} + +/** + * Queue list + * @param {String} name + * @param {Number} max Maximum stack. + * @param {Function(next)} fn + */ +exports.queue = function(name, max, fn) { + + if (!fn) + return false; + + if (!max) { + fn(NOOP); + return true; + } + + if (!exports.queuecache[name]) + exports.queuecache[name] = { limit: max, running: 0, pending: [] }; + + var item = exports.queuecache[name]; + if (item.running >= item.limit) { + item.pending.push(fn); + return false; + } + + item.running++; + setImmediate(queue_next_callback, fn, name); + return true; +}; + +exports.minifyStyle = function(val) { + return Internal.compile_css(val); +}; + +exports.minifyScript = function(val) { + return Internal.compile_javascript(val); +}; + +exports.minifyHTML = function(val) { + return Internal.compile_html(val); +}; + +exports.parseTheme = function(value) { + if (value[0] !== '=') + return ''; + var index = value.indexOf('/', 2); + if (index === -1) + return ''; + value = value.substring(1, index); + return value === '?' ? CONF.default_theme : value; }; -Async.prototype.refresh = function(name) { - var self = this; +exports.set = function(obj, path, value) { + var cachekey = 'S+' + path; - if (!self._isRunning) - return self; + if (F.temporary.other[cachekey]) + return F.temporary.other[cachekey](obj, value); - self._count = self.tasksAll.length; + if ((/__proto__|constructor|prototype|eval|function|\*|\+|;|\s|\(|\)|!/).test(path)) + return value; - for (var i = 0; i < self._count; i++) { + var arr = parsepath(path); + var builder = []; - var task = self.tasksPending[self.tasksAll[i]]; + 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 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; + return fn(obj, value, path); +}; - if (task.isRunning !== 0) - continue; +exports.get = function(obj, path) { - if (task.waiting !== null && typeof(self.tasksWaiting[task.waiting]) !== UNDEFINED) - continue; + var cachekey = 'G=' + path; - task.run(); - } + if (F.temporary.other[cachekey]) + return F.temporary.other[cachekey](obj); - if (self._count === 0) { - self._isRunning = false; - self.emit('complete'); - self.emit('percentage', 100); - self._max = 0; - var complete = self.onComplete; - var length = complete.length; - self.onComplete = []; - for (var i = 0; i < length; i++) { - try - { - complete[i](); - } catch (ex) { - self.emit('error', ex); - } - } - } + if ((/__proto__|constructor|prototype|eval|function|\*|\+|;|\s|\(|\)|!/).test(path)) + return; - return self; + var arr = parsepath(path); + var builder = []; + + for (var i = 0, length = arr.length - 1; i < length; i++) + builder.push('if(!w' + (!arr[i] || arr[i][0] === '[' ? '' : '.') + arr[i] + ')return'); + + 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 FileList() { - this.pending = []; - this.pendingDirectory = []; - this.directory = []; - this.file = []; - this.onComplete = null; - this.onFilter = null; +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; } -FileList.prototype.reset = function() { - var self = this; - self.file = []; - self.directory = []; - self.pendingDirectory = []; +global.Async = global.async = exports.async; +global.sync = global.SYNCHRONIZE = exports.sync; +global.sync2 = exports.sync2; + +// ============================================= +// SHELL SORT IMPLEMENTATION OF ALGORITHM +// ============================================= + +function _shellInsertionSort(list, length, gapSize, fn) { + var temp, i, j; + for (i = gapSize; i < length; i += gapSize ) { + j = i; + while(j > 0 && fn(list[j - gapSize], list[j]) === 1) { + temp = list[j]; + list[j] = list[j - gapSize]; + list[j - gapSize] = temp; + j -= gapSize; + } + } +} + +function shellsort(arr, fn) { + var length = arr.length; + var gapSize = Math.floor(length / 2); + while(gapSize) { + _shellInsertionSort(arr, length, gapSize, fn); + gapSize = Math.floor(gapSize / 2); + } + return arr; +} + +function EventEmitter2(obj) { + if (obj) { + !obj.emit && EventEmitter2.extend(obj); + return obj; + } else + this.$events = {}; +} + +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; + 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; +}; + +EE2P.on = function(name, fn) { + if (!this.$events) + this.$events = {}; + if (this.$events[name]) + this.$events[name].push(fn); + else + this.$events[name] = [fn]; + return this; +}; + +EE2P.once = function(name, fn) { + fn.$once = true; + return this.on(name, fn); +}; + +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; +}; + +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; }; -FileList.prototype.walk = function(directory) { +exports.EventEmitter2 = EventEmitter2; + +function Chunker(name, max) { + this.name = name; + this.max = max || 50; + this.index = 0; + 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); +} + +const CHP = Chunker.prototype; - var self = this; +CHP.append = CHP.write = function(obj) { + var self = this; - if (directory instanceof Array) { - var length = directory.length; + self.stack.push(obj); - for (var i = 0; i < length; i++) - self.pendingDirectory.push(directory[i]); + var tmp = self.stack.length; - self.next(); - return; - } + if (tmp >= self.max) { - fs.readdir(directory, function(err, arr) { + self.flushing++; + self.pages++; + self.count += tmp; - if (err) - return self.next(); + var index = (self.index++); - var length = arr.length; - for (var i = 0; i < length; i++) - self.pending.push(path.join(directory, arr[i])); + 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.next(); - }); + self.stack = []; + } + + return self; }; -FileList.prototype.stat = function(path) { - var self = this; +CHP.end = function() { + var self = this; + var tmp = self.stack.length; + if (tmp) { + self.flushing++; + self.pages++; + self.count += tmp; + + var index = (self.index++); - fs.stat(path, function(err, stats) { + 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; +}; - if (err) - return self.next(); +CHP.each = function(onItem, onEnd, indexer) { - if (stats.isDirectory() && (self.onFilter === null || self.onFilter(path, true))) { - self.directory.push(path); - self.pendingDirectory.push(path); - self.next(); - return; - } + var self = this; - if (self.onFilter === null || self.onFilter(path, false)) - self.file.push(path); + if (indexer == null) { + self.percentage = 0; + indexer = 0; + } - self.next(); - }); + 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; }; -FileList.prototype.next = function() { - var self = this; +CHP.read = function(index, callback) { + var self = this; + + if (self.flushing) { + self.flushing_timeout = setTimeout(() => self.read(index, callback), 300); + return; + } + + var filename = self.filename + index + '.chunker'; + + Fs.readFile(filename, function(err, data) { + + if (err) { + callback(null, EMPTYARRAY); + 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; +}; - if (self.pending.length > 0) { - var item = self.pending.shift(); - self.stat(item); - return; - } +CHP.clear = function() { + var files = []; + for (var i = 0; i < this.index; i++) + files.push(this.filename + i + '.chunker'); + files.wait((filename, next) => Fs.unlink(filename, next)); + return this; +}; - if (self.pendingDirectory.length > 0) { - var directory = self.pendingDirectory.shift(); - self.walk(directory); - return; - } +CHP.destroy = function() { + this.clear(); + this.indexer = 0; + this.flushing = 0; + clearTimeout(this.flushing_timeout); + this.stack = null; + return this; +}; - self.onComplete(self.file, self.directory); +exports.chunker = function(name, max) { + return new Chunker(name, max); }; -exports.Async = Async; +exports.Chunker = Chunker; -exports.sync = function(fn, owner) { - return function() { - - var args = [].slice.call(arguments); - var params; - var callback; - var executed = false; - var self = owner || this; - - args.push(function() { - params = arguments; - if (!executed && callback) { - executed = true; - callback.apply(self, params); - } - }); - - fn.apply(self, args); - - return function(cb) { - callback = cb; - if (!executed && params) { - executed = true; - callback.apply(self, params); - } - }; - }; -}; - -exports.async = function(fn) { - return function(complete) { - - var self = this; - var generator = fn(); - - next(null); - - function next(err, result) { - - var g; - - try - { - switch (err === null) { - case true: - g = generator.next(result); - break; - case false: - g = generator.throw(err); - break; - } - } catch (e) { - if (complete) { - if (typeof(complete) === OBJECT && complete.view500) - complete.view500(e); - else - complete(e); - } - return; - } - - if (g.done) { - if (complete && typeof(complete) !== OBJECT) - complete(null, g.value); - return; - } - - if (typeof(g.value) !== FUNCTION) { - next.call(self, null, g.value); - return; - } - - try - { - g.value.call(self, function() { - next.apply(self, arguments); - }); - } catch (e) { - setImmediate(function() { - next.call(self, e); - }); - } - } - - return generator.value; - }; +exports.ObjectToArray = function(obj) { + if (obj == null) + return EMPTYARRAY; + var keys = Object.keys(obj); + var output = []; + for (var i = 0, length = keys.length; i < length; i++) + output.push({ key: keys[i], value: obj[keys[i]]}); + return output; }; -// MIT -// Written by Jozef Gula -exports.getMessageLength = function(data, isLE) { +if (NODEVERSION > 699) { + exports.createBufferSize = (size) => Buffer.alloc(size || 0); + exports.createBuffer = (val, type) => Buffer.from(val || '', type); +} else { + exports.createBufferSize = (size) => new Buffer(size || 0); + exports.createBuffer = (val, type) => new Buffer(val || '', type); +} - var length = data[1] & 0x7f; +function Callback(count, callback) { + this.pending = count; + this.$callback = callback; +} +const CP = Callback.prototype; - if (length === 126) { +CP.done = function(callback) { + this.$callback = callback; + return this; +}; - if (data.length < 4) - return -1; +CP.next = function() { + var self = this; + self.pending--; + if (!self.pending && self.$callback) { + self.$callback(); + self.$callback = null; + } + return self; +}; - var a = 211; - var bLength = [data[3], data[2], 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; - return converBytesToInt64(bLength, 0, isLE); - } +global.Callback = Callback; - if (length === 127) { - if (data.Length < 10) - return -1; - var bLength = [data[9], data[8], data[7], data[6], data[5], data[4], data[3], data[2]]; - return converBytesToInt64(bLength, 0, isLE); - } - return length; +exports.Callback = function(count, callback) { + return new Callback(count, callback); }; -// MIT -// Written by Jozef Gula -function converBytesToInt64(data, startIndex, isLE) { - if (isLE) - return (data[startIndex] | (data[startIndex + 1] << 0x08) | (data[startIndex + 2] << 0x10) | (data[startIndex + 3] << 0x18) | (data[startIndex + 4] << 0x20) | (data[startIndex + 5] << 0x28) | (data[startIndex + 6] << 0x30) | (data[startIndex + 7] << 0x38)); - return ((data[startIndex + 7] << 0x20) | (data[startIndex + 6] << 0x28) | (data[startIndex + 5] << 0x30) | (data[startIndex + 4] << 0x38) | (data[startIndex + 3]) | (data[startIndex + 2] << 0x08) | (data[startIndex + 1] << 0x10) | (data[startIndex] << 0x18)); +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.async = exports.async; -global.sync = exports.sync; \ No newline at end of file +global.WAIT = exports.wait; +!global.F && require('./index'); diff --git a/websocketclient.js b/websocketclient.js new file mode 100644 index 000000000..eb5a03c1f --- /dev/null +++ b/websocketclient.js @@ -0,0 +1,690 @@ +// 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 WebSocketClient + * @version 3.4.4 + */ + +if (!global.framework_utils) + global.framework_utils = require('./utils'); + +const Crypto = require('crypto'); +const Https = require('https'); +const Http = require('http'); +const Url = require('url'); +const Zlib = require('zlib'); +const ENCODING = 'utf8'; +const WEBSOCKET_COMPRESS = Buffer.from([0x00, 0x00, 0xFF, 0xFF]); +const WEBSOCKET_COMPRESS_OPTIONS = { windowBits: Zlib.Z_DEFAULT_WINDOWBITS }; +const CONCAT = [null, null]; + +function WebSocketClient() { + this.current = {}; + this.$events = {}; + this.pending = []; + this.reconnect = 0; + this.closed = true; + + // type: json, text, binary + this.options = { type: 'json', compress: true, reconnect: 3000, encodedecode: true }; + this.cookies = {}; + this.headers = {}; +} + +const WebSocketClientProto = WebSocketClient.prototype; + +WebSocketClientProto.connect = function(url, protocol, origin) { + + var self = this; + var options = {}; + var key = Crypto.randomBytes(16).toString('base64'); + + self.url = url; + self.origin = origin; + self.protocol = protocol; + + url = Url.parse(url); + + var isSecure = url.protocol === 'wss:'; + + options.port = url.port || (isSecure ? 443 : 80); + options.host = url.hostname; + 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'; + protocol && (options.headers['Sec-WebSocket-Protocol'] = protocol); + origin && (options.headers['Sec-WebSocket-Origin'] = origin); + options.headers.Connection = 'Upgrade'; + options.headers.Upgrade = 'websocket'; + + var keys = Object.keys(self.headers); + for (var i = 0, length = keys.length; i < length; i++) + options.headers[keys[i]] = self.headers[keys[i]]; + + keys = Object.keys(self.cookies); + if (keys.length) { + var tmp = []; + for (var i = 0, length = keys.length; i < length; 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); + }); + + self.req.on('response', function() { + self.$events.error && self.emit('error', new Error('Unexpected server response.')); + if (self.options.reconnectserver) + self.connect(url, protocol, origin); + else + self.free(); + }); + + self.req.on('upgrade', function(response, socket) { + + self.socket = socket; + self.socket.$websocket = self; + + var compress = (response.headers['sec-websocket-extensions'] || '').indexOf('-deflate') !== -1; + var digest = Crypto.createHash('sha1').update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary').digest('base64'); + + if (response.headers['sec-websocket-accept'] !== digest) { + socket.destroy(); + self.closed = true; + self.$events.error && self.emit('error', new Error('Invalid server key.')); + self.free(); + return; + } + + self.closed = false; + self.socket.setTimeout(0); + self.socket.setNoDelay(); + self.socket.on('data', websocket_ondata); + self.socket.on('error', websocket_onerror); + self.socket.on('close', websocket_close); + self.socket.on('end', websocket_close); + + if (compress) { + self.inflatepending = []; + self.inflatelock = false; + self.inflate = Zlib.createInflateRaw(WEBSOCKET_COMPRESS_OPTIONS); + self.inflate.$websocket = self; + self.inflate.on('error', F.error()); + self.inflate.on('data', websocket_inflate); + self.deflatepending = []; + self.deflatelock = false; + self.deflate = Zlib.createDeflateRaw(WEBSOCKET_COMPRESS_OPTIONS); + self.deflate.$websocket = self; + self.deflate.on('error', F.error()); + self.deflate.on('data', websocket_deflate); + } + + self.$events.open && self.emit('open'); + }); +}; + +function websocket_ondata(chunk) { + this.$websocket.$ondata(chunk); +} + +function websocket_onerror(e) { + this.$websocket.$onerror(e); +} + +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); +} + +WebSocketClientProto.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; +}; + +WebSocketClientProto.on = function(name, fn) { + if (this.$events[name]) + this.$events[name].push(fn); + else + this.$events[name] = [fn]; + return this; +}; + +WebSocketClientProto.once = function(name, fn) { + fn.$once = true; + return this.on(name, fn); +}; + +WebSocketClientProto.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; +}; + +WebSocketClientProto.removeAllListeners = function(name) { + if (name === true) + this.$events = EMPTYOBJECT; + else if (name) + this.$events[name] = undefined; + else + this.$events = {}; + return this; +}; + +WebSocketClientProto.free = function() { + var self = this; + self.socket && self.socket.destroy(); + self.socket = null; + self.req && self.req.abort(); + self.req = null; + return self; +}; + +/** + * Internal handler written by Jozef Gula + * @param {Buffer} data + * @return {Framework} + */ +WebSocketClientProto.$ondata = function(data) { + + if (this.isClosed) + return; + + var current = this.current; + if (data) { + if (current.buffer) { + CONCAT[0] = current.buffer; + CONCAT[1] = data; + current.buffer = Buffer.concat(CONCAT); + } else + current.buffer = data; + } + + if (!this.$parse()) + return; + + if (!current.final && current.type !== 0x00) + current.type2 = current.type; + + var tmp; + var decompress = current.compressed && this.inflate; + + switch (current.type === 0x00 ? current.type2 : current.type) { + case 0x01: + + // text + if (decompress) { + current.final && this.parseInflate(); + } else { + tmp = this.$readbody(); + if (current.body) + current.body += tmp; + else + current.body = tmp; + current.final && this.$decode(); + } + + break; + + case 0x02: + // binary + if (decompress) { + current.final && this.parseInflate(); + } else { + tmp = this.$readbody(); + if (current.body) { + CONCAT[0] = current.body; + CONCAT[1] = tmp; + current.body = Buffer.concat(CONCAT); + } else + current.body = tmp; + current.final && this.$decode(); + } + + break; + + case 0x08: + 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: + // ping, response pong + this.socket.write(U.getWebSocketFrame(0, 'PONG', 0x0A)); + current.buffer = null; + current.inflatedata = null; + break; + + case 0x0A: + // pong + current.buffer = null; + current.inflatedata = null; + break; + } + + if (current.buffer) { + current.buffer = current.buffer.slice(current.length, current.buffer.length); + current.buffer.length && this.$ondata(); + } +}; + +function buffer_concat(buffers, length) { + var buffer = Buffer.alloc(length); + var offset = 0; + for (var i = 0, n = buffers.length; i < n; i++) { + buffers[i].copy(buffer, offset); + offset += buffers[i].length; + } + return buffer; +} + +// MIT +// Written by Jozef Gula +// Optimized by Peter Sirka +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) + return; + + // 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; + + // does frame contain mask? + current.isMask = ((current.buffer[1] & 0xfe) >> 7) === 0x01; + + // data length + var length = U.getMessageLength(current.buffer, F.isLE); + // index for data + + 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; + + // Check length of data + if (current.buffer.length < mlength) + return; + + current.length = mlength; + + // Not Ping & Pong + if (current.type !== 0x09 && current.type !== 0x0A) { + + // does frame contain mask? + if (current.isMask) { + current.mask = Buffer.alloc(4); + current.buffer.copy(current.mask, 0, index - 4, index); + } + + if (current.compressed && this.inflate) { + + var buf = Buffer.alloc(length); + current.buffer.copy(buf, 0, index, mlength); + + // does frame contain mask? + if (current.isMask) { + for (var i = 0; i < length; i++) + buf[i] = buf[i] ^ current.mask[i % 4]; + } + + // Does the buffer continue? + buf.$continue = current.final === false; + this.inflatepending.push(buf); + } else { + current.data = Buffer.alloc(length); + current.buffer.copy(current.data, 0, index, mlength); + } + } + + return true; +}; + +WebSocketClientProto.$readbody = function() { + + var current = this.current; + var length = current.data.length; + var buf; + + if (current.type === 1) { + + buf = Buffer.alloc(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 = 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; + } + +}; + +WebSocketClientProto.$decode = function() { + var data = this.current.body; + + if (global.F) + global.F.stats.performance.message++; + + switch (this.options.type) { + + case 'buffer': // Buffer + case 'binary': // Binary + // this.emit('message', Buffer.from(new Uint8Array(data))); + // break; + this.emit('message', data); + break; + + case 'json': // JSON + if (data instanceof Buffer) + data = data.toString(ENCODING); + this.options.encodedecode && (data = $decodeURIComponent(data)); + data.isJSON() && this.emit('message', F.onParseJSON(data, this.req)); + break; + + default: // TEXT + if (data instanceof Buffer) + data = data.toString(ENCODING); + this.emit('message', this.options.encodedecode ? $decodeURIComponent(data) : data); + break; + } + + this.current.body = null; +}; + +WebSocketClientProto.parseInflate = function() { + var self = this; + + if (self.inflatelock) + return; + + var buf = self.inflatepending.shift(); + if (buf) { + self.inflatechunks = []; + self.inflatechunkslength = 0; + self.inflatelock = true; + self.inflate.write(buf); + !buf.$continue && self.inflate.write(Buffer.from(WEBSOCKET_COMPRESS)); + self.inflate.flush(function() { + + if (!self.inflatechunks) + return; + + var data = buffer_concat(self.inflatechunks, self.inflatechunkslength); + + self.inflatechunks = null; + self.inflatelock = false; + + if (self.current.body) { + CONCAT[0] = self.current.body; + CONCAT[1] = data; + self.current.body = Buffer.concat(CONCAT); + } else + self.current.body = data; + + !buf.$continue && self.$decode(); + self.parseInflate(); + }); + } +}; + +WebSocketClientProto.$onerror = function(err) { + this.$events.error && this.emit('error', err); + if (!this.isClosed) { + this.isClosed = true; + this.$onclose(); + } +}; + +WebSocketClientProto.$onclose = function() { + + if (this._isClosed) + return; + + this.isClosed = true; + this._isClosed = true; + + if (this.inflate) { + this.inflate.removeAllListeners(); + this.inflate = null; + this.inflatechunks = null; + } + + if (this.deflate) { + this.deflate.removeAllListeners(); + this.deflate = null; + this.deflatechunks = null; + } + + this.$events.close && this.emit('close', this.closecode, this.closemessage); + this.socket && this.socket.removeAllListeners(); +}; + +/** + * Sends a message + * @param {String/Object} message + * @param {Boolean} raw The message won't be converted e.g. to JSON. + * @return {WebSocketClient} + */ +WebSocketClientProto.send = function(message, raw, replacer) { + + if (this.isClosed) + return this; + + var t = this.options.type; + + 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(Buffer.from(data, ENCODING)); + this.sendDeflate(); + } else + this.socket.write(U.getWebSocketFrame(0, data, 0x01)); + + } else if (message) { + if (this.deflate) { + this.deflatepending.push(message); + this.sendDeflate(); + } else + this.socket.write(U.getWebSocketFrame(0, message, 0x02)); + } + + return this; +}; + +/** + * 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) + return; + + var buf = self.deflatepending.shift(); + if (buf) { + self.deflatechunks = []; + self.deflatechunkslength = 0; + self.deflatelock = true; + self.deflate.write(buf); + self.deflate.flush(function() { + 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(); + } + }); + } +}; + +/** + * Ping message + * @return {WebSocketClient} + */ +WebSocketClientProto.ping = function() { + if (!this.isClosed) { + this.socket.write(U.getWebSocketFrame(0, '', 0x09)); + this.$ping = false; + } + return this; +}; + +function websocket_inflate(data) { + this.$websocket.inflatechunks.push(data); + this.$websocket.inflatechunkslength += data.length; +} + +function websocket_deflate(data) { + this.$websocket.deflatechunks.push(data); + this.$websocket.deflatechunkslength += data.length; +} + +/** + * Close connection + * @param {String} message Message. + * @param {Number} code WebSocket code. + * @return {WebSocketClient} + */ +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)); + } + return this; +}; + +function $decodeURIComponent(value) { + try + { + return decodeURIComponent(value); + } catch (e) { + return value; + } +} + +exports.create = function() { + return new WebSocketClient(); +}; \ No newline at end of file