Commit 4b765483 authored by Antoor's avatar Antoor Committed by GitHub

Merge pull request #59 from antoor/v2.0-beta-advanced-configure

v2.0-beta Advanced configure
parents 464c23d6 e87446fc
...@@ -9,12 +9,10 @@ ...@@ -9,12 +9,10 @@
const fs = require('fs'), const fs = require('fs'),
path = require('path'), path = require('path'),
CONF = require('./config'), CONF = require('./config'),
// Logger = require('./logger'),
// logger = null,
// logger = require('log4js').getLogger('Cache'),
Datastore = require('nedb'); Datastore = require('nedb');
let logger var logger;
class Cache { class Cache {
/** /**
......
/** /**
* Shell数据库管理模块 * Shell数据库管理模块
* 更新:2016/04/28 * 更新:2016/06/28
* 作者:蚁逅 <https://github.com/antoor>
*/ */
'use strict'; 'use strict';
...@@ -10,13 +9,10 @@ const fs = require('fs'), ...@@ -10,13 +9,10 @@ const fs = require('fs'),
dns = require('dns'), dns = require('dns'),
path = require('path'), path = require('path'),
CONF = require('./config'), CONF = require('./config'),
// Logger = require('./logger'),
// logger = null,
// logger = require('log4js').getLogger('Database'),
Datastore = require('nedb'), Datastore = require('nedb'),
qqwry = require("geoips").info(); qqwry = require("geoips").info();
let logger; var logger;
class Database { class Database {
...@@ -80,6 +76,36 @@ class Database { ...@@ -80,6 +76,36 @@ class Database {
}); });
} }
/**
* 根据URL解析出IP&&地理位置
* @param {String} url URL地址
* @return {Promise} ip, addr
*/
_url2ip(url) {
return new Promise((res, rej) => {
// 解析domain
const urlArr = url.match(/(\w+):\/\/([\w\.\-]+)[:]?([\d]*)([\s\S]*)/i);
// 无效url
if (!urlArr || urlArr.length < 3) {
return rej('Unable to resolve domain name from URL');
}
// 获取IP
const domain = urlArr[2];
dns.lookup(domain, (err, ip) => {
if (err) {
return rej(err.toString());
}
// 获取地理位置
const _addr = qqwry.searchIP(ip);
return res({
ip: ip,
addr: `${_addr.Country} ${_addr.Area}`
});
})
})
}
/** /**
* 添加shell数据 * 添加shell数据
* @param {Object} event ipcMain对象 * @param {Object} event ipcMain对象
...@@ -87,68 +113,67 @@ class Database { ...@@ -87,68 +113,67 @@ class Database {
*/ */
addShell(event, opts) { addShell(event, opts) {
logger.info('addShell', opts); logger.info('addShell', opts);
// 获取目标IP以及地理位置
// 1. 获取域名 this._url2ip(opts.base['url'])
let parse = opts['url'].match(/(\w+):\/\/([\w\.\-]+)[:]?([\d]*)([\s\S]*)/i); .then((ret) => {
if (!parse || parse.length < 3) { return event.returnValue = 'Unable to resolve domain name from URL' }; this.cursor.insert({
// 2. 获取域名IP category: opts.base['category'] || 'default',
dns.lookup(parse[2], (err, ip) => { url: opts.base['url'],
if (err) { return event.returnValue = err.toString() }; pwd: opts.base['pwd'],
// 3. 查询IP对应物理位置 type: opts.base['type'],
const addr = qqwry.searchIP(ip); ip: ret['ip'],
// 插入数据库 addr: ret['addr'],
this.cursor.insert({ encode: opts.base['encode'],
category: opts['category'] || 'default', encoder: opts.base['encoder'],
url: opts['url'], httpConf: opts.http,
pwd: opts['pwd'], otherConf: opts.other,
type: opts['type'], ctime: +new Date,
ip: ip, utime: +new Date
addr: `${addr.Country} ${addr.Area}`, }, (_err, _ret) => {
encode: opts['encode'], event.returnValue = _err || _ret;
encoder: opts['encoder'], });
ctime: +new Date, })
utime: +new Date .catch((_err) => {
}, (err, ret) => { event.returnValue = _err;
event.returnValue = err || ret; })
});
});
} }
/** /**
* 编辑shell数据 * 编辑shell数据
* @param {Object} event ipcMain对象 * @param {Object} event ipcMain对象
* @param {Object} opts 数据(url,_id,pwd,type,encode,encoder * @param {Object} opts 数据(old,new
* @return {[type]} [description] * @return {[type]} [description]
*/ */
editShell(event, opts) { editShell(event, opts) {
logger.warn('editShell', opts); logger.warn('editShell', opts);
// 获取目标IP以及地理位置
// 1. 获取域名 const _new = opts.new;
let parse = opts['url'].match(/(\w+):\/\/([\w\.\-]+)[:]?([\d]*)([\s\S]*)/i); const _old = opts.old;
if (!parse || parse.length < 3) { return event.returnValue = 'Unable to resolve domain name from URL' };
// 2. 获取域名IP this._url2ip(_new.base['url'])
dns.lookup(parse[2], (err, ip) => { .then((ret) => {
if (err) { return event.returnValue = err.toString() }; this.cursor.update({
// 3. 查询IP对应物理位置 _id: _old['_id']
const addr = qqwry.searchIP(ip); }, {
// 更新数据库 $set: {
this.cursor.update({ ip: ret['ip'],
_id: opts['_id'] addr: ret['addr'],
}, { url: _new.base['url'],
$set: { pwd: _new.base['pwd'],
ip: ip, type: _new.base['type'],
addr: `${addr.Country} ${addr.Area}`, encode: _new.base['encode'],
url: opts['url'], encoder: _new.base['encoder'],
pwd: opts['pwd'], httpConf: _new.http,
type: opts['type'], otherConf: _new.other,
encode: opts['encode'], utime: +new Date
encoder: opts['encoder'], }
utime: +new Date }, (_err, _ret) => {
} event.returnValue = _err || _ret;
}, (err, num) => { })
event.returnValue = err || num;
}) })
}); .catch((_err) => {
event.returnValue = _err;
});
} }
/** /**
......
...@@ -68,33 +68,6 @@ class Menubar { ...@@ -68,33 +68,6 @@ class Menubar {
click: this.app.quit.bind(this.app) click: this.app.quit.bind(this.app)
}, },
] ]
}, {
// 数据管理
label: LANG['shell']['title'],
submenu: [
{
label: LANG['shell']['add'],
accelerator: 'Shift+A',
click: event.sender.send.bind(event.sender, 'menubar', 'shell-add')
}, {
label: LANG['shell']['search'],
accelerator: 'Shift+S',
enabled: false
}, {
type: 'separator'
}, {
label: LANG['shell']['import'],
enabled: false
}, {
label: LANG['shell']['dump'],
enabled: false
}, {
type: 'separator'
}, {
label: LANG['shell']['clear'],
enabled: false
}
]
}, { }, {
// 编辑 // 编辑
label: LANG['edit']['title'], label: LANG['edit']['title'],
......
...@@ -19,14 +19,6 @@ module.exports = { ...@@ -19,14 +19,6 @@ module.exports = {
update: 'Check update', update: 'Check update',
quit: 'Quit' quit: 'Quit'
}, },
shell: {
title: 'Data',
add: 'Add data',
search: 'Search data',
dump: 'Dump data',
import: 'Import data',
clear: 'Clear all data'
},
edit: { edit: {
title: 'Edit', title: 'Edit',
undo: 'Undo', undo: 'Undo',
...@@ -154,6 +146,15 @@ module.exports = { ...@@ -154,6 +146,15 @@ module.exports = {
confirm: 'Are you sure to clear all the cache?', confirm: 'Are you sure to clear all the cache?',
success: 'Clear all cache success!', success: 'Clear all cache success!',
error: (err) => antSword.noxss(`Clear all cache failed!\n${err}`) error: (err) => antSword.noxss(`Clear all cache failed!\n${err}`)
},
accordion: {
base: 'Base',
http: 'HTTP',
other: 'Other'
},
otherConf: {
nohttps: 'Ignore HTTPS certificate',
notermcache: "Don't use the terminal's cache"
} }
} }
}, },
......
...@@ -20,14 +20,6 @@ module.exports = { ...@@ -20,14 +20,6 @@ module.exports = {
update: '检查更新', update: '检查更新',
quit: '退出程序' quit: '退出程序'
}, },
shell: {
title: '数据',
add: '添加数据',
search: '搜索数据',
dump: '导出数据',
import: '导入数据',
clear: '清空数据'
},
edit: { edit: {
title: '编辑', title: '编辑',
undo: '撤销', undo: '撤销',
...@@ -155,6 +147,15 @@ module.exports = { ...@@ -155,6 +147,15 @@ module.exports = {
confirm: '确定清空所有缓存数据吗?', confirm: '确定清空所有缓存数据吗?',
success: '清空全部缓存完毕!', success: '清空全部缓存完毕!',
error: (err) => antSword.noxss(`清空全部缓存失败!\n${err}`) error: (err) => antSword.noxss(`清空全部缓存失败!\n${err}`)
},
accordion: {
base: '基础配置',
http: '请求信息',
other: '其他设置'
},
otherConf: {
nohttps: '忽略HTTPS证书',
notermcache: "虚拟终端不使用缓存"
} }
} }
}, },
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
* 右键菜单 * 右键菜单
*/ */
const DATA = require('../data'); const Data = require('../data');
const Form = require('./form');
const Terminal = require('../../terminal/'); const Terminal = require('../../terminal/');
const Database = require('../../database/'); const Database = require('../../database/');
const FileManager = require('../../filemanager/'); const FileManager = require('../../filemanager/');
...@@ -41,13 +42,13 @@ class ContextMenu { ...@@ -41,13 +42,13 @@ class ContextMenu {
], ],
false, false,
['add', 'plus-circle', false, this.addData.bind(this)], ['add', 'plus-circle', false, this.addData.bind(this)],
['edit', 'edit', selectedData, this.editData.bind(this, id)], ['edit', 'edit', selectedData, this.editData.bind(this, data[0])],
['delete', 'remove', selectedMultiData, this.delData.bind(this, ids)], ['delete', 'remove', selectedMultiData, this.delData.bind(this, ids)],
false, false,
['move', 'share-square', selectedMultiData, null, this.parseMoveCategoryMenu(ids)], ['move', 'share-square', selectedMultiData, null, this.parseMoveCategoryMenu(ids)],
['search', 'search', true], ['search', 'search', true],
false, false,
['clearCache', 'trash-o', selectedMultiData, this.clearCache.bind(this, ids)], ['clearCache', 'trash-o', selectedData, this.clearCache.bind(this, id)],
['clearAllCache', 'trash', false, this.clearAllCache.bind(this)] ['clearAllCache', 'trash', false, this.clearAllCache.bind(this)]
].map((menu) => { ].map((menu) => {
// 分隔符号 // 分隔符号
...@@ -182,16 +183,57 @@ class ContextMenu { ...@@ -182,16 +183,57 @@ class ContextMenu {
* 添加数据 * 添加数据
*/ */
addData() { addData() {
new Form({
title: LANG['list']['add']['title'],
icon: 'plus-circle',
text: LANG['list']['add']['toolbar']['add']
}, {}, (data) => {
return new Promise((res, rej) => {
// 获取当前分类
data['base']['category'] = antSword.modules.shellmanager.category.sidebar.getActiveItem();
// 通知后台插入数据
const ret = antSword.ipcRenderer.sendSync('shell-add', data);
if (ret instanceof Object) {
// 重新加载数据
antSword.modules.shellmanager.reloadData({
category: data['base']['category']
});
return res(LANG['list']['add']['success']);
} else {
return rej(LANG['list']['add']['error'](ret.toString()));
}
});
})
} }
/** /**
* 编辑数据 * 编辑数据
* @param {number} id [description] * @param {Object} info 当前选中的数据
* @return {[type]} [description] * @return {[type]} [description]
*/ */
editData(id) { editData(info) {
new Form({
title: LANG['list']['edit']['title'](info.url),
icon: 'save',
text: LANG['list']['edit']['toolbar']['save']
}, info, (data) => {
return new Promise((res, rej) => {
// 通知后台更新数据
const ret = antSword.ipcRenderer.sendSync('shell-edit', {
old: info,
new: data
});
if (ret === 1) {
// 重新加载数据
antSword.modules.shellmanager.reloadData({
category: info['category']
});
return res(LANG['list']['edit']['success']);
} else {
return rej(LANG['list']['edit']['error'](ret.toString()));
}
})
})
} }
/** /**
...@@ -200,7 +242,23 @@ class ContextMenu { ...@@ -200,7 +242,23 @@ class ContextMenu {
* @return {[type]} [description] * @return {[type]} [description]
*/ */
delData(ids) { delData(ids) {
layer.confirm(
LANG['list']['del']['confirm'](ids.length), {
icon: 2, shift: 6,
title: `<i class="fa fa-trash"></i> ${LANG['list']['del']['title']}`
}, (_) => {
layer.close(_);
const ret = antSword['ipcRenderer'].sendSync('shell-del', ids);
if (typeof(ret) === 'number') {
toastr.success(LANG['list']['del']['success'](ret), LANG_T['success']);
// 更新UI
antSword.modules.shellmanager.reloadData({
category: antSword.modules.shellmanager.category.sidebar.getActiveItem()
});
}else{
toastr.error(LANG['list']['del']['error'](ret.toString()), LANG_T['error']);
}
});
} }
/** /**
...@@ -213,11 +271,29 @@ class ContextMenu { ...@@ -213,11 +271,29 @@ class ContextMenu {
/** /**
* 清空缓存 * 清空缓存
* @param {array} ids [description] * @param {number} id ID
* @return {[type]} [description] * @return {[type]} [description]
*/ */
clearCache(ids) { clearCache(id) {
layer.confirm(
LANG['list']['clearCache']['confirm'], {
icon: 2, shift: 6,
title: `<i class="fa fa-trash"></i> ${LANG['list']['clearCache']['title']}`
}, (_) => {
layer.close(_);
const ret = antSword['ipcRenderer'].sendSync('cache-clear', {
id: id
});
if (ret === true) {
toastr.success(LANG['list']['clearCache']['success'], LANG_T['success']);
}else{
toastr.error(
LANG['list']['clearCache']['error'](
ret['errno'] === -2 ? 'Not cache file.' : ret['errno']
), LANG_T['error']
);
}
});
} }
/** /**
...@@ -225,7 +301,19 @@ class ContextMenu { ...@@ -225,7 +301,19 @@ class ContextMenu {
* @return {[type]} [description] * @return {[type]} [description]
*/ */
clearAllCache() { clearAllCache() {
layer.confirm(
LANG['list']['clearAllCache']['confirm'], {
icon: 2, shift: 6,
title: `<i class="fa fa-trash"></i> ${LANG['list']['clearAllCache']['title']}`
}, (_) => {
layer.close(_);
const ret = antSword['ipcRenderer'].sendSync('cache-clearAll');
if (ret === true) {
toastr.success(LANG['list']['clearAllCache']['success'], LANG_T['success']);
}else{
toastr.error(LANG['list']['clearAllCache']['error'](ret), LANG_T['error']);
}
});
} }
} }
......
/**
* 添加/编辑数据表单
*/
const LANG_T = antSword['language']['toastr'];
const LANG = antSword['language']['shellmanager'];
const ENCODES = require('../../../base/encodes');
class Form {
/**
* 初始化函数
* @param {object} opt ui配置
* @param {object} arg = {} 默认数据
* @param {function} callback 点击按钮后回调数据
*/
constructor(opt, arg = {}, callback = false) {
// 创建win窗口
const win = this._createWin(opt);
// 创建toolbar工具栏
this.toolbar = this._createToolbar(win, opt);
// 创建表单分隔accordion
this.accordion = this._createAccordion(win);
// 创建表单
this.baseForm = this._createBaseForm(arg);
this.httpForm = this._createHttpForm(arg);
this.otherForm = this._createOtherForm(arg);
// toolbar点击事件
this.toolbar.attachEvent('onClick', (id) => {
if (id === 'clear') {
return this.baseForm.clear();
}
// 检测表单数据
if (
!this.baseForm.validate() ||
!this.httpForm.validate() ||
!this.otherForm.validate()
) {
return toastr.warning(LANG['list']['add']['warning'], LANG_T['warning']);
};
// 回调数据
if (callback) {
win.progressOn();
callback(this._parseFormData(
this.baseForm.getValues(),
this.httpForm.getValues(),
this.otherForm.getValues()
)).then((msg) => {
// 添加/保存完毕后回调
win.close();
toastr.success(msg, LANG_T['success']);
}).catch((msg) => {
// 添加/保存错误
win.progressOff();
toastr.error(msg, LANG_T['error']);
});
};
});
}
/**
* 创建win窗口
* @param {object} opts = {} 窗口属性(title,width,height)
* @return {object} win
*/
_createWin(opts = {}) {
let _id = String(Math.random()).substr(5, 10);
// 默认配置
let opt = Object.assign({
title: opts['title'] || 'Window:' + _id,
width: 550, height: 450
}, opts);
// 创建窗口
let win = antSword.modules.shellmanager.list.win;
if (!win) {
win = new dhtmlXWindows();
win.attachViewportTo(antSword.modules.shellmanager.list.cell.cell);
antSword.modules.shellmanager.list.win = win;
}
let _win = win.createWindow(_id, 0, 0, opt['width'], opt['height']);
_win.setText(opt['title']);
_win.centerOnScreen();
_win.button('minmax').show();
_win.button('minmax').enable();
return _win;
}
/**
* 创建工具栏
* @param {object} win [description]
* @param {object} opt ui配置
* @return {[type]} [description]
*/
_createToolbar(win, opt) {
const toolbar = win.attachToolbar();
toolbar.loadStruct([{
id: 'act',
type: 'button',
icon: opt['icon'],
text: opt['text']
}, {
type: 'separator'
}, {
id: 'clear',
type: 'button',
icon: 'remove',
text: LANG['list']['add']['toolbar']['clear']
}]);
return toolbar;
}
/**
* 创建Accordion
* @param {[type]} win [description]
* @return {[type]} [description]
*/
_createAccordion(win) {
const accordion = win.attachAccordion({
items: [{
id: 'base',
text: `<i class="fa fa-file-text"></i> ${LANG['list']['accordion']['base']}`
}, {
id: 'http',
text: `<i class="fa fa-edge"></i> ${LANG['list']['accordion']['http']}`
}, {
id: 'other',
text: `<i class="fa fa-cogs"></i> ${LANG['list']['accordion']['other']}`
}]});
return accordion;
}
/**
* 创建基础表单
* @param {object} arg 默认表单数据
* @return {[type]} [description]
*/
_createBaseForm(arg) {
const opt = Object.assign({}, {
url: '',
pwd: '',
type: 'php',
encode: 'utf8',
encoder: 'default'
}, arg);
const form = this.accordion.cells('base').attachForm([
{ type: 'settings', position: 'label-left', labelWidth: 80, inputWidth: 400 },
{ type: 'block', inputWidth: 'auto', offsetTop: 12, list: [
{
type: 'input', label: LANG['list']['add']['form']['url'],
name: 'url', required: true, value: opt.url
}, {
type: 'input', label: LANG['list']['add']['form']['pwd'],
name: 'pwd', required: true, value: opt.pwd
}, {
type: 'combo', label: LANG['list']['add']['form']['encode'],
readonly: true, name: 'encode', options: this._parseEncodes(opt.encode)
}, {
type: 'combo', label: LANG['list']['add']['form']['type'],
name: 'type', readonly: true, options: this._parseTypes(opt.type, opt.encoder)
}
] }
], true);
return form;
}
/**
* 解析编码列表
* @param {String} _default 默认编码器
* @return {array} [description]
*/
_parseEncodes(_default = 'utf8') {
let ret = [];
ENCODES.map((_) => {
ret.push({
text: _, value: _,
selected: _ === _default.toUpperCase()
});
});
return ret;
}
/**
* 解析脚本支持列表
* @param {String} _default 默认类型
* @param {String} _encoder 默认编码器
* @return {array} [description]
*/
_parseTypes(_default = 'php', _encoder = 'default') {
let ret = [];
for (let c in antSword['core']) {
let encoders = antSword['core'][c].prototype.encoders;
ret.push({
text: c.toUpperCase(), value: c,
selected: c === _default,
list: ((c) => {
let _ = [
{ type: 'settings', position: 'label-right', offsetLeft: 60, labelWidth: 100 },
{ type: 'label', label: LANG['list']['add']['form']['encoder'] },
{ type: 'radio', name: `encoder_${c}`, value: 'default', label: 'default', checked: true }
];
encoders.map((e) => {
_.push({
type: 'radio', name: `encoder_${c}`,
value: e, label: e, checked: e === _encoder
})
});
return _;
})(c)
});
}
return ret;
}
/**
* 解析表单数据
* @param {object} base 原始base数据
* @param {object} http 原始http数据
* @param {object} other 原始other数据
* @return {object} {base,http,other}
*/
_parseFormData(base, http, other) {
// 提取需要的base数据
let _baseData = {
url: base['url'],
pwd: base['pwd'],
type: base['type'],
encode: base['encode'],
encoder: base[`encoder_${base['type']}`]
};
// 提取需要的http数据
let [headers, bodys] = [{}, {}];
for (let _ in http) {
if (_.endsWith('value') || !http[_]) {
continue
}
let _tmp = _.split('-');
if (_tmp[0] === 'header') {
headers[ http[_] ] = http[_.replace(/name$/, 'value')];
} else {
bodys[ http[_] ] = http[_.replace(/name$/, 'value')];
}
}
// 返回处理完毕的数据
return {
base: _baseData,
http: {
body: bodys,
headers: headers
},
other: other
};
}
/**
* 创建其他设置表单
* @param {object} arg 默认配置
* @return {[type]} [description]
*/
_createOtherForm(arg) {
const opt = Object.assign({}, {
'ignore-https': 0,
'terminal-cache': 1
}, arg.otherConf);
const form = this.accordion.cells('other').attachForm([{
type: 'settings', position: 'label-right', inputWidth: 400
}, {
type: 'block', inputWidth: 'auto', offsetTop: 12, list: [
{
type: "checkbox", name: 'ignore-https', label: LANG['list']['otherConf']['nohttps'],
checked: opt['ignore-https'] === 1
}, {
type: "checkbox", name: 'terminal-cache', label: LANG['list']['otherConf']['notermcache'],
checked: opt['terminal-cache'] === 1
}
]}], true);
return form;
}
/**
* 创建HTTP请求表单
* @param {object} arg [description]
* @return {[type]} [description]
*/
_createHttpForm(arg) {
const opt = Object.assign({}, {
headers: {},
body: {}
}, arg.httpConf);
const cell = this.accordion.cells('http');
// 创建toolbar,用于添加数据
const toolbar = cell.attachToolbar();
toolbar.loadStruct([{
id: 'add-header',
type: 'button',
icon: 'plus-square-o',
text: 'Header'
}, {
type: 'separator'
}, {
id: 'add-body',
type: 'button',
icon: 'plus-square-o',
text: 'Body'
}]);
// 创建表单
const form = cell.attachForm([{
type: 'block', inputWidth: 'auto', offsetTop: 12, name: 'header', list: [
{type: "label", label: "HTTP HEADERS"}
]
}, {
type: 'block', inputWidth: 'auto', offsetTop: 12, name: 'body', list: [
{type: "label", label: "HTTP BODY"}
]
}], true);
// 添加Header
let _headerCount = 0;
const _addHeader = (name = '', value = '') => {
_headerCount ++;
form.addItem(
'header',
{
type: "fieldset", label: `#${_headerCount}`, inputWidth: 480, list:[
{type: "input", name: `header-${_headerCount}_name`, inputWidth: 350, labelWidth: 50, label: "Name", value: name},
{type: "input", name: `header-${_headerCount}_value`, inputWidth: 350, labelWidth: 50, label: "Value", value: value}
]
}
)
}
// 添加Body
let _bodyCount = 0;
const _addBody = (name = '', value = '') => {
_bodyCount ++;
form.addItem(
'body',
{
type: "fieldset", label: `#${_bodyCount}`, inputWidth: 480, list:[
{type: "input", name: `body-${_bodyCount}_name`, inputWidth: 350, labelWidth: 50, label: "Name", value: name},
{type: "input", name: `body-${_bodyCount}_value`, inputWidth: 350, labelWidth: 50, label: "Value", value: value}
]
}
)
}
// 监听toolbar事件
toolbar.attachEvent('onClick', (id, e) => {
switch (id) {
case 'add-header':
_addHeader();
break;
case 'add-body':
_addBody();
break;
}
});
// 添加存储的配置
for (let _ in opt.headers) {
_addHeader(_, opt.headers[_]);
}
for (let _ in opt.body) {
_addBody(_, opt.body[_]);
}
// 如果没有配置,则添加空白的输入框
if (_headerCount === 0) {
_addHeader();
}
if (_bodyCount === 0) {
_addBody();
}
return form;
}
}
module.exports = Form;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment