Unverified Commit 4539abcd authored by Medicean's avatar Medicean Committed by GitHub

Release v2.0.3 🎄 Merry Christmas 🎄

Merge pull request #112 from AntSwordProject/v2.0.x
parents a94c3c48 cfa61fab
...@@ -2,6 +2,32 @@ ...@@ -2,6 +2,32 @@
> 有空会补补BUG、添添新功能。 > 有空会补补BUG、添添新功能。
> 同时也欢迎大家的参与!感谢各位朋友的支持! .TAT. > 同时也欢迎大家的参与!感谢各位朋友的支持! .TAT.
## 2018/12/25 `(v2.0.3)`
### 模块增强
#### Shell 管理
* 添加 Shell 时 URL 默认前缀 `http://`
* 添加 Shell 时根据文件后缀选择 Shell 类型并赋予默认编码(asp 默认 GBK, 其它默认 UTF8) #109
* 其它配置新增 `Multipart 发包` 功能
#### 后端模块
* 数据存储新增插件配置存储管理功能 (`shell-addPluginDataConf`, `shell-editPluginDataConf`, `shell-delPluginDataConf`, `shell-getPluginDataConf`)
* 后台发包方式支持 `Multipart`, 可在「编辑Shell配置」-「其它配置」里选择是否开启此功能,默认关闭。(thx @phith0n)
### Bug Fix
* 修复数据库编码无法保存的 Bug (#110 thx @Twi1ight)
* 修复 PHP Mysql(i) 数据管理模版代码中编码设置部分的错误 (#110 thx @Twi1ight)
### Other
* 自动检查更新每24小时触发一次(GitHub 访问频率限制)
* 插件市场默认窗口大小调整
* Loading 界面增加了圣诞节彩蛋, 偶尔跟风过个节 🎄 Merry Christmas 🎄
## 2018/12/05 `(v2.0.2)` ## 2018/12/05 `(v2.0.2)`
### 模块增强 ### 模块增强
......
...@@ -41,7 +41,11 @@ class Database { ...@@ -41,7 +41,11 @@ class Database {
.on('shell-delDataConf', this.delDataConf.bind(this)) .on('shell-delDataConf', this.delDataConf.bind(this))
.on('shell-getDataConf', this.getDataConf.bind(this)) .on('shell-getDataConf', this.getDataConf.bind(this))
.on('shell-renameCategory', this.renameShellCategory.bind(this)) .on('shell-renameCategory', this.renameShellCategory.bind(this))
.on('shell-updateHttpConf', this.updateHttpConf.bind(this)); .on('shell-updateHttpConf', this.updateHttpConf.bind(this))
.on('shell-addPluginDataConf', this.addPluginDataConf.bind(this))
.on('shell-editPluginDataConf', this.editPluginDataConf.bind(this))
.on('shell-delPluginDataConf', this.delPluginDataConf.bind(this))
.on('shell-getPluginDataConf', this.getPluginDataConf.bind(this));
} }
/** /**
...@@ -389,6 +393,120 @@ class Database { ...@@ -389,6 +393,120 @@ class Database {
event.returnValue = err || confs[opts['id']]; event.returnValue = err || confs[opts['id']];
}); });
} }
/**
* 添加插件数据配置
* @param {Object} event ipcMain对象
* @param {string} plugin 插件注册的名称
* @param {Object} opts 配置(_id,data
*/
addPluginDataConf(event, plugin, opts) {
logger.info('addPluginDataConf', plugin, opts);
// 1. 获取原配置列表
this.cursor.findOne({
_id: opts['_id']
}, (err, ret) => {
ret['plugins'] = ret['plugins'] || {};
let confs = ret['plugins'][plugin] || {};
// 随机Id(顺序增长
const random_id = parseInt(+new Date + Math.random() * 1000).toString(16);
// 添加到配置
confs[random_id] = opts['data'];
let setdata = {
utime: +new Date,
}
setdata[`plugins.${plugin}`] = confs;
// 更新
this.cursor.update({
_id: opts['_id']
}, {
$set: setdata
}, (_err, _ret) => {
event.returnValue = random_id;
});
});
}
/**
* 修改插件数据配置
* @param {Object} event ipcMain对象
* @param {string} plugin 插件注册的名称
* @param {Object} opts 配置(_id,id,data
*/
editPluginDataConf(event, plugin, opts) {
logger.info('editPluginDataConf', plugin, opts);
// 1. 获取原配置列表
this.cursor.findOne({
_id: opts['_id']
}, (err, ret) => {
ret['plugins'] = ret['plugins'] || {};
let confs = ret['plugins'][plugin] || {};
// 添加到配置
confs[opts['id']] = opts['data'];
let setdata = {
utime: +new Date,
}
setdata[`plugins.${plugin}`] = confs;
// 更新数据库
this.cursor.update({
_id: opts['_id']
}, {
$set: setdata
}, (_err, _ret) => {
event.returnValue = opts['id'];
});
});
}
/**
* 删除插件数据配置
* @param {Object} event ipcMain对象
* @param {string} plugin 插件注册的名称
* @param {Object} opts 配置(_id,id
* @return {[type]} [description]
*/
delPluginDataConf(event, plugin, opts) {
logger.info('delPluginDataConf', plugin, opts);
// 1. 获取原配置
this.cursor.findOne({
_id: opts['_id']
}, (err, ret) => {
ret['plugins'] = ret['plugins'] || {};
let confs = ret['plugins'][plugin] || {};
// 2. 删除配置
delete confs[opts['id']];
let setdata = {
utime: +new Date,
}
setdata[`plugins.${plugin}`] = confs;
// 3. 更新数据库
this.cursor.update({
_id: opts['_id']
}, {
$set: setdata
}, (_err, _ret) => {
event.returnValue = _err || _ret;
});
})
}
/**
* 获取单个插件数据配置
* @param {Object} event ipcMain对象
* @param {string} plugin 插件注册的名称
* @param {Object} opts 配置(_id,id
* @return {[type]} [description]
*/
getPluginDataConf(event, plugin, opts) {
logger.info('getPluginDatConf', plugin, opts);
this.cursor.findOne({
_id: opts['_id']
}, (err, ret) => {
ret['plugins'] = ret['plugins'] || {};
const confs = ret['plugins'][plugin] || {};
event.returnValue = err || confs[opts['id']];
});
}
} }
module.exports = Database; module.exports = Database;
...@@ -99,7 +99,7 @@ class Request { ...@@ -99,7 +99,7 @@ class Request {
if(opts['url'].match(CONF.urlblacklist)) { if(opts['url'].match(CONF.urlblacklist)) {
return event.sender.send('request-error-' + opts['hash'], "Blacklist URL"); return event.sender.send('request-error-' + opts['hash'], "Blacklist URL");
} }
const _request = superagent.post(opts['url']); let _request = superagent.post(opts['url']);
// 设置headers // 设置headers
_request.set('User-Agent', USER_AGENT); _request.set('User-Agent', USER_AGENT);
// 自定义headers // 自定义headers
...@@ -108,6 +108,13 @@ class Request { ...@@ -108,6 +108,13 @@ class Request {
} }
// 自定义body // 自定义body
const _postData = Object.assign({}, opts.body, opts.data); const _postData = Object.assign({}, opts.body, opts.data);
// 通过替换函数方式来实现发包方式切换, 后续可改成别的
const old_send = _request.send;
if(opts['useMultipart'] == 1) {
_request.send = _request.field;
}else{
_request.send = old_send;
}
_request _request
.proxy(APROXY_CONF['uri']) .proxy(APROXY_CONF['uri'])
.type('form') .type('form')
...@@ -158,7 +165,7 @@ class Request { ...@@ -158,7 +165,7 @@ class Request {
let indexEnd = -1; let indexEnd = -1;
let tempData = []; let tempData = [];
const _request = superagent.post(opts['url']); let _request = superagent.post(opts['url']);
// 设置headers // 设置headers
_request.set('User-Agent', USER_AGENT); _request.set('User-Agent', USER_AGENT);
// 自定义headers // 自定义headers
...@@ -167,6 +174,13 @@ class Request { ...@@ -167,6 +174,13 @@ class Request {
} }
// 自定义body // 自定义body
const _postData = Object.assign({}, opts.body, opts.data); const _postData = Object.assign({}, opts.body, opts.data);
// 通过替换函数方式来实现发包方式切换, 后续可改成别的
const old_send = _request.send;
if(opts['useMultipart'] == 1) {
_request.send = _request.field;
}else{
_request.send = old_send;
}
_request _request
.proxy(APROXY_CONF['uri']) .proxy(APROXY_CONF['uri'])
.type('form') .type('form')
......
{ {
"name": "antsword", "name": "antsword",
"version": "2.0.0", "version": "2.0.3",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
......
{ {
"name": "antsword", "name": "antsword",
"version": "2.0.2", "version": "2.0.3",
"description": "中国蚁剑是一款跨平台的开源网站管理工具", "description": "中国蚁剑是一款跨平台的开源网站管理工具",
"main": "app.js", "main": "app.js",
"dependencies": { "dependencies": {
......
...@@ -385,8 +385,12 @@ antSword.reloadPlug(); ...@@ -385,8 +385,12 @@ antSword.reloadPlug();
antSword['menubar'].reg('check-update', ()=>{ antSword['menubar'].reg('check-update', ()=>{
antSword.ipcRenderer.send('check-update'); antSword.ipcRenderer.send('check-update');
}); });
// 检查更新
setTimeout( if(new Date() - new Date(antSword['storage']('lastautocheck', false, "0")) >= 86400000) {
antSword.ipcRenderer.send.bind(antSword.ipcRenderer, 'check-update'), // 检查更新
1000 * 60 antSword['storage']('lastautocheck', new Date().getTime());
); setTimeout(
antSword.ipcRenderer.send.bind(antSword.ipcRenderer, 'check-update'),
1000 * 60
);
}
\ No newline at end of file
...@@ -244,6 +244,7 @@ class Base { ...@@ -244,6 +244,7 @@ class Base {
tag_e: opt['tag_e'], tag_e: opt['tag_e'],
encode: this.__opts__['encode'], encode: this.__opts__['encode'],
ignoreHTTPS: (this.__opts__['otherConf'] || {})['ignore-https'] === 1, ignoreHTTPS: (this.__opts__['otherConf'] || {})['ignore-https'] === 1,
useMultipart: (this.__opts__['otherConf'] || {})['use-multipart'] === 1,
timeout: parseInt((this.__opts__['otherConf'] || {})['request-timeout']), timeout: parseInt((this.__opts__['otherConf'] || {})['request-timeout']),
headers: (this.__opts__['httpConf'] || {})['headers'] || {}, headers: (this.__opts__['httpConf'] || {})['headers'] || {},
body: (this.__opts__['httpConf'] || {})['body'] || {} body: (this.__opts__['httpConf'] || {})['body'] || {}
...@@ -285,6 +286,8 @@ class Base { ...@@ -285,6 +286,8 @@ class Base {
data: opt['data'], data: opt['data'],
tag_s: opt['tag_s'], tag_s: opt['tag_s'],
tag_e: opt['tag_e'], tag_e: opt['tag_e'],
ignoreHTTPS: (this.__opts__['otherConf'] || {})['ignore-https'] === 1,
useMultipart: (this.__opts__['otherConf'] || {})['use-multipart'] === 1,
encode: this.__opts__['encode'] encode: this.__opts__['encode']
}); });
}) })
......
...@@ -34,7 +34,7 @@ module.exports = (arg1, arg2, arg3, arg4, arg5, arg6) => ({ ...@@ -34,7 +34,7 @@ module.exports = (arg1, arg2, arg3, arg4, arg5, arg6) => ({
// 执行SQL语句 // 执行SQL语句
query: { query: {
_: _:
`$m=get_magic_quotes_gpc();$hst=$m?stripslashes($_POST["${arg1}"]):$_POST["${arg1}"];$usr=$m?stripslashes($_POST["${arg2}"]):$_POST["${arg2}"];$pwd=$m?stripslashes($_POST["${arg3}"]):$_POST["${arg3}"];$dbn=$m?stripslashes($_POST["${arg4}"]):$_POST["${arg4}"];$sql=base64_decode($_POST["${arg5}"]);$T=@mysql_connect($hst,$usr,$pwd);@mysql_query("SET NAMES ${arg6}");@mysql_select_db($dbn, $T);$q=@mysql_query($sql);if(is_bool($q)){echo("Status\t|\t\r\n".($q?"VHJ1ZQ==":"RmFsc2U=")."\t|\t\r\n");}else{$i=0;while($col=@mysql_fetch_field($q)){echo($col->name."\t|\t");$i++;}echo("\r\n");while($rs=@mysql_fetch_row($q)){for($c=0;$c<$i;$c++){echo(base64_encode(trim($rs[$c])));echo("\t|\t");}echo("\r\n");}}@mysql_close($T);`, `$m=get_magic_quotes_gpc();$hst=$m?stripslashes($_POST["${arg1}"]):$_POST["${arg1}"];$usr=$m?stripslashes($_POST["${arg2}"]):$_POST["${arg2}"];$pwd=$m?stripslashes($_POST["${arg3}"]):$_POST["${arg3}"];$dbn=$m?stripslashes($_POST["${arg4}"]):$_POST["${arg4}"];$sql=base64_decode($_POST["${arg5}"]);$T=@mysql_connect($hst,$usr,$pwd);@mysql_query("SET NAMES $_POST[${arg6}]");@mysql_select_db($dbn, $T);$q=@mysql_query($sql);if(is_bool($q)){echo("Status\t|\t\r\n".($q?"VHJ1ZQ==":"RmFsc2U=")."\t|\t\r\n");}else{$i=0;while($col=@mysql_fetch_field($q)){echo($col->name."\t|\t");$i++;}echo("\r\n");while($rs=@mysql_fetch_row($q)){for($c=0;$c<$i;$c++){echo(base64_encode(trim($rs[$c])));echo("\t|\t");}echo("\r\n");}}@mysql_close($T);`,
[arg1]: '#{host}', [arg1]: '#{host}',
[arg2]: '#{user}', [arg2]: '#{user}',
[arg3]: '#{passwd}', [arg3]: '#{passwd}',
......
...@@ -34,7 +34,7 @@ module.exports = (arg1, arg2, arg3, arg4, arg5, arg6) => ({ ...@@ -34,7 +34,7 @@ module.exports = (arg1, arg2, arg3, arg4, arg5, arg6) => ({
// 执行SQL语句 // 执行SQL语句
query: { query: {
_: _:
`$m=get_magic_quotes_gpc();$hst=$m?stripslashes($_POST["${arg1}"]):$_POST["${arg1}"];$usr=$m?stripslashes($_POST["${arg2}"]):$_POST["${arg2}"];$pwd=$m?stripslashes($_POST["${arg3}"]):$_POST["${arg3}"];$dbn=$m?stripslashes($_POST["${arg4}"]):$_POST["${arg4}"];$sql=base64_decode($_POST["${arg5}"]);$T=@mysqli_connect($hst,$usr,$pwd);@mysqli_query($T,"SET NAMES ${arg6}");@mysqli_select_db($T,$dbn);$q=@mysqli_query($T,$sql);if(is_bool($q)){echo("Status\t|\t\r\n".($q?"VHJ1ZQ==":"RmFsc2U=")."\t|\t\r\n");}{$i=0;while($col=@mysqli_fetch_field($q)){echo($col->name."\t|\t");$i++;}echo("\r\n");while($rs=@mysqli_fetch_row($q)){for($c=0;$c<$i;$c++){echo(base64_encode(trim($rs[$c])));echo("\t|\t");}echo("\r\n");}}@mysqli_close($T);`, `$m=get_magic_quotes_gpc();$hst=$m?stripslashes($_POST["${arg1}"]):$_POST["${arg1}"];$usr=$m?stripslashes($_POST["${arg2}"]):$_POST["${arg2}"];$pwd=$m?stripslashes($_POST["${arg3}"]):$_POST["${arg3}"];$dbn=$m?stripslashes($_POST["${arg4}"]):$_POST["${arg4}"];$sql=base64_decode($_POST["${arg5}"]);$T=@mysqli_connect($hst,$usr,$pwd);@mysqli_query($T,"SET NAMES $_POST[${arg6}]");@mysqli_select_db($T,$dbn);$q=@mysqli_query($T,$sql);if(is_bool($q)){echo("Status\t|\t\r\n".($q?"VHJ1ZQ==":"RmFsc2U=")."\t|\t\r\n");}{$i=0;while($col=@mysqli_fetch_field($q)){echo($col->name."\t|\t");$i++;}echo("\r\n");while($rs=@mysqli_fetch_row($q)){for($c=0;$c<$i;$c++){echo(base64_encode(trim($rs[$c])));echo("\t|\t");}echo("\r\n");}}@mysqli_close($T);`,
[arg1]: '#{host}', [arg1]: '#{host}',
[arg2]: '#{user}', [arg2]: '#{user}',
[arg3]: '#{passwd}', [arg3]: '#{passwd}',
......
...@@ -168,6 +168,7 @@ module.exports = { ...@@ -168,6 +168,7 @@ module.exports = {
}, },
otherConf: { otherConf: {
nohttps: 'Ignore HTTPS certificate', nohttps: 'Ignore HTTPS certificate',
usemultipart: 'Use Multipart send payload',
terminalCache: "Use the terminal's cache", terminalCache: "Use the terminal's cache",
filemanagerCache: "Use the filemanager's cache", filemanagerCache: "Use the filemanager's cache",
uploadFragment: "Upload File Fragmentation Size", uploadFragment: "Upload File Fragmentation Size",
......
...@@ -169,6 +169,7 @@ module.exports = { ...@@ -169,6 +169,7 @@ module.exports = {
}, },
otherConf: { otherConf: {
nohttps: '忽略HTTPS证书', nohttps: '忽略HTTPS证书',
usemultipart: '使用 Multipart 发包',
terminalCache: '虚拟终端使用缓存', terminalCache: '虚拟终端使用缓存',
filemanagerCache: '文件管理使用缓存', filemanagerCache: '文件管理使用缓存',
uploadFragment: '上传文件分片大小', uploadFragment: '上传文件分片大小',
......
...@@ -85,6 +85,17 @@ window.addEventListener('load', () => { ...@@ -85,6 +85,17 @@ window.addEventListener('load', () => {
}); });
} }
/**
* 加载界面 UI 修改
*/
function loadingUI() {
let now = new Date();
/** 加载圣诞节 loading 效果 */
if(now.getMonth()+1 == 12) {
document.getElementById('loading').classList.add('loading_christmas');
}
}
loadingUI();
// 开始加载css // 开始加载css
loadCSS('ant-static://libs/bmenu/bmenu.css') loadCSS('ant-static://libs/bmenu/bmenu.css')
.then(() => loadCSS('ant-static://libs/toastr/toastr.min.css')) .then(() => loadCSS('ant-static://libs/toastr/toastr.min.css'))
......
...@@ -485,26 +485,35 @@ class PHP { ...@@ -485,26 +485,35 @@ class PHP {
], true); ], true);
form.attachEvent('onChange', (_, id) => { form.attachEvent('onChange', (_, id) => {
if (_ !== 'type') { return }; if (_ == 'type') {
switch(id) { switch(id) {
case 'mysql': case 'mysql':
case 'mysqli': case 'mysqli':
form.setFormData({ form.setFormData({
user: conf['user'], encode: conf['encode'],
passwd: conf['passwd'] user: conf['user'],
}); passwd: conf['passwd']
break; });
case 'mssql': break;
form.setFormData({ case 'mssql':
user: conf['user'], form.setFormData({
passwd: conf['passwd'] encode: conf['encode'],
}); user: conf['user'],
break; passwd: conf['passwd']
default: });
form.setFormData({ break;
user: conf['user'], default:
passwd: conf['passwd'] form.setFormData({
}); encode: conf['encode'],
user: conf['user'],
passwd: conf['passwd']
});
}
};
if(_ == 'encode') {
form.setFormData({
encode: id,
});
} }
}); });
......
...@@ -29,9 +29,9 @@ class Plugin { ...@@ -29,9 +29,9 @@ class Plugin {
return this.win.focus(); return this.win.focus();
} }
let win = new antSword['remote'].BrowserWindow({ let win = new antSword['remote'].BrowserWindow({
width: 930, width: 950,
height: 666, height: 666,
minWidth: 888, minWidth: 650,
minHeight: 555, minHeight: 555,
show: false, show: false,
title: 'AntSword.Store' title: 'AntSword.Store'
......
...@@ -140,7 +140,7 @@ class Form { ...@@ -140,7 +140,7 @@ class Form {
*/ */
_createBaseForm(arg) { _createBaseForm(arg) {
const opt = Object.assign({}, { const opt = Object.assign({}, {
url: '', url: 'http://',
pwd: '', pwd: '',
note: '', note: '',
type: 'php', type: 'php',
...@@ -168,6 +168,47 @@ class Form { ...@@ -168,6 +168,47 @@ class Form {
} }
] } ] }
], true); ], true);
form.attachEvent('onChange', (_, id) => {
// 根据后缀自动修改 shell 类型
if(_ == "url") {
let file_match = {
"php": /.+\.ph(p[345]?|s|t|tml)/,
"aspx": /.+\.as(px|mx)/,
"asp": /.+\.(as(p|a|hx)|c(dx|er))/,
"custom": /.+\.((jsp[x]?)|cgi)/,
}
let typecombo = form.getCombo('type');
if(file_match.php.test(id) == true) {
typecombo.selectOption(typecombo.getOption('php').index);
}else if(file_match.aspx.test(id) == true){
typecombo.selectOption(typecombo.getOption('aspx').index);
}else if(file_match.asp.test(id) == true){
typecombo.selectOption(typecombo.getOption('asp').index);
}else if(file_match.custom.test(id) == true){
typecombo.selectOption(typecombo.getOption('custom').index);
}
}
// 默认编码设置
if(_ == "type") {
let encodecombo = form.getCombo('encode');
switch(id) {
case 'php':
encodecombo.selectOption(encodecombo.getOption('UTF8').index);
break;
case 'asp':
encodecombo.selectOption(encodecombo.getOption('GBK').index);
break;
case 'aspx':
encodecombo.selectOption(encodecombo.getOption('UTF8').index);
break;
case 'custom':
encodecombo.selectOption(encodecombo.getOption('UTF8').index);
break;
}
}
});
return form; return form;
} }
...@@ -276,6 +317,7 @@ class Form { ...@@ -276,6 +317,7 @@ class Form {
_createOtherForm(arg) { _createOtherForm(arg) {
const opt = Object.assign({}, { const opt = Object.assign({}, {
'ignore-https': 0, 'ignore-https': 0,
'use-multipart': 0,
'terminal-cache': 0, 'terminal-cache': 0,
'filemanager-cache': 1, 'filemanager-cache': 1,
'upload-fragment': '500', 'upload-fragment': '500',
...@@ -289,6 +331,9 @@ class Form { ...@@ -289,6 +331,9 @@ class Form {
{ {
type: "checkbox", name: 'ignore-https', label: LANG['list']['otherConf']['nohttps'], type: "checkbox", name: 'ignore-https', label: LANG['list']['otherConf']['nohttps'],
checked: opt['ignore-https'] === 1 checked: opt['ignore-https'] === 1
}, {
type: "checkbox", name: 'use-multipart', label: LANG['list']['otherConf']['usemultipart'],
checked: opt['use-multipart'] === 1
}, { }, {
type: "checkbox", name: 'terminal-cache', label: LANG['list']['otherConf']['terminalCache'], type: "checkbox", name: 'terminal-cache', label: LANG['list']['otherConf']['terminalCache'],
checked: opt['terminal-cache'] === 1 checked: opt['terminal-cache'] === 1
......
...@@ -11,11 +11,18 @@ html, body, #container, #loading { ...@@ -11,11 +11,18 @@ html, body, #container, #loading {
position: fixed; position: fixed;
background-color: #FFF; background-color: #FFF;
text-align: center; text-align: center;
background-image: url(ant-static://imgs/load.png);
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 50% 50%; background-position: 50% 50%;
} }
.loading_default {
background-image: url(ant-static://imgs/load.png);
}
.loading_christmas {
background-image: url(ant-static://imgs/load-christmas.png);
}
/*sidebar.bubble*/ /*sidebar.bubble*/
.dhxsidebar_bubble { .dhxsidebar_bubble {
width: auto !important; width: auto !important;
......
...@@ -7,6 +7,6 @@ ...@@ -7,6 +7,6 @@
<script src="ant-src://load.entry.js"></script> <script src="ant-src://load.entry.js"></script>
</head> </head>
<body> <body>
<div id="loading"></div> <div id="loading" class="loading_default"></div>
</body> </body>
</html> </html>
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