total5
Version:
Total.js framework v5
1,868 lines (1,472 loc) • 42.5 kB
JavaScript
// Total.js Controller
// The MIT License
// Copyright 2023-2025 (c) Peter Širka <petersirka@gmail.com>
'use strict';
const REG_FILETMP = /\//g;
const REG_RANGE = /bytes=/;
const REG_ROBOT = /search|agent|bot|crawler|spider/i;
const REG_MOBILE = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|Tablet/i;
const REG_ENCODINGCLEANER = /[;\s]charset=utf-8/g;
const CHECK_DATA = { POST: 1, PUT: 1, PATCH: 1, DELETE: 1 };
const CHECK_COMPRESSION = { 'text/plain': true, 'text/javascript': true, 'text/css': true, 'text/jsx': true, 'application/javascript': true, 'application/x-javascript': true, 'application/json': true, 'application/xml': true, 'text/xml': true, 'image/svg+xml': true, 'text/x-markdown': true, 'text/html': true, 'application/x-ipynb+json': true, 'application/x-ijsnb+json': true };
const CHECK_CHARSET = { '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, 'text/x-markdown': true, 'text/html': true, 'application/x-ijsnb+json': true, 'application/x-ipynb+json': true };
const CHECK_NOCACHE = { zip: 1, rar: 1 };
const CHECK_MIN = /(\.|-|@)min/i;
const CHECK_CACHEDEBUG = { html: 1, js: 1, css: 1 };
const GZIP_FILE = { memLevel: 9 };
const GZIP_STREAM = { memLevel: 1 };
const NOCACHE = 'private, no-cache, no-store, max-age=0';
function Controller(req, res) {
var ctrl = this;
req.controller = ctrl;
ctrl.timeout = F.config.$httptimeout;
ctrl.req = req;
ctrl.res = res;
ctrl.method = ctrl.req.method;
ctrl.route = null;
ctrl.uri = F.TUtils.parseURI2(req.url);
ctrl.isfile = ctrl.uri.file;
ctrl.language = '';
ctrl.headers = req.headers;
ctrl.ext = ctrl.uri.ext;
ctrl.split = ctrl.uri.split;
ctrl.split2 = [];
ctrl.url = ctrl.uri.pathname;
ctrl.released = false;
ctrl.downloaded = false;
ctrl.protocol = req.connection.encrypted || req.headers['x-forwarded-ssl'] === 'on' || req.headers['x-forwarded-port'] === '443' || (req.headers['x-forwarded-proto'] || req.headers['x-forwarded-protocol']) === 'https' ? 'https' : 'http';
for (let path of ctrl.split)
ctrl.split2.push(path.toLowerCase());
ctrl.params = {};
ctrl.query = ctrl.uri.search.parseEncoded();
ctrl.files = [];
ctrl.body = {};
if (ctrl.isfile)
F.stats.performance.file++;
else
F.stats.performance.request++;
// ctrl.payload = null;
// ctrl.payloadsize = 0;
// ctrl.user = null;
ctrl.datatype = ''; // json|xml|multipart|urlencoded
ctrl.response = {
status: 200,
cache: !global.DEBUG || !CHECK_CACHEDEBUG[ctrl.ext],
minify: true,
// minifyjson: false
// encrypt: false
headers: {}
};
if (CHECK_DATA[ctrl.method]) {
if (ctrl.isfile) {
ctrl.destroyed = true;
ctrl.req.destroy();
return;
}
let type = ctrl.headers['content-type'] || '';
let index = type.indexOf(';', 10);
if (index != -1)
type = type.substring(0, index);
switch (type) {
case 'application/json':
case 'text/json':
ctrl.datatype = 'json';
break;
case 'application/x-www-form-urlencoded':
ctrl.datatype = 'urlencoded';
break;
case 'multipart/form-data':
ctrl.datatype = 'multipart';
break;
case 'application/xml':
case 'text/xml':
ctrl.datatype = 'xml';
break;
case 'text/html':
case 'text/plain':
ctrl.datatype = 'text';
break;
default:
ctrl.datatype = 'binary';
break;
}
}
}
Controller.prototype = {
get query() {
return this.$query || (this.$query = ctrl.uri.search.parseEncoded());
},
set query(val) {
this.$query = val;
},
get mobile() {
let ua = this.headers['user-agent'];
return ua ? REG_MOBILE.test(ua) : false;
},
get robot() {
let ua = this.headers['user-agent'];
return ua ? REG_ROBOT.test(ua) : false;
},
get xhr() {
return this.headers['x-requested-with'] === 'XMLHttpRequest';
},
get extension() {
return this.ext;
},
get ua() {
if (this.$ua != null)
return this.$ua;
this.$ua = this.headers['user-agent'] || '';
if (this.$ua)
this.$ua = this.$ua.parseUA();
return this.$ua;
},
get ip() {
if (this.$ip != null)
return this.$ip;
// x-forwarded-for: client, proxy1, proxy2, ...
let proxy = this.headers['x-forwarded-for'];
if (proxy)
this.$ip = proxy.split(',', 1)[0] || this.req.connection.remoteAddress;
else if (!this.$ip)
this.$ip = this.req.connection.remoteAddress;
return this.$ip;
},
get referrer() {
return this.headers.referer;
},
get host() {
return this.headers.host;
},
get address() {
return (this.protocol + '://' + this.headers?.host || '') + (this.req?.url || '');
}
};
Controller.prototype.callback = function(err, value) {
var ctrl = this;
if (arguments.length == 0) {
return function(err, response) {
if (err)
ctrl.invalid(err);
else
ctrl.json(response);
};
}
if (err)
ctrl.invalid(err);
else {
if (value === undefined)
ctrl.success();
else
ctrl.json(value);
}
};
Controller.prototype.cancel = function() {
this.iscanceled = true;
};
Controller.prototype.csrf = function() {
return F.def.onCSRFcreate(this);
};
Controller.prototype.redirect = function(value, permanent) {
var ctrl = this;
if (ctrl.destroyed)
return;
ctrl.response.headers.Location = value;
ctrl.response.status = permanent ? 301 : 302;
ctrl.flush();
F.stats.response.redirect++;
};
Controller.prototype.html = function(value) {
var ctrl = this;
if (ctrl.destroyed)
return;
ctrl.response.headers['content-type'] = 'text/html';
if (value != null)
ctrl.response.value = ctrl.response.minify && F.config.$minifyhtml ? F.TMinificators.html(value) : value;
ctrl.flush();
F.stats.response.html++;
};
Controller.prototype.xml = function(value) {
var ctrl = this;
if (ctrl.destroyed)
return;
if (value != null)
ctrl.response.value = value;
ctrl.response.headers['content-type'] = 'text/xml';
ctrl.flush();
F.stats.response.xml++;
};
Controller.prototype.text = Controller.prototype.plain = function(value) {
var ctrl = this;
if (ctrl.destroyed)
return;
ctrl.response.headers['content-type'] = 'text/plain';
if (value != null)
ctrl.response.value = value;
ctrl.flush();
F.stats.response.text++;
};
Controller.prototype.respond = function(value, a, b) {
var ctrl = this;
var output = ctrl.response.output;
switch (output) {
case 'html':
case 'plain':
case 'text':
case 'xml':
case 'json':
case 'redirect':
case 'jsonstring':
case 'empty':
case 'file':
ctrl[output](value, a, b);
break;
default:
ctrl.json(value, a, b);
break;
}
return ctrl;
};
Controller.prototype.json = function(value, beautify, replacer) {
var ctrl = this;
if (ctrl.destroyed)
return;
var response = ctrl.response;
response.headers['content-type'] = 'application/json';
response.headers['cache-control'] = NOCACHE;
response.headers.vary = 'Accept-Encoding, Last-Modified, User-Agent';
response.headers.expires = '-1';
response.value = JSON.stringify(value, beautify ? '\t' : null, replacer);
ctrl.flush();
F.stats.response.json++;
};
Controller.prototype.jsonstring = function(value) {
var ctrl = this;
if (ctrl.destroyed)
return;
var response = ctrl.response;
response.headers['content-type'] = 'application/json';
response.headers['cache-control'] = NOCACHE;
response.headers.vary = 'Accept-Encoding, Last-Modified, User-Agent';
response.headers.expires = '-1';
response.value = value;
response.type = 'json';
ctrl.flush();
F.stats.response.json++;
};
Controller.prototype.empty = function() {
var ctrl = this;
if (ctrl.destroyed)
return;
ctrl.response.status = 204;
ctrl.flush();
F.stats.response.empty++;
};
function $errorhandling(ctrl, err) {
var response = ctrl.response;
response.headers['content-type'] = 'application/json';
response.headers['cache-control'] = NOCACHE;
response.headers.vary = 'Accept-Encoding, Last-Modified, User-Agent';
response.value = JSON.stringify(err.output(ctrl.language));
response.status = err.status === 408 ? 503 : err.status;
ctrl.flush();
var key = 'error' + err.status;
if (F.stats.response[key] != null)
F.stats.response[key]++;
}
Controller.prototype.invalid = function(value) {
var ctrl = this;
if (ctrl.destroyed)
return;
var err;
if (value instanceof F.TBuilders.ErrorBuilder) {
err = value;
} else {
err = new F.TBuilders.ErrorBuilder();
err.push(value);
}
setTimeout($errorhandling, 1, ctrl, err);
};
Controller.prototype.flush = function() {
var ctrl = this;
if (ctrl.destroyed)
return;
let accept = ctrl.headers['accept-encoding'];
let response = ctrl.response;
let buffer = response.value ? response.value instanceof Buffer ? response.value : Buffer.from(response.value, 'utf8') : null;
let type = response.headers['content-type'];
if (F.config.$xpoweredby)
response.headers['x-powered-by'] = F.config.$xpoweredby;
// GZIP compression
if (F.config.$httpcompress && buffer && accept && buffer.length > 256 && accept.indexOf('gzip') !== -1) {
if (CHECK_COMPRESSION[type]) {
if (CHECK_CHARSET[type])
response.headers['content-type'] += '; charset=utf-8';
F.Zlib.gzip(buffer, function(err, buffer) {
if (err) {
ctrl.fallback(400, err.toString());
} else {
response.headers['content-encoding'] = 'gzip';
ctrl.res.writeHead(response.status, response.headers);
if (ctrl.method === 'HEAD') {
ctrl.res.end();
} else {
ctrl.res.end(buffer, 'utf8');
F.stats.performance.upload += buffer.length / 1024 / 1024;
}
ctrl.free();
}
});
return;
}
}
if (CHECK_CHARSET[type])
response.headers['content-type'] += '; charset=utf-8';
try {
ctrl.res.writeHead(response.status, response.headers);
if (ctrl.method === 'HEAD')
ctrl.res.end();
else
ctrl.res.end(buffer);
} finally {
ctrl.free();
}
};
Controller.prototype.fallback = function(code, err) {
var ctrl = this;
if (ctrl.destroyed)
return;
let key = code + '';
var route = F.routes.fallback[key];
if (route) {
ctrl.route = route;
ctrl.route.action(ctrl);
} else {
var view;
if (ctrl.xhr) {
if (code === 999)
code = 503;
ctrl.invalid(code);
return;
}
// Paused
if (code === 999) {
view = F.temporary.views.$pause;
ctrl.response.status = 503;
if (!view) {
F.temporary.views.$pause = view = new F.TViewEngine.View();
view.compiled = F.TViewEngine.compile('$pause', F.Fs.readFileSync(F.Path.join(F.config.$total5, 'pause.html'), 'utf8'), false);
}
view.model = F.paused;
} else {
ctrl.response.status = code === 408 ? 503 : code;
view = F.temporary.views.$error;
if (!view) {
F.temporary.views.$error = view = new F.TViewEngine.View();
view.compiled = F.TViewEngine.compile('$error', F.Fs.readFileSync(F.Path.join(F.config.$total5, 'error.html'), 'utf8'), false);
}
view.model = { code: code, status: F.TUtils.httpstatus(code), error: err ? (DEBUG ? err.toString() : '') : '' };
}
ctrl.html(view.compiled(view));
}
};
Controller.prototype.layout = function(name) {
var ctrl = this;
ctrl.response.layout = name;
};
Controller.prototype.view = function(name, model) {
var ctrl = this;
var view = new F.TViewEngine.View(ctrl);
if (!name)
return view;
if (ctrl.destroyed)
return;
ctrl.response.layout && view.layout(ctrl.response.layout);
setImmediate(renderview, view, name, model);
return view;
};
function renderview(view, name, model) {
view.controller.html(view.render(name, model));
}
Controller.prototype.file = function(path, download) {
var ctrl = this;
if (ctrl.destroyed)
return;
var response = ctrl.response;
if (download) {
if (typeof(download) !== 'string')
download = F.TUtils.getName(path);
response.headers['content-disposition'] = 'attachment; filename*=utf-8\'\'' + encodeURIComponent(download);
}
var ext = F.TUtils.getExtension(path);
if (ext === 'js') {
if (response.minify) {
response.minify = F.config.$minifyjs;
if (response.minify)
response.minify = !CHECK_MIN.test(path);
}
} else if (ext === 'css') {
if (response.minify) {
response.minify = F.config.$minifycss;
if (response.minify)
response.minify = !CHECK_MIN.test(path);
}
} else if (ext === 'html') {
if (response.minify) {
response.minify = F.config.$minifyhtml;
if (response.minify)
response.minify = !CHECK_MIN.test(path);
}
}
if (response.minify) {
switch (ext) {
case 'js':
send_js(ctrl, path);
break;
case 'css':
send_css(ctrl, path);
break;
case 'html':
send_html(ctrl, path);
break;
default:
send_file(ctrl, path, ext);
break;
}
} else
send_file(ctrl, path, ext);
F.stats.response.file++;
};
Controller.prototype.stream = function(type, stream, download) {
var ctrl = this;
if (ctrl.destroyed)
return;
var response = ctrl.response;
if (download && typeof(download) === 'string')
response.headers['content-disposition'] = 'attachment; filename*=utf-8\'\'' + encodeURIComponent(download);
var accept = ctrl.headers['accept-encoding'];
var compress = F.config.$httpcompress && accept && CHECK_COMPRESSION[type] && accept.indexOf('gzip') !== -1;
if (response.headers.expires)
delete response.headers.expires;
response.headers.etag = '858' + F.config.$httpetag;
if (CHECK_CHARSET[type])
type += '; charset=utf-8';
response.headers['content-type'] = type || 'application/octet-stream';
if (compress)
response.headers['content-encoding'] = 'gzip';
ctrl.res.writeHead(response.status, response.headers);
if (compress)
stream.pipe(F.Zlib.createGzip(GZIP_STREAM)).pipe(ctrl.res);
else
stream.pipe(ctrl.res);
F.stats.response.stream++;
};
Controller.prototype.filefs = function(name, id, download, checkmeta) {
var ctrl = this;
if (ctrl.destroyed)
return;
var opt = {};
opt.id = id;
opt.download = download;
opt.check = checkmeta;
F.filestorage(name).http(ctrl, opt);
return opt;
};
Controller.prototype.image = function(opt) {
// opt.ext {String} required, image extension
// opt.date {Date} optional, for HTTP cache
// opt.cache {String} optional, a cache key
// opt.load {Function(next(stream), opt)};
// opt.image {Function(img, opt)}
var ctrl = this;
var date = opt.date ? opt.date instanceof Date ? opt.date.toUTCString() : opt.date : null;
// HTTP Cache
if (ctrl.response.cache && date && ctrl.notmodified(date))
return;
if (opt.cache) {
var tmp = F.path.tmp('img_' + opt.cache + '.' + opt.ext);
if (!DEBUG && date)
ctrl.httpcache(date);
F.Fs.lstat(tmp, function(err) {
if (err) {
opt.load.call(ctrl, function(stream) {
var img = F.TImages.load(stream);
opt.image.call(ctrl, img, opt);
img.save(tmp, function(err) {
if (err)
ctrl.fallback(404, err);
else
ctrl.file(tmp);
});
}, opt);
} else
ctrl.file(tmp);
});
} else {
opt.load.call(ctrl, function(stream) {
var img = F.TImages.load(stream);
var response = ctrl.response;
opt.image.call(ctrl, img, opt);
if (ctrl.response.headers.expires)
delete response.headers.expires;
if (!DEBUG && date)
ctrl.httpcache(date);
response.headers.etag = '858' + F.config.$httpetag;
response.headers['content-type'] = F.TUtils.getContentType(opt.ext);
ctrl.res.writeHead(response.status, response.headers);
F.stats.response.stream++;
img.pipe(ctrl.res);
}, opt);
}
};
Controller.prototype.binary = function(buffer, type, download) {
var ctrl = this;
if (ctrl.destroyed)
return;
var response = ctrl.response;
response.headers['content-type'] = type;
response.type = 'binary';
if (typeof(download) === 'string')
response.headers['content-disposition'] = 'attachment; filename*=utf-8\'\'' + encodeURIComponent(download);
response.value = buffer;
ctrl.flush();
F.stats.response.binary++;
};
Controller.prototype.proxy = function(opt) {
var ctrl = this;
if (ctrl.destroyed)
return;
if (typeof(opt) === 'string')
opt = { url: opt };
if (!opt.headers)
opt.headers = {};
if (!opt.method)
opt.method = ctrl.method;
opt.resolve = true;
opt.encoding = 'binary';
opt.body = ctrl.payload;
var tmp;
if (opt.url.indexOf('?') === -1) {
tmp = F.TUtils.toURLEncode(ctrl.query);
if (tmp)
opt.url += '?' + tmp;
}
for (let key in ctrl.headers) {
switch (key) {
case 'x-forwarded-for':
case 'x-forwarded-protocol':
case 'x-forwarded-proto':
case 'x-nginx-proxy':
case 'connection':
case 'host':
case 'accept-encoding':
break;
default:
opt.headers[key] = ctrl.headers[key];
break;
}
}
if (!opt.timeout)
opt.timeout = 10000;
var prepare = opt.callback;
opt.callback = function(err, response) {
prepare && prepare(err, response);
if (err) {
ctrl.invalid(err);
return;
}
ctrl.response.status = response.status;
ctrl.binary(response.body, (response.headers['content-type'] || 'text/plain').replace(REG_ENCODINGCLEANER, ''));
};
REQUEST(opt);
};
Controller.prototype.successful = function(callback) {
var ctrl = this;
return function(err, a, b, c) {
if (err)
ctrl.invalid(err);
else
callback.call(ctrl, a, b, c);
};
};
Controller.prototype.done = function(arg) {
var ctrl = this;
return function(err, response) {
if (err)
ctrl.invalid(err);
else
ctrl.json(DEF.onSuccess(arg === true ? response : arg));
};
};
Controller.prototype.transform = function(name, value, callback) {
return F.transform(name, value, callback, this);
};
Controller.prototype.success = function(value) {
F.TUtils.success.value = value;
this.json(F.TUtils.success);
};
Controller.prototype.clear = function() {
var ctrl = this;
if (ctrl.files.length) {
let remove = [];
for (var file of ctrl.files) {
if (file.removable)
remove.push(file.path);
}
F.path.unlink(remove);
ctrl.files.length = 0;
}
};
Controller.prototype.cookie = function(name, value, expires, options) {
let ctrl = this;
let arr;
if (value === undefined) {
if (ctrl.cookies)
return F.TUtils.decodeURIComponent(ctrl.cookies[name] || '');
let cookie = ctrl.headers.cookie;
if (!cookie) {
ctrl.cookies = F.EMPTYOBJECT;
return '';
}
ctrl.cookies = {};
arr = cookie.split(';');
for (let i = 0; i < arr.length; i++) {
let line = arr[i].trim();
let index = line.indexOf('=');
if (index !== -1)
ctrl.cookies[line.substring(0, index)] = line.substring(index + 1);
}
return name ? F.TUtils.decodeURIComponent(ctrl.cookies[name] || '') : '';
}
let cookiename = name + '=';
let builder = [cookiename + value];
let type = typeof(expires);
if (expires && !(expires instanceof Date) && type === 'object') {
options = expires;
expires = options.expires || options.expire || null;
}
if (type === 'string')
expires = expires.parseDateExpiration();
if (!options)
options = {};
if (!options.path)
options.path = '/';
expires && builder.push('Expires=' + expires.toUTCString());
options.domain && builder.push('Domain=' + options.domain);
options.path && builder.push('Path=' + options.path);
if (options.secure == true || (options.secure == null && F.config.$cookiesecure))
builder.push('Secure');
if (options.httpOnly || options.httponly || options.HttpOnly)
builder.push('HttpOnly');
let same = options.security || options.samesite || F.config.$cookiesamesite;
switch (same) {
case 1:
same = 'Lax';
break;
case 2:
same = 'Strict';
break;
}
builder.push('SameSite=' + same);
arr = ctrl.response.headers['set-cookie'] || [];
// Cookie, already, can be in array, resulting in duplicate 'set-cookie' header
if (arr.length) {
let l = cookiename.length;
for (let i = 0; i < arr.length; i++) {
if (arr[i].substring(0, l) === cookiename) {
arr.splice(i, 1);
break;
}
}
}
arr.push(builder.join('; '));
ctrl.response.headers['set-cookie'] = arr;
return ctrl;
};
Controller.prototype.custom = function() {
this.destroyed = true;
};
Controller.prototype.autoclear = function(value) {
this.preventclearfiles = value === false;
};
Controller.prototype.resume = function() {
let ctrl = this;
if (ctrl.isfile) {
let path = ctrl.uri.key;
if (CONF.$root)
path = path.substring(CONF.$root.length - 1);
if (path[1] === '_') {
let tmp = path.substring(1);
let index = tmp.indexOf('/', 1);
if (index === -1) {
ctrl.fallback(404);
return;
}
path = F.path.root('plugins/' + tmp.substring(1, index) + '/public/' + tmp.substring(index + 1));
} else
path = F.path.public(path.substring(1));
switch (ctrl.ext) {
case 'js':
if (CHECK_MIN.test(path))
send_file(ctrl, path, ctrl.ext);
else
send_js(ctrl, path);
break;
case 'css':
if (CHECK_MIN.test(path))
send_file(ctrl, path, ctrl.ext);
else
send_css(ctrl, path);
break;
case 'html':
if (CHECK_MIN.test(path))
send_file(ctrl, path, ctrl.ext);
else
send_html(ctrl, path);
break;
default:
send_file(ctrl, path, ctrl.ext);
break;
}
} else
ctrl.fallback(404);
};
Controller.prototype.free = function() {
var ctrl = this;
if (ctrl.released)
return;
ctrl.released = true;
ctrl.destroyed = true;
ctrl.payload = null;
// Potential problem
// ctrl.body = null;
// ctrl.params = null;
// ctrl.query = null;
if (ctrl.preventclearfiles != true)
ctrl.clear();
// Clear resources
ctrl.req.controller = null;
};
Controller.prototype.hostname = function(path) {
var ctrl = this;
return ctrl.protocol + '://' + ctrl.headers.host + (path ? path : '');
};
Controller.prototype.$route = function() {
var ctrl = this;
if (ctrl.isfile) {
if (F.routes.files.length) {
let route = F.TRouting.lookupfile(ctrl);
if (route) {
ctrl.route = route;
if (route.middleware.length)
middleware(ctrl);
else
route.action(ctrl);
return;
}
}
if (F.config.$httpfiles[ctrl.ext])
ctrl.resume();
else
ctrl.fallback(404);
return;
}
let route = F.TRouting.lookup(ctrl);
if (route) {
ctrl.route = route;
// process data
// call action
if (ctrl.datatype === 'multipart') {
multipart(ctrl);
} else if (ctrl.datatype) {
ctrl.payload = [];
ctrl.payloadsize = 0;
ctrl.toolarge = false;
ctrl.downloaded = false;
ctrl.req.on('data', function(chunk) {
ctrl.payloadsize += chunk.length;
if (ctrl.payloadsize > ctrl.route.size) {
if (!ctrl.toolarge) {
ctrl.toolarge = true;
delete ctrl.payload;
}
} else
ctrl.payload.push(chunk);
});
ctrl.req.on('abort', () => ctrl.free());
ctrl.req.on('end', function() {
ctrl.downloaded = true;
if (ctrl.toolarge) {
ctrl.fallback(431);
return;
}
ctrl.payload = Buffer.concat(ctrl.payload);
F.stats.performance.download += ctrl.payload.length / 1024 / 1024;
let val;
switch (ctrl.datatype) {
case 'json':
val = ctrl.payload.toString('utf8');
ctrl.body = val ? F.def.parsers.json(val) : null;
break;
case 'urlencoded':
val = ctrl.payload.toString('utf8');
ctrl.body = val ? F.def.parsers.urlencoded(val) : {};
break;
}
authorize(ctrl);
});
} else
authorize(ctrl);
} else
ctrl.fallback(404);
};
Controller.prototype.href = function(key, value) {
return F.TViewEngine.prototype.href.call(this, key, value);
};
function readfile(filename, callback) {
F.stats.performance.open++;
F.Fs.lstat(filename, function(err, stats) {
if (err) {
callback(err);
return;
}
F.stats.performance.open++;
F.Fs.readFile(filename, 'utf8', function(err, text) {
if (err) {
callback(err);
} else {
let obj = {};
obj.date = stats.mtime.toUTCString();
obj.body = text.ROOT();
callback(null, obj);
}
});
});
}
/*
@Path: Controller
@Method: instance.authorize([callback]); #callback {Function(err, session)} optional;
The method performs "manual" authorization. If the user is logged in, then `session {Object}` is not null, otherwise `null`. If you don't use the `callback` argument, then the method returns `Promise`.
*/
Controller.prototype.authorize = function(callback) {
var ctrl = this;
if (!callback)
return new Promise((resolve, reject) => ctrl.authorize((err, response) => resolve(response)));
if (F.def.onAuthorize) {
var opt = new F.TBuilders.Options(ctrl);
opt.TYPE = 'auth';
opt.query = ctrl.query;
opt.next = opt.callback;
opt.$callback = function(err, response) {
if (response)
ctrl.user = response;
callback(null, response);
};
F.def.onAuthorize(opt);
} else
callback();
};
Controller.prototype.action = function(name, model) {
return F.action(name, model, this);
};
Controller.prototype.notmodified = function(date) {
var ctrl = this;
if (ctrl.headers['if-modified-since'] === date) {
ctrl.response.status = 304;
ctrl.response.headers['cache-control'] = 'public, must-revalidate, max-age=' + F.config.$httpmaxage; // 5 min.
ctrl.response.headers['last-modified'] = date;
ctrl.flush();
F.stats.response.notmodified++;
return true;
}
};
Controller.prototype.httpcache = function(date) {
var ctrl = this;
if (date instanceof Date)
date = date.toUTCString();
if (!ctrl.response.headers.expires)
ctrl.response.headers.expires = F.config.$httpexpire;
if (!ctrl.response.headers['cache-control'])
ctrl.response.headers['cache-control'] = 'public, must-revalidate, max-age=' + F.config.$httpmaxage; // 5 minute cache for revalidate (304)
ctrl.response.headers['last-modified'] = date;
ctrl.response.headers.etag = '858' + F.config.$httpetag;
};
function multipart(ctrl) {
var type = ctrl.headers['content-type'];
var index = type.indexOf('boundary=');
if (index === -1) {
ctrl.fallback(400);
return;
}
var end = type.length;
for (var i = (index + 10); i < end; i++) {
if (type[i] === ';' || type[i] === ' ') {
end = i;
break;
}
}
var boundary = type.substring(index + 9, end);
var parser = F.TUtils.multipartparser(boundary, ctrl.req, function(err, meta) {
F.stats.performance.download += meta.size / 1024 / 1024;
for (var i = 0; i < meta.files.length; i++) {
var item = meta.files[i];
var file = new HttpFile(item);
// IE9 sends absolute filename
var index = file.filename.lastIndexOf('\\');
// For Unix like senders
if (index === -1)
index = file.filename.lastIndexOf('/');
if (index !== -1)
file.filename = file.filename.substring(index + 1);
file.ext = F.TUtils.getExtension(file.filename);
ctrl.files.push(file);
}
// Error
if (err) {
ctrl.clear();
switch (err[0][0]) {
case '4':
case '5':
case '6':
ctrl.fallback(431, err[0]);
break;
default:
ctrl.fallback(400, err[0]);
break;
}
} else {
ctrl.body = meta.body;
authorize(ctrl);
}
});
parser.skipcheck = !F.config.$httpchecktypes;
parser.limits.total = ctrl.route.size;
}
function authorize(ctrl) {
if (F.def.onAuthorize) {
var opt = new F.TBuilders.Options(ctrl);
opt.TYPE = 'auth'; // important
opt.query = ctrl.query;
opt.next = opt.callback;
opt.$callback = function(err, user) {
let auth = user ? 1 : 2;
ctrl.user = user;
if (ctrl.route.auth === auth) {
execute(ctrl);
} else {
ctrl.route = F.TRouting.lookup(ctrl, auth);
if (ctrl.route)
execute(ctrl);
else
ctrl.fallback(401);
}
};
F.def.onAuthorize(opt);
} else
execute(ctrl);
}
function execute(ctrl, skipmiddleware) {
if (ctrl.route.timeout)
ctrl.timeout = ctrl.route.timeout;
for (let param of ctrl.route.params) {
let value = ctrl.split[param.index];
ctrl.params[param.name] = value;
}
if (!ctrl.language && F.def.onLocalize) {
ctrl.language = F.def.onLocalize(ctrl);
ctrl.uri.cache += ctrl.language;
}
if (!skipmiddleware && ctrl.route.middleware.length) {
middleware(ctrl);
} else {
if (ctrl.route.api) {
let body = ctrl.body;
if (body && typeof(body) === 'object' && body.schema && typeof(body.schema) === 'string') {
let index = body.schema.indexOf('?');
let query = null;
if (index !== -1) {
query = body.schema.substring(index + 1);
body.schema = body.schema.substring(0, index);
}
let schema = body.schema.split('/');
let endpoint = ctrl.route.api[schema[0]];
if (endpoint) {
if ((endpoint.auth === 1 && ctrl.user == null) || (endpoint.auth === 2 && ctrl.user)) {
ctrl.fallback(401);
return;
}
let params = {};
if (endpoint.params) {
let err = null;
for (let m of endpoint.params) {
params[m.name] = schema[m.index] || '';
if (!params[m.name]) {
if (!err)
err = new F.TBuilders.ErrorBuilder();
err.push2('params.' + m.name);
}
}
if (err) {
ctrl.invalid(err);
return;
}
}
body = body.data;
if (!body || typeof(body) === 'object') {
if (endpoint.timeout)
ctrl.timeout = endpoint.timeout;
ctrl.params = params;
ctrl.query = query ? query.parseEncoded() : {};
if (body)
ctrl.body = body;
if (F.$events.controller) {
F.emit('controller', ctrl);
if (ctrl.iscanceled)
return;
}
let action = endpoint.action;
if (action)
action(ctrl);
else
F.action(endpoint.actions, ctrl.body, ctrl).autorespond();
return;
}
}
}
ctrl.fallback(400, 'Invalid data');
} else {
if (F.$events.controller) {
/*
@Path: Framework
@Event: ON('controller', function(ctrl) { ... }); #ctrl {Controller};
The event captures all processed controllers on HTTP requests. The next processing can be canceled via the `ctrl.cancel()` method.
*/
F.emit('controller', ctrl);
if (ctrl.iscanceled)
return;
}
if (ctrl.route.actions) {
F.action(ctrl.route.actions, ctrl.body, ctrl).autorespond();
} else {
if (ctrl.route.view) {
ctrl.view(ctrl.route.view);
return;
}
let action = ctrl.route.action;
if (!action)
action = auto_view;
action(ctrl);
}
}
}
}
function auto_view(ctrl) {
ctrl.view(ctrl.split[0] || 'index');
}
function send_html(ctrl, path) {
if (!ctrl.language && F.def.onLocalize) {
ctrl.language = F.def.onLocalize(ctrl);
ctrl.uri.cache += ctrl.language;
}
if (F.temporary.notfound[ctrl.uri.cache]) {
ctrl.fallback(404);
return;
}
let filename = F.temporary.minified[ctrl.uri.cache];
if (filename) {
send_file(ctrl, filename, 'html');
return;
}
if (!DEBUG) {
if (F.temporary.files[ctrl.uri.cache]) {
F.temporary.files[ctrl.uri.cache].push(ctrl);
return;
}
F.temporary.files[ctrl.uri.cache] = [];
}
readfile(path, function(err, output) {
if (err) {
if (!DEBUG) {
F.temporary.notfound[ctrl.uri.cache] = 1;
for (let $ of F.temporary.files[ctrl.uri.cache])
$.fallback(404);
delete F.temporary.files[ctrl.uri.cache];
}
ctrl.fallback(404);
return;
}
output.body = F.translate(ctrl.language, output.body);
if (ctrl.response.minify && F.config.$minifyhtml)
output.body = F.TMinificators.html(output.body);
if (DEBUG) {
ctrl.response.headers['cache-control'] = NOCACHE;
ctrl.response.headers['last-modified'] = output.date;
ctrl.response.headers['content-type'] = 'text/html';
ctrl.response.value = output.body;
ctrl.flush();
} else {
let filename = F.path.tmp(F.clusterid + ctrl.uri.cache.substring(1).replace(REG_FILETMP, '-') + '-min.html');
F.Fs.writeFile(filename, output.body, function(err) {
if (err) {
err = err.toString();
F.temporary.notfound[ctrl.uri.cache] = 1;
ctrl.fallback(404, err);
for (let $ of F.temporary.files[ctrl.uri.cache])
$.fallback(404, err);
} else {
F.temporary.minified[ctrl.uri.cache] = filename;
send_file(ctrl, filename, 'html');
for (let $ of F.temporary.files[ctrl.uri.cache])
send_file($, filename, 'html');
}
delete F.temporary.files[ctrl.uri.cache];
});
}
});
}
function send_css(ctrl, path) {
if (F.temporary.notfound[ctrl.uri.cache]) {
ctrl.fallback(404);
return;
}
let filename = F.temporary.minified[ctrl.uri.cache];
if (filename) {
send_file(ctrl, filename, 'css');
return;
}
if (!DEBUG) {
if (F.temporary.files[ctrl.uri.cache]) {
F.temporary.files[ctrl.uri.cache].push(ctrl);
return;
} else
F.temporary.files[ctrl.uri.cache] = [];
}
readfile(path, function(err, output) {
if (err) {
if (!DEBUG) {
for (let $ of F.temporary.files[ctrl.uri.cache])
$.fallback(404);
delete F.temporary.files[ctrl.uri.cache];
F.temporary.notfound[ctrl.uri.cache] = 1;
}
ctrl.fallback(404);
return;
}
if (ctrl.response.minify && F.config.$minifycss)
output.body = F.TMinificators.css(output.body);
if (DEBUG) {
ctrl.response.headers['cache-control'] = NOCACHE;
ctrl.response.headers['last-modified'] = output.date;
ctrl.response.headers['content-type'] = 'text/css';
ctrl.response.value = output.body;
ctrl.flush();
} else {
let filename = F.path.tmp(F.clusterid + ctrl.uri.cache.substring(1).replace(REG_FILETMP, '-') + '-min.css');
F.Fs.writeFile(filename, output.body, function(err) {
if (err) {
F.temporary.notfound[ctrl.uri.cache] = 1;
err = err.toString();
ctrl.fallback(404, err);
for (let $ of F.temporary.files[ctrl.uri.cache])
$.fallback(404, err);
} else {
F.temporary.minified[ctrl.uri.cache] = filename;
send_file(ctrl, filename, 'css');
for (let $ of F.temporary.files[ctrl.uri.cache])
send_file($, filename, 'css');
}
delete F.temporary.files[ctrl.uri.cache];
});
}
});
}
function send_js(ctrl, path) {
if (F.temporary.notfound[ctrl.uri.cache]) {
ctrl.fallback(404);
return;
}
let filename = F.temporary.minified[ctrl.uri.cache];
if (filename) {
send_file(ctrl, filename, 'js');
return;
}
if (!DEBUG) {
if (F.temporary.files[ctrl.uri.cache]) {
F.temporary.files[ctrl.uri.cache].push(ctrl);
return;
} else
F.temporary.files[ctrl.uri.cache] = [];
}
readfile(path, function(err, output) {
if (err) {
if (!DEBUG) {
for (let $ of F.temporary.files[ctrl.uri.cache])
$.fallback(404);
delete F.temporary.files[ctrl.uri.cache];
F.temporary.notfound[ctrl.uri.cache] = 1;
}
ctrl.fallback(404);
return;
}
if (ctrl.response.minify && F.config.$minifyjs)
output.body = F.TMinificators.js(output.body);
if (DEBUG) {
ctrl.response.headers['cache-control'] = NOCACHE;
ctrl.response.headers['last-modified'] = output.date;
ctrl.response.headers['content-type'] = 'text/javascript';
ctrl.response.value = output.body;
ctrl.flush();
} else {
let filename = F.path.tmp(F.clusterid + ctrl.uri.cache.substring(1).replace(REG_FILETMP, '-') + '-min.js');
F.Fs.writeFile(filename, output.body, function(err) {
if (err) {
F.temporary.notfound[ctrl.uri.cache] = 1;
err = err.toString();
ctrl.fallback(404, err);
for (let $ of F.temporary.files[ctrl.uri.cache])
$.fallback(404, err);
} else {
F.temporary.minified[ctrl.uri.cache] = filename;
send_file(ctrl, filename, 'js');
for (let $ of F.temporary.files[ctrl.uri.cache])
send_file($, filename, 'js');
}
delete F.temporary.files[ctrl.uri.cache];
});
}
});
}
function send_file(ctrl, path, ext) {
// Check the file existence
if (F.temporary.notfound[ctrl.uri.cache]) {
ctrl.fallback(404);
return;
}
let cache = F.temporary.tmp[ctrl.uri.cache];
// HTTP Cache
if (ctrl.response.cache && cache && ctrl.notmodified(cache.date))
return;
var accept = ctrl.headers['accept-encoding'];
var type = F.TUtils.getContentType(ext);
var compress = F.config.$httpcompress && accept && CHECK_COMPRESSION[type] && accept.indexOf('gzip') !== -1;
var range = ctrl.headers.range;
var httpcache = ctrl.response.cache && !CHECK_NOCACHE[ext] && F.config.$httpexpire;
var loadstats = function(err, stats, cache) {
if (err) {
if (!DEBUG && ctrl.response.cache)
F.temporary.notfound[ctrl.uri.cache] = true;
ctrl.fallback(404);
return;
}
if (httpcache) {
if (!ctrl.response.headers.expires)
ctrl.response.headers.expires = F.config.$httpexpire;
if (!ctrl.response.headers['cache-control'])
ctrl.response.headers['cache-control'] = 'public, must-revalidate, max-age=' + F.config.$httpmaxage; // 5 minute cache for revalidate (304)
} else if (ctrl.response.headers.expires)
delete ctrl.response.headers.expires;
if (!httpcache)
ctrl.response.headers['cache-control'] = NOCACHE;
if (!cache)
cache = { date: stats.mtime.toUTCString(), size: stats.size };
ctrl.response.headers['last-modified'] = cache.date;
ctrl.response.headers.etag = '858' + F.config.$httpetag;
var type = F.TUtils.contentTypes[ext] || F.TUtils.contentTypes.bin;
if (CHECK_CHARSET[type])
type += '; charset=utf-8';
ctrl.response.headers['content-type'] = type;
F.temporary.tmp[ctrl.uri.cache] = cache;
F.stats.performance.open++;
var reader;
if (range) {
let size = range.replace(REG_RANGE, '').split('-');
let beg = +size[0] || 0;
let end = +size[1] || 0;
if (end <= 0)
end = beg + (1024 * F.config.$httprangebuffer); // 5 MB
if (beg > end) {
beg = 0;
end = cache.size - 1;
}
if (end > cache.size)
end = cache.size - 1;
ctrl.response.headers['content-length'] = (end - beg) + 1;
ctrl.response.headers['content-range'] = 'bytes ' + beg + '-' + end + '/' + cache.size;
ctrl.res.writeHead(206, ctrl.response.headers);
if (ctrl.method === 'HEAD') {
ctrl.res.end();
} else {
reader = F.Fs.createReadStream(path, { start: beg, end: end });
reader.pipe(ctrl.res);
F.stats.response.streaming++;
}
} else {
reader = F.Fs.createReadStream(path);
if (compress)
ctrl.response.headers['content-encoding'] = 'gzip';
ctrl.res.writeHead(ctrl.response.status, ctrl.response.headers);
if (ctrl.method === 'HEAD') {
ctrl.res.end();
} else {
if (compress)
reader.pipe(F.Zlib.createGzip(GZIP_FILE)).pipe(ctrl.res);
else
reader.pipe(ctrl.res);
F.stats.response.file++;
}
}
};
if (cache) {
loadstats(null, null, cache);
} else {
F.stats.performance.open++;
F.Fs.lstat(path, loadstats);
}
}
function middleware(ctrl) {
var run = function(index) {
let name = ctrl.route.middleware[index];
if (name) {
let fn = F.routes.middleware[name];
if (fn)
fn(ctrl, () => run(index + 1));
else
run(index + 1);
} else
execute(ctrl, true);
};
run(0);
}
function HttpFile(meta) {
var self = this;
self.path = meta.path;
self.name = meta.name;
self.filename = meta.filename;
self.size = meta.size || 0;
self.width = meta.width || 0;
self.height = meta.height || 0;
self.type = meta.type;
self.removable = true;
}
HttpFile.prototype = {
get extension() {
return this.ext;
},
get isImage() {
return this.type.indexOf('image/') !== -1;
},
get isVideo() {
return this.type.indexOf('video/') !== -1;
},
get isAudio() {
return this.type.indexOf('audio/') !== -1;
}
};
HttpFile.prototype.rename = HttpFile.prototype.move = function(filename, callback) {
var self = this;
if (callback)
return self.$move(filename, callback);
else
return new Promise((resolve, reject) => self.$move(filename, err => err ? reject(err) : resolve()));
};
HttpFile.prototype.$move = function(filename, callback) {
var self = this;
F.stats.performance.open++;
F.Fs.rename(self.path, filename, function(err) {
if (err && err.code === 'EXDEV') {
F.stats.performance.open++;
self.copy(filename, function(err){
F.stats.performance.open++;
F.path.unlink(self.path, NOOP);
if (!err) {
self.path = filename;
self.removable = false;
}
callback && callback(err);
});
} else {
if (!err) {
self.path = filename;
self.removable = false;
}
callback && callback(err);
}
});
return self;
};
HttpFile.prototype.copy = function(filename, callback) {
var self = this;
if (callback)
return self.$copy(filename, callback);
else
return new Promise((resolve, reject) => self._copy(filename, err => err ? reject(err) : resolve()));
};
HttpFile.prototype.$copy = function(filename, callback) {
var self = this;
if (!callback) {
F.stats.performance.open++;
F.Fs.createReadStream(self.path).pipe(F.Fs.createWriteStream(filename));
return;
}
F.stats.performance.open++;
var reader = F.Fs.createReadStream(self.path);
var writer = F.Fs.createWriteStream(filename);
reader.on('close', callback);
reader.pipe(writer);
return self;
};
HttpFile.prototype.read = function(callback) {
var self = this;
if (callback)
return self.$read(callback);
else
return new Promise((resolve, reject) => self.$read((err, res) => err ? reject(err) : resolve(res)));
};
HttpFile.prototype.$read = function(callback) {
var self = this;
F.stats.performance.open++;
F.Fs.readFile(self.path, callback);
return self;
};
HttpFile.prototype.md5 = function(callback) {
var self = this;
if (callback)
return self.$md5(callback);
else
return new Promise((resolve, reject) => self.$md5((err, res) => err ? reject(err) : resolve(res)));
};
HttpFile.prototype.$md5 = function(callback) {
var self = this;
var md5 = F.Crypto.createHash('md5');
var stream = F.Fs.createReadStream(self.path);
F.stats.performance.open++;
stream.on('data', buffer => md5.update(buffer));
stream.on('error', function(error) {
if (callback) {
callback(error, null);
callback = null;
}
});
F.cleanup(stream, function() {
if (callback) {
callback(null, md5.digest('hex'));
callback = null;
}
});
return self;
};
HttpFile.prototype.stream = function(opt) {
F.stats.performance.open++;
return F.Fs.createReadStream(this.path, opt);
};
HttpFile.prototype.pipe = function(stream, opt) {
F.stats.performance.open++;
return F.Fs.createReadStream(this.path, opt).pipe(stream, opt);
};
HttpFile.prototype.image = function(shell) {
return F.TImages.load(this.path, shell, this.width, this.height);
};
HttpFile.prototype.fs = function(storage, fileid, callback, custom, expire) {
return F.filestorage(storage).save(fileid, this.filename, this.path, callback, custom, expire);
};
exports.Controller = Controller;