Commit ee6be16c authored by zhenghongbin's avatar zhenghongbin

add 网络框架加密

parent 7f18ade4
......@@ -3,6 +3,7 @@ package com.yidian.common
class AppConfig {
companion object{
//生活圈项目
const val IS_ENCRYPT = false
const val KUANGSHI_ALIVE_API_KEY = "32f9XIsReV4S15Ck_Sa3ky43XgAHUB9v"
const val KUANGSHI_SECRET = "usZbQYdI4PQeXhMftsRfHK2msj0DmSIl"
......
package com.yidian.common.http
/**
* create by Administrator
* date 2019/2/12 0012
* desc
*/
class HttpDecryptResult<T> {
lateinit var secret: String
lateinit var reqid: String
var ts: Long = 0
var data: T? = null
}
\ No newline at end of file
package com.yidian.common.http
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.yidian.common.AppConfig
import com.yidian.common.YdBaseApplication
import com.yidian.common.utils.EncryptUtil
import com.yidian.common.utils.NetWorkUtils
import com.yidian.framework.mobile.xdiamond.SecretUtil
import com.yidian.utils.LogUtil
import com.yidian.utils.ToastUtil
import com.yidian.yac.ftdevicefinger.core.FtDeviceFingerManager
class HttpParamsUtils{
companion object{
fun getParamsMap(): HashMap<String, String>{
fun getPublicParamsMap(timeStamp: Long): HashMap<String, String>{
val networkType = NetWorkUtils.getNetWorkType(YdBaseApplication.context)
val timeStamp = System.currentTimeMillis()
val queryParamsMap = HashMap<String, String>()
queryParamsMap["appid"] = AppConfig.appid
queryParamsMap["cv"] = AppConfig.cv
......@@ -23,6 +26,54 @@ class HttpParamsUtils{
return queryParamsMap
}
fun getPrivateParamsMap(paramsMap: HashMap<String, String?>, timeStamp: Long): HashMap<String, String?>{
return if(AppConfig.IS_ENCRYPT){
val reqId = getRequestId(timeStamp)
paramsMap["secret"] = getSignString(reqId, timeStamp)
paramsMap["ts"] = timeStamp.toString()
val paramsJson = Gson().toJson(paramsMap)
LogUtil.show("请求参数:$paramsJson")
val tok = SecretUtil.rsaEncrypt(paramsJson)
val queryParamsMap = HashMap<String, String?>()
queryParamsMap["tok"] = tok
queryParamsMap
}else{
paramsMap
}
}
fun<T> rsaDecryptResult(res: HttpResult<Any?>): HttpResult<T>{
val rsaResult = res.result
val decodeResult = HttpResult<T>()
decodeResult.code = res.code
decodeResult.reason = res.reason
decodeResult.status = res.status
if(rsaResult != null){
val gson = Gson()
if(AppConfig.IS_ENCRYPT){
val result = SecretUtil.rsaDecrypt(rsaResult as String)
LogUtil.show("返回参数:$result")
val decryptResult = gson.fromJson<HttpDecryptResult<T>>(result, object: TypeToken<HttpDecryptResult<T>>(){}.type)
val verifyParams = decryptResult.reqid + decryptResult.ts
val secretNet = decryptResult.secret
val verify = SecretUtil.verifySign(verifyParams, secretNet)
return if(verify){
LogUtil.show("===============验签成功==================")
decodeResult.result = decryptResult.data
decodeResult
}else {
ToastUtil.showToast(YdBaseApplication.context, "验签失败")
decodeResult
}
}else{
val resultJson = gson.toJson(rsaResult)
decodeResult.result = gson.fromJson<T>(resultJson, object: TypeToken<T>(){}.type)
return decodeResult
}
}
return decodeResult
}
private fun getDeviceFinger(): String{
var deviceFinger = ""
val tempId = FtDeviceFingerManager.getDeviceFinger()
......@@ -42,19 +93,10 @@ class HttpParamsUtils{
return sb.toString()
}
private fun getSignString(timeStamp: Long): String{
private fun getSignString(reqId: String, timeStamp: Long): String?{
val sb = StringBuilder()
sb.append(AppConfig.appid)
sb.append(AppConfig.platform)
sb.append(getDeviceFinger())
sb.append(getRequestId(timeStamp))
sb.append("ydapp-metro")
val signMD5 = EncryptUtil.getMD5(sb.toString())
return if(signMD5.length > 8){
signMD5.substring(0, 8)
}else{
signMD5
}
sb.append(reqId).append(timeStamp)
return SecretUtil.sign(sb.toString())
}
}
}
\ No newline at end of file
......@@ -12,7 +12,7 @@ import io.reactivex.rxjava3.core.Observer
import io.reactivex.rxjava3.disposables.Disposable
import kotlin.reflect.typeOf
abstract class HttpResultSubscriber<T>(private var showProgress: Boolean = false) : Observer<HttpResult<T>?> {
abstract class HttpResultSubscriber<T>(private var showProgress: Boolean = false): Observer<HttpResult<Any?>> {
private var isShowErrorMsg = true
override fun onSubscribe(d: Disposable) { //网络请求之前
......@@ -48,20 +48,17 @@ abstract class HttpResultSubscriber<T>(private var showProgress: Boolean = false
onRequestFailure(Exception(e.toString()))
}
override fun onNext(res: HttpResult<T>?) {
if (res?.code == 0) {
// val result = res.result as String
// val resultObj = Gson().fromJson<T>(result, object: TypeToken<T>(){}.type)
// val decodeResult = HttpResult<T>()
// decodeResult.code = res.code
// decodeResult.reason = res.reason
// decodeResult.status = res.status
// decodeResult.result = resultObj
// onRequestSuccess(decodeResult)
onRequestSuccess(res)
override fun onNext(res: HttpResult<Any?>) {
if (res.code == 0) {
val decodeResult = HttpParamsUtils.rsaDecryptResult<T>(res)
onRequestSuccess(decodeResult)
}else{
onFailer(res)
ToastUtil.showToast(YdBaseApplication.context, res?.reason)
val decodeResult = HttpResult<T>()
decodeResult.code = res.code
decodeResult.reason = res.reason
decodeResult.status = res.status
onFailer(decodeResult)
ToastUtil.showToast(YdBaseApplication.context, res.reason)
}
}
......
......@@ -6,7 +6,7 @@ class URLs {
companion object{
val BASE_URL: String
private const val BASE_URL_DEBUG = "http://bp-test.go2yd.com"
private const val BASE_URL_DEBUG = "http://bp-dev.go2yd.com"
private const val BASE_URL_PRO = "http://bp-test.go2yd.com"
init {
......
......@@ -7,8 +7,8 @@ class CreateLifeAccountBean {
* user_id 是 str 用id
*/
data class Request(
var life_account_name:String,
var code:String,
var user_id:String
var life_account_name: String,
var code: String,
var user_id: String
)
}
\ No newline at end of file
......@@ -16,47 +16,47 @@ interface CommonService {
@Headers("Content-Type: application/json")
@POST(URLs.pushTokenAndroid)
fun pushTokenAndroid(@QueryMap commonParams: Map<String, String>, @Body requestParams: PushTokenAndroidBean.Request): Observable<HttpResult<Any?>>
fun pushTokenAndroid(@QueryMap commonParams: Map<String, String>, @Body requestParams: Map<String, String?>): Observable<HttpResult<Any?>>
@Headers("Content-Type: application/json")
@POST(URLs.authPersonalGetToken)
fun authPersonalGetToken(@QueryMap commonParams: Map<String, String>, @Body requestParams: AuthPersonalGetTokenBean.Request): Observable<HttpResult<AuthPersonalGetTokenBean.Response?>>
fun authPersonalGetToken(@QueryMap commonParams: Map<String, String>, @Body requestParams: Map<String, String?>): Observable<HttpResult<Any?>>
// @(URLs.authPersonalGetToken)
// fun authPersonalGetToken(@QueryMap commonParams: Map<String, String>, @QueryMap requestParams: Map<String, String>): Observable<HttpResult<Any?>>
@Headers("Content-Type: application/json")
@POST(URLs.getKSYunToken)
fun getKSYunToken(@QueryMap commonParams: Map<String, String>, @Body requestParams: GetKSYunTokenBean.Request): Call<HttpResult<GetKSYunTokenBean.Response>>
fun getKSYunToken(@QueryMap commonParams: Map<String, String>, @Body requestParams: Map<String, String?>): Call<HttpResult<Any?>>
@Headers("Content-Type: application/json")
@POST(URLs.getIDCardOCR)
fun getIDCardOCR(@QueryMap commonParams: Map<String, String>, @Body requestParams: GetIDCardOCRBean.Request): Observable<HttpResult<GetIDCardOCRBean.Response>>
fun getIDCardOCR(@QueryMap commonParams: Map<String, String>, @Body requestParams: Map<String, String?>): Observable<HttpResult<Any?>>
@Headers("Content-Type: application/json")
@POST(URLs.identifyIdOcrVerify)
fun identifyIdOcrVerify(@QueryMap commonParams: Map<String, String>, @Body requestParams: IdentifyIdOcrVerifyBean.Request): Observable<HttpResult<IdentifyIdOcrVerifyBean.Response?>>
fun identifyIdOcrVerify(@QueryMap commonParams: Map<String, String>, @Body requestParams: Map<String, String?>): Observable<HttpResult<Any?>>
@GET(URLs.getKSYunObjectId)
fun getKSYunObjectId(@QueryMap commonParams: Map<String, String>) : Observable<HttpResult<GetKSYunObjectIdBean.Response>>
fun getKSYunObjectId(@QueryMap commonParams: Map<String, String>) : Observable<HttpResult<Any?>>
@GET(URLs.getKSYunBucket)
fun getKSYunBucket(@QueryMap commonParams: Map<String, String>) : Observable<HttpResult<GetKSYunBucketBean.Response>>
fun getKSYunBucket(@QueryMap commonParams: Map<String, String>) : Observable<HttpResult<Any?>>
@Headers("Content-Type: application/json")
@POST(URLs.businessLicenseOCR)
fun businessLicenseOCR(@QueryMap commonParams: Map<String, String>, @Body requestParams: BusinessLicenseOCRBean.Request) : Observable<HttpResult<BusinessLicenseOCRBean.Response>>
fun businessLicenseOCR(@QueryMap commonParams: Map<String, String>, @Body requestParams: Map<String, String?>) : Observable<HttpResult<Any?>>
@GET(URLs.sendMsgCode)
fun sendMsgCode(@QueryMap commonParams: Map<String, String>, @QueryMap requestParams: Map<String, String>): Observable<HttpResult<Any?>>
fun sendMsgCode(@QueryMap commonParams: Map<String, String>, @QueryMap requestParams: Map<String, String?>): Observable<HttpResult<Any?>>
@GET(URLs.mobileLogin)
fun mobileLogin(@QueryMap commonParams: Map<String, String>, @QueryMap requestParams: Map<String, String>): Observable<HttpResult<MobileLoginBean.Response?>>
fun mobileLogin(@QueryMap commonParams: Map<String, String>, @QueryMap requestParams: Map<String, String?>): Observable<HttpResult<Any?>>
@GET(URLs.accountList)
fun getAccountList(@QueryMap commonParams: Map<String, String>): Observable<HttpResult<ArrayList<AccountItemBean>?>>
fun getAccountList(@QueryMap commonParams: Map<String, String>): Observable<HttpResult<Any?>>
@Headers("Content-Type: application/json")
@POST(URLs.createLifeAccount)
fun createLifeAccount(@QueryMap commonParams: Map<String, String>,@Body requestParams: CreateLifeAccountBean.Request) :Observable<HttpResult<Any?>>
fun createLifeAccount(@QueryMap commonParams: Map<String, String>, @Body requestParams: Map<String, String?>) :Observable<HttpResult<Any?>>
}
\ No newline at end of file
......@@ -30,20 +30,31 @@ class FlashActivity: BaseActivity<ActivityFlashBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
getPermissions()
viewBind.clRoot.postDelayed({
XPageManager.push(XRouterPathConstants.LOGIN_LIFE_CIRCLE, null)
finish()
}, 2000)
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
launcher()
// launcher()
}
private fun launcher(){
val isAgree = Hawk.get<Boolean>(HawkConfig.ProtocolIsAgree, false)
if(isAgree){
viewBind.clRoot.postDelayed({
XPageManager.push(XRouterPathConstants.LOGIN_LIFE_CIRCLE, null)
}, 2000)
val loginStatus = Hawk.get(HawkConfig.LoginStatus, false)
if(loginStatus){
//跳转管理页
}else{
viewBind.clRoot.postDelayed({
XPageManager.push(XRouterPathConstants.LOGIN_LIFE_CIRCLE, null)
finish()
}, 2000)
}
}else{
//调起协议dialog
}
}
......
......@@ -115,7 +115,8 @@ class LifeAccountEnterpriseCertificationActivity :
}
override fun onTaskSuccess(bucket: String?, objectKey: String?) {
val request = BusinessLicenseOCRBean.Request(objectKey)
val requestParams = HashMap<String, String?>()
requestParams["business_image_objectid"] = objectKey
ApiService.businessLicenseOCR(object : IBusinessLicenseOCRCallback {
override fun businessLicenseOCRSuccess(result: BusinessLicenseOCRBean.Response?) {
Log.d(TAG, "name: ${result?.name}, code: ${result?.code}")
......@@ -126,7 +127,7 @@ class LifeAccountEnterpriseCertificationActivity :
}
}, request)
}, requestParams)
}
override fun onTaskFailure(statesCode: Int, message: String?) {
......
......@@ -96,8 +96,11 @@ class LiveAccountCreateActivity : BaseActivity<ActivityCreateAccountBinding>(),
ToastUtil.showToast(this,"生活号信息异常,请退出重试!")
return
}
val request = CreateLifeAccountBean.Request(accountName,enterpriseCode,userId)
ApiService.createLifeAccount(this, request)
val requestParams = HashMap<String, String?>()
requestParams["life_account_name"] = accountName
requestParams["code"] = enterpriseCode
requestParams["user_id"] = userId
ApiService.createLifeAccount(this, requestParams)
}
......
package com.yidian.shenghuoquan.newscontent.ui
import android.content.Intent
import android.os.Bundle
import com.orhanobut.hawk.Hawk
import com.yidian.common.HawkConfig
......@@ -12,7 +11,9 @@ import com.yidian.common.widget.EditTextInputCallback
import com.yidian.common.widget.PhoneNumberTextWatcher
import com.yidian.shenghuoquan.newscontent.databinding.ActivityLoginBinding
import com.yidian.shenghuoquan.newscontent.http.ApiService
import com.yidian.shenghuoquan.newscontent.http.httpbean.*
import com.yidian.shenghuoquan.newscontent.http.httpbean.AccountItemBean
import com.yidian.shenghuoquan.newscontent.http.httpbean.IMobileLoginCallback
import com.yidian.shenghuoquan.newscontent.http.httpbean.MobileLoginBean
import com.yidian.shenghuoquan.newscontent.utils.CountDownTimerUtils
import com.yidian.shenghuoquan.newscontent.utils.TextWatcherAdapter
import com.yidian.utils.ToastUtil
......@@ -52,7 +53,7 @@ class LoginLifeCircleActivity : BaseActivity<ActivityLoginBinding>() {
viewBind.tvGetCode.setOnClickListener {
val mobile = viewBind.etMobileNo.text.toString().replace(" ", "")
if(mobile.length == 11){
val paramsMap = HashMap<String, String>()
val paramsMap = HashMap<String, String?>()
paramsMap["mobile"] = mobile
ApiService.sendMsgCode(loginImpl, paramsMap)
}else{
......@@ -87,7 +88,7 @@ class LoginLifeCircleActivity : BaseActivity<ActivityLoginBinding>() {
viewBind.tvLogin.setOnClickListener {
val mobile = viewBind.etMobileNo.text.toString().replace(" ", "")
val code = viewBind.etCode.text.toString()
val paramsMap = HashMap<String, String>()
val paramsMap = HashMap<String, String?>()
paramsMap["mobile"] = mobile
paramsMap["code"] = code
ApiService.mobileLogin(loginImpl, paramsMap)
......
......@@ -129,11 +129,16 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
}
private fun bindPushToken() {
val requestParams: PushTokenAndroidBean.Request
val encryptionToken = ToolsUtil.getYDEncryptionToken()
val pushLevel = AppConfig.PUSH_LEVEL+ToolsUtil.getDeviceBrandMask()
if (null != Hawk.get<String>(HawkConfig.UmToken)) {
requestParams = PushTokenAndroidBean.Request("", encryptionToken, 1, 1, Hawk.get(HawkConfig.UmToken),pushLevel)
val requestParams = HashMap<String, String?>()
requestParams["old_token"] = ""
requestParams["new_token"] = encryptionToken
requestParams["personalRec"] = "1"
requestParams["enable"] = "1"
requestParams["push_key"] = Hawk.get(HawkConfig.UmToken)
requestParams["push_level"] = pushLevel.toString()
ApiService.pushTokenAndroid(requestParams)
}
}
......
......@@ -66,8 +66,10 @@ class AliveTestActivity : BaseActivity<ActivityAliveLayoutBinding>(), PreCallbac
private fun setOnListener() {
viewBind.btActionYy.setOnClickListener {
val requestParams: AuthPersonalGetTokenBean.Request = AuthPersonalGetTokenBean.Request(
idCardNo!!, idCardName!!, "meglive")
val requestParams = HashMap<String, String?>()
requestParams["id_number"] = idCardNo
requestParams["id_card_name"] = idCardName
requestParams["liveness_type"] = "meglive"
ApiService.authPersonalGetToken(authPersonalGetTokenCallback, requestParams)
}
}
......@@ -200,7 +202,9 @@ class AliveTestActivity : BaseActivity<ActivityAliveLayoutBinding>(), PreCallbac
}
override fun onTaskSuccess(bucket: String?, objectKey: String?) {
val requestParams: IdentifyIdOcrVerifyBean.Request = IdentifyIdOcrVerifyBean.Request(bizToken, objectKey!!)
val requestParams = HashMap<String, String?>()
requestParams["biz_token"] = bizToken
requestParams["meglive_objectid"] = objectKey
ApiService.identifyIdOcrVerify(identifyIdOcrVerifyCallback, requestParams)
}
......
......@@ -180,8 +180,9 @@ class LifeAccountBusinessLicenseAuthFragment :
* 执行营业执照OCR
*/
private fun startBusinessLicenseOCR(objectKey: String?) {
val request = BusinessLicenseOCRBean.Request(objectKey)
ApiService.businessLicenseOCR(this, request)
val requestParams = HashMap<String, String?>()
requestParams["business_image_objectid"] = objectKey
ApiService.businessLicenseOCR(this, requestParams)
}
override fun onTaskStart() {
......
......@@ -187,13 +187,11 @@ class LifeAccountIDCardAuthFragment : BaseFragment<FragmentLifeAccountIdCardAuth
// 若存在上一次活体检测数据则删除 处理活体检测失败 再次进入
File(cachePath + Constant.FILE_PATH_ALIVE_DETECT_VERIFY_DATA).delete()
// 跳转人脸认证
ApiService.authPersonalGetToken(
this, AuthPersonalGetTokenBean.Request(
LifeAccountAuthDataManager.personalAuthData.idCardNum,
LifeAccountAuthDataManager.personalAuthData.realName,
Constant.TYPE_MEG_LIVE
)
)
val requestParams = HashMap<String, String?>()
requestParams["id_number"] = LifeAccountAuthDataManager.personalAuthData.idCardNum
requestParams["id_card_name"] = LifeAccountAuthDataManager.personalAuthData.realName
requestParams["liveness_type"] = Constant.TYPE_MEG_LIVE
ApiService.authPersonalGetToken(this, requestParams)
}
}
}
......@@ -392,13 +390,10 @@ class LifeAccountIDCardAuthFragment : BaseFragment<FragmentLifeAccountIdCardAuth
* 执行身份证OCR
*/
private fun startIDCardOCR() {
ApiService.getIDCardOCR(
this,
GetIDCardOCRBean.Request(
LifeAccountAuthDataManager.personalAuthData.idCardPortraitFaceObjectKey,
LifeAccountAuthDataManager.personalAuthData.idCardNationalEmblemFaceObjectKey
)
)
val requestParams = HashMap<String, String?>()
requestParams["posit_image_objectid"] = LifeAccountAuthDataManager.personalAuthData.idCardPortraitFaceObjectKey
requestParams["back_image_objectid"] = LifeAccountAuthDataManager.personalAuthData.idCardNationalEmblemFaceObjectKey
ApiService.getIDCardOCR(this, requestParams)
}
override fun getIDCardOCRSuccess(result: GetIDCardOCRBean.Response?) {
......@@ -516,13 +511,10 @@ class LifeAccountIDCardAuthFragment : BaseFragment<FragmentLifeAccountIdCardAuth
objectKey?.let {
LifeAccountAuthDataManager.personalAuthData.liveDetectObjectKey = it
}
ApiService.identifyIdOcrVerify(
this,
IdentifyIdOcrVerifyBean.Request(
LifeAccountAuthDataManager.personalAuthData.liveDetectBizToken,
LifeAccountAuthDataManager.personalAuthData.liveDetectObjectKey
)
)
val requestParams = HashMap<String, String?>()
requestParams["biz_token"] = LifeAccountAuthDataManager.personalAuthData.liveDetectBizToken
requestParams["meglive_objectid"] = LifeAccountAuthDataManager.personalAuthData.liveDetectObjectKey
ApiService.identifyIdOcrVerify(this, requestParams)
}
override fun onTaskFailure(statesCode: Int, message: String?) {
......
......@@ -20,6 +20,7 @@ import com.yidian.shenghuoquan.newscontent.http.ApiService
import com.yidian.shenghuoquan.newscontent.http.httpbean.GetIDCardOCRBean
import com.yidian.shenghuoquan.newscontent.http.httpbean.IGetIDCardOCRCallback
import com.yidian.shenghuoquan.newscontent.ui.alive.AliveTestActivity
import com.yidian.shenghuoquan.newscontent.ui.auth.LifeAccountAuthDataManager
import com.yidian.shenghuoquan.newscontent.utils.KS3Core
import com.yidian.utils.ToastUtil
import java.io.File
......@@ -118,7 +119,9 @@ class IDCardTestActivity : BaseActivity<ActivityIdcardBinding>() {
if (objectKey != null) {
idCardBackObjectKey = objectKey
}
val request = GetIDCardOCRBean.Request(idCardFrontObjectKey, idCardBackObjectKey)
val requestParams = HashMap<String, String?>()
requestParams["posit_image_objectid"] = idCardFrontObjectKey
requestParams["back_image_objectid"] = idCardBackObjectKey
ApiService.getIDCardOCR(object : IGetIDCardOCRCallback {
override fun getIDCardOCRSuccess(result: GetIDCardOCRBean.Response?) {
Log.d(KS3Core.TAG, "name: ${result?.posit?.name}, id num: ${result?.posit?.idcard_number}")
......@@ -135,7 +138,7 @@ class IDCardTestActivity : BaseActivity<ActivityIdcardBinding>() {
}
}, request)
}, requestParams)
}
override fun onTaskFailure(statesCode: Int, message: String?) {
......
......@@ -266,16 +266,14 @@ class KS3Core private constructor(val context: Context) : AuthListener {
TAG,
"KSYun calculate auth, httpMethod: $httpMethod, contentType: $contentType, date: $date, contentMD5: $contentMD5, resource: $resource, headers: $headers"
)
return ApiService.getKSYunToken(
GetKSYunTokenBean.Request(
httpMethod,
date,
resource,
contentMD5,
contentType,
headers
)
)?.result?.token
val requestParams = HashMap<String, String?>()
requestParams["http_method"] = httpMethod
requestParams["date"] = date
requestParams["resource"] = resource
requestParams["content_md5"] = contentMD5
requestParams["content_type"] = contentType
requestParams["headers"] = headers
return ApiService.getKSYunToken(requestParams)?.result?.token
}
/**
......
......@@ -56,7 +56,7 @@ ext.dependencies = [
'io.reactivex.rxjava3:rxjava:3.0.9',
'com.yidian.framework.mobile:ydhttp:1.0.8-SNAPSHOT',
'com.yidian.framework.mobile:ydutils:1.0.1-SNAPSHOT',
'com.yidian.framework.mobile:xdiamond:1.0.2-SNAPSHOT',
'com.yidian.framework.mobile:xdiamond:1.0.4-SNAPSHOT',
'com.loopj.android:android-async-http:1.4.9'
],
......
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