Preliminary Java 9+ Support (#20).
We will still not allow these versions to be used until they have been fully verified on our far-future 1.13 test server.
This commit is contained in:
parent
be533af38b
commit
d7fe519923
@ -475,12 +475,29 @@ class AssetGuard extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a **full** Java Runtime version string and resolves
|
* Parses a **full** Java Runtime version string and resolves
|
||||||
* the version information. Uses Java 8 formatting.
|
* the version information. Dynamically detects the formatting
|
||||||
|
* to use.
|
||||||
*
|
*
|
||||||
* @param {string} verString Full version string to parse.
|
* @param {string} verString Full version string to parse.
|
||||||
* @returns Object containing the version information.
|
* @returns Object containing the version information.
|
||||||
*/
|
*/
|
||||||
static parseJavaRuntimeVersion(verString){
|
static parseJavaRuntimeVersion(verString){
|
||||||
|
const major = verString.split('.')[0]
|
||||||
|
if(major == 1){
|
||||||
|
return AssetGuard._parseJavaRuntimeVersion_8(verString)
|
||||||
|
} else {
|
||||||
|
return AssetGuard._parseJavaRuntimeVersion_9(verString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a **full** Java Runtime version string and resolves
|
||||||
|
* the version information. Uses Java 8 formatting.
|
||||||
|
*
|
||||||
|
* @param {string} verString Full version string to parse.
|
||||||
|
* @returns Object containing the version information.
|
||||||
|
*/
|
||||||
|
static _parseJavaRuntimeVersion_8(verString){
|
||||||
// 1.{major}.0_{update}-b{build}
|
// 1.{major}.0_{update}-b{build}
|
||||||
// ex. 1.8.0_152-b16
|
// ex. 1.8.0_152-b16
|
||||||
const ret = {}
|
const ret = {}
|
||||||
@ -492,16 +509,56 @@ class AssetGuard extends EventEmitter {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a **full** Java Runtime version string and resolves
|
||||||
|
* the version information. Uses Java 9+ formatting.
|
||||||
|
*
|
||||||
|
* @param {string} verString Full version string to parse.
|
||||||
|
* @returns Object containing the version information.
|
||||||
|
*/
|
||||||
|
static _parseJavaRuntimeVersion_9(verString){
|
||||||
|
// {major}.{minor}.{revision}+{build}
|
||||||
|
// ex. 10.0.2+13
|
||||||
|
const ret = {}
|
||||||
|
let pts = verString.split('+')
|
||||||
|
ret.build = parseInt(pts[1])
|
||||||
|
pts = pts[0].split('.')
|
||||||
|
ret.major = parseInt(pts[0])
|
||||||
|
ret.minor = parseInt(pts[1])
|
||||||
|
ret.revision = parseInt(pts[2])
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the actual version is greater than
|
||||||
|
* or equal to the desired version.
|
||||||
|
*
|
||||||
|
* @param {string} desired The desired version.
|
||||||
|
* @param {string} actual The actual version.
|
||||||
|
*/
|
||||||
|
static mcVersionAtLeast(desired, actual){
|
||||||
|
const des = desired.split('.')
|
||||||
|
const act = actual.split('.')
|
||||||
|
|
||||||
|
for(let i=0; i<des.length; i++){
|
||||||
|
if(!(parseInt(act[i]) >= parseInt(des[i]))){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the output of a JVM's properties. Currently validates that a JRE is x64
|
* Validates the output of a JVM's properties. Currently validates that a JRE is x64
|
||||||
* and that the major = 8, update > 52.
|
* and that the major = 8, update > 52.
|
||||||
*
|
*
|
||||||
* @param {string} stderr The output to validate.
|
* @param {string} stderr The output to validate.
|
||||||
|
* @param {string} mcVersion The minecraft version we are scanning for.
|
||||||
*
|
*
|
||||||
* @returns {Promise.<Object>} A promise which resolves to a meta object about the JVM.
|
* @returns {Promise.<Object>} A promise which resolves to a meta object about the JVM.
|
||||||
* The validity is stored inside the `valid` property.
|
* The validity is stored inside the `valid` property.
|
||||||
*/
|
*/
|
||||||
static _validateJVMProperties(stderr){
|
static _validateJVMProperties(stderr, mcVersion){
|
||||||
const res = stderr
|
const res = stderr
|
||||||
const props = res.split('\n')
|
const props = res.split('\n')
|
||||||
|
|
||||||
@ -526,6 +583,8 @@ class AssetGuard extends EventEmitter {
|
|||||||
let verString = props[i].split('=')[1].trim()
|
let verString = props[i].split('=')[1].trim()
|
||||||
console.log(props[i].trim())
|
console.log(props[i].trim())
|
||||||
const verOb = AssetGuard.parseJavaRuntimeVersion(verString)
|
const verOb = AssetGuard.parseJavaRuntimeVersion(verString)
|
||||||
|
if(verOb.major < 9){
|
||||||
|
// Java 8
|
||||||
if(verOb.major === 8 && verOb.update > 52){
|
if(verOb.major === 8 && verOb.update > 52){
|
||||||
meta.version = verOb
|
meta.version = verOb
|
||||||
++checksum
|
++checksum
|
||||||
@ -533,6 +592,17 @@ class AssetGuard extends EventEmitter {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Java 9+
|
||||||
|
if(AssetGuard.mcVersionAtLeast('1.13', mcVersion)){
|
||||||
|
console.log('Java 9+ not yet tested.')
|
||||||
|
/* meta.version = verOb
|
||||||
|
++checksum
|
||||||
|
if(checksum === goal){
|
||||||
|
break
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -550,11 +620,12 @@ class AssetGuard extends EventEmitter {
|
|||||||
* removed.
|
* removed.
|
||||||
*
|
*
|
||||||
* @param {string} binaryExecPath Path to the java executable we wish to validate.
|
* @param {string} binaryExecPath Path to the java executable we wish to validate.
|
||||||
|
* @param {string} mcVersion The minecraft version we are scanning for.
|
||||||
*
|
*
|
||||||
* @returns {Promise.<Object>} A promise which resolves to a meta object about the JVM.
|
* @returns {Promise.<Object>} A promise which resolves to a meta object about the JVM.
|
||||||
* The validity is stored inside the `valid` property.
|
* The validity is stored inside the `valid` property.
|
||||||
*/
|
*/
|
||||||
static _validateJavaBinary(binaryExecPath){
|
static _validateJavaBinary(binaryExecPath, mcVersion){
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if(!AssetGuard.isJavaExecPath(binaryExecPath)){
|
if(!AssetGuard.isJavaExecPath(binaryExecPath)){
|
||||||
@ -563,7 +634,7 @@ class AssetGuard extends EventEmitter {
|
|||||||
child_process.exec('"' + binaryExecPath + '" -XshowSettings:properties', (err, stdout, stderr) => {
|
child_process.exec('"' + binaryExecPath + '" -XshowSettings:properties', (err, stdout, stderr) => {
|
||||||
try {
|
try {
|
||||||
// Output is stored in stderr?
|
// Output is stored in stderr?
|
||||||
resolve(this._validateJVMProperties(stderr))
|
resolve(this._validateJVMProperties(stderr, mcVersion))
|
||||||
} catch (err){
|
} catch (err){
|
||||||
// Output format might have changed, validation cannot be completed.
|
// Output format might have changed, validation cannot be completed.
|
||||||
resolve({valid: false})
|
resolve({valid: false})
|
||||||
@ -772,10 +843,11 @@ class AssetGuard extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {Set.<string>} rootSet A set of JVM root strings to validate.
|
* @param {Set.<string>} rootSet A set of JVM root strings to validate.
|
||||||
|
* @param {string} mcVersion The minecraft version we are scanning for.
|
||||||
* @returns {Promise.<Object[]>} A promise which resolves to an array of meta objects
|
* @returns {Promise.<Object[]>} A promise which resolves to an array of meta objects
|
||||||
* for each valid JVM root directory.
|
* for each valid JVM root directory.
|
||||||
*/
|
*/
|
||||||
static async _validateJavaRootSet(rootSet){
|
static async _validateJavaRootSet(rootSet, mcVersion){
|
||||||
|
|
||||||
const rootArr = Array.from(rootSet)
|
const rootArr = Array.from(rootSet)
|
||||||
const validArr = []
|
const validArr = []
|
||||||
@ -783,7 +855,7 @@ class AssetGuard extends EventEmitter {
|
|||||||
for(let i=0; i<rootArr.length; i++){
|
for(let i=0; i<rootArr.length; i++){
|
||||||
|
|
||||||
const execPath = AssetGuard.javaExecFromRoot(rootArr[i])
|
const execPath = AssetGuard.javaExecFromRoot(rootArr[i])
|
||||||
const metaOb = await AssetGuard._validateJavaBinary(execPath)
|
const metaOb = await AssetGuard._validateJavaBinary(execPath, mcVersion)
|
||||||
|
|
||||||
if(metaOb.valid){
|
if(metaOb.valid){
|
||||||
metaOb.execPath = execPath
|
metaOb.execPath = execPath
|
||||||
@ -805,36 +877,50 @@ class AssetGuard extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
static _sortValidJavaArray(validArr){
|
static _sortValidJavaArray(validArr){
|
||||||
const retArr = validArr.sort((a, b) => {
|
const retArr = validArr.sort((a, b) => {
|
||||||
// Note that Java 9+ uses semver and that will need to be accounted for in
|
|
||||||
// the future.
|
|
||||||
|
|
||||||
if(a.version.major === b.version.major){
|
if(a.version.major === b.version.major){
|
||||||
|
|
||||||
|
if(a.version.major < 9){
|
||||||
|
// Java 8
|
||||||
if(a.version.update === b.version.update){
|
if(a.version.update === b.version.update){
|
||||||
|
|
||||||
if(a.version.build === b.version.build){
|
if(a.version.build === b.version.build){
|
||||||
|
|
||||||
// Same version, give priority to JRE.
|
// Same version, give priority to JRE.
|
||||||
|
|
||||||
if(a.execPath.toLowerCase().indexOf('jdk') > -1){
|
if(a.execPath.toLowerCase().indexOf('jdk') > -1){
|
||||||
return b.execPath.toLowerCase().indexOf('jdk') > -1 ? 0 : 1
|
return b.execPath.toLowerCase().indexOf('jdk') > -1 ? 0 : 1
|
||||||
} else {
|
} else {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return a.version.build > b.version.build ? -1 : 1
|
return a.version.build > b.version.build ? -1 : 1
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return a.version.update > b.version.update ? -1 : 1
|
return a.version.update > b.version.update ? -1 : 1
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Java 9+
|
||||||
|
if(a.version.minor === b.version.minor){
|
||||||
|
if(a.version.revision === b.version.revision){
|
||||||
|
|
||||||
|
// Same version, give priority to JRE.
|
||||||
|
if(a.execPath.toLowerCase().indexOf('jdk') > -1){
|
||||||
|
return b.execPath.toLowerCase().indexOf('jdk') > -1 ? 0 : 1
|
||||||
|
} else {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return a.version.revision > b.version.revision ? -1 : 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return a.version.minor > b.version.minor ? -1 : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return a.version.major > b.version.major ? -1 : 1
|
return a.version.major > b.version.major ? -1 : 1
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return retArr
|
return retArr
|
||||||
@ -851,10 +937,11 @@ class AssetGuard extends EventEmitter {
|
|||||||
* If versions are equal, JRE > JDK.
|
* If versions are equal, JRE > JDK.
|
||||||
*
|
*
|
||||||
* @param {string} dataDir The base launcher directory.
|
* @param {string} dataDir The base launcher directory.
|
||||||
|
* @param {string} mcVersion The minecraft version we are scanning for.
|
||||||
* @returns {Promise.<string>} A Promise which resolves to the executable path of a valid
|
* @returns {Promise.<string>} A Promise which resolves to the executable path of a valid
|
||||||
* x64 Java installation. If none are found, null is returned.
|
* x64 Java installation. If none are found, null is returned.
|
||||||
*/
|
*/
|
||||||
static async _win32JavaValidate(dataDir){
|
static async _win32JavaValidate(dataDir, mcVersion){
|
||||||
|
|
||||||
// Get possible paths from the registry.
|
// Get possible paths from the registry.
|
||||||
let pathSet1 = await AssetGuard._scanRegistry()
|
let pathSet1 = await AssetGuard._scanRegistry()
|
||||||
@ -875,7 +962,7 @@ class AssetGuard extends EventEmitter {
|
|||||||
uberSet.add(jHome)
|
uberSet.add(jHome)
|
||||||
}
|
}
|
||||||
|
|
||||||
let pathArr = await AssetGuard._validateJavaRootSet(uberSet)
|
let pathArr = await AssetGuard._validateJavaRootSet(uberSet, mcVersion)
|
||||||
pathArr = AssetGuard._sortValidJavaArray(pathArr)
|
pathArr = AssetGuard._sortValidJavaArray(pathArr)
|
||||||
|
|
||||||
if(pathArr.length > 0){
|
if(pathArr.length > 0){
|
||||||
@ -896,10 +983,11 @@ class AssetGuard extends EventEmitter {
|
|||||||
* If versions are equal, JRE > JDK.
|
* If versions are equal, JRE > JDK.
|
||||||
*
|
*
|
||||||
* @param {string} dataDir The base launcher directory.
|
* @param {string} dataDir The base launcher directory.
|
||||||
|
* @param {string} mcVersion The minecraft version we are scanning for.
|
||||||
* @returns {Promise.<string>} A Promise which resolves to the executable path of a valid
|
* @returns {Promise.<string>} A Promise which resolves to the executable path of a valid
|
||||||
* x64 Java installation. If none are found, null is returned.
|
* x64 Java installation. If none are found, null is returned.
|
||||||
*/
|
*/
|
||||||
static async _darwinJavaValidate(dataDir){
|
static async _darwinJavaValidate(dataDir, mcVersion){
|
||||||
|
|
||||||
const pathSet1 = await AssetGuard._scanFileSystem('/Library/Java/JavaVirtualMachines')
|
const pathSet1 = await AssetGuard._scanFileSystem('/Library/Java/JavaVirtualMachines')
|
||||||
const pathSet2 = await AssetGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64'))
|
const pathSet2 = await AssetGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64'))
|
||||||
@ -922,7 +1010,7 @@ class AssetGuard extends EventEmitter {
|
|||||||
uberSet.add(jHome)
|
uberSet.add(jHome)
|
||||||
}
|
}
|
||||||
|
|
||||||
let pathArr = await AssetGuard._validateJavaRootSet(uberSet)
|
let pathArr = await AssetGuard._validateJavaRootSet(uberSet, mcVersion)
|
||||||
pathArr = AssetGuard._sortValidJavaArray(pathArr)
|
pathArr = AssetGuard._sortValidJavaArray(pathArr)
|
||||||
|
|
||||||
if(pathArr.length > 0){
|
if(pathArr.length > 0){
|
||||||
@ -941,10 +1029,11 @@ class AssetGuard extends EventEmitter {
|
|||||||
* If versions are equal, JRE > JDK.
|
* If versions are equal, JRE > JDK.
|
||||||
*
|
*
|
||||||
* @param {string} dataDir The base launcher directory.
|
* @param {string} dataDir The base launcher directory.
|
||||||
|
* @param {string} mcVersion The minecraft version we are scanning for.
|
||||||
* @returns {Promise.<string>} A Promise which resolves to the executable path of a valid
|
* @returns {Promise.<string>} A Promise which resolves to the executable path of a valid
|
||||||
* x64 Java installation. If none are found, null is returned.
|
* x64 Java installation. If none are found, null is returned.
|
||||||
*/
|
*/
|
||||||
static async _linuxJavaValidate(dataDir){
|
static async _linuxJavaValidate(dataDir, mcVersion){
|
||||||
|
|
||||||
const pathSet1 = await AssetGuard._scanFileSystem('/usr/lib/jvm')
|
const pathSet1 = await AssetGuard._scanFileSystem('/usr/lib/jvm')
|
||||||
const pathSet2 = await AssetGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64'))
|
const pathSet2 = await AssetGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64'))
|
||||||
@ -957,7 +1046,7 @@ class AssetGuard extends EventEmitter {
|
|||||||
uberSet.add(jHome)
|
uberSet.add(jHome)
|
||||||
}
|
}
|
||||||
|
|
||||||
let pathArr = await AssetGuard._validateJavaRootSet(uberSet)
|
let pathArr = await AssetGuard._validateJavaRootSet(uberSet, mcVersion)
|
||||||
pathArr = AssetGuard._sortValidJavaArray(pathArr)
|
pathArr = AssetGuard._sortValidJavaArray(pathArr)
|
||||||
|
|
||||||
if(pathArr.length > 0){
|
if(pathArr.length > 0){
|
||||||
@ -971,10 +1060,11 @@ class AssetGuard extends EventEmitter {
|
|||||||
* Retrieve the path of a valid x64 Java installation.
|
* Retrieve the path of a valid x64 Java installation.
|
||||||
*
|
*
|
||||||
* @param {string} dataDir The base launcher directory.
|
* @param {string} dataDir The base launcher directory.
|
||||||
|
* @param {string} mcVersion The minecraft version we are scanning for.
|
||||||
* @returns {string} A path to a valid x64 Java installation, null if none found.
|
* @returns {string} A path to a valid x64 Java installation, null if none found.
|
||||||
*/
|
*/
|
||||||
static async validateJava(dataDir){
|
static async validateJava(dataDir, mcVersion){
|
||||||
return await AssetGuard['_' + process.platform + 'JavaValidate'](dataDir)
|
return await AssetGuard['_' + process.platform + 'JavaValidate'](dataDir, mcVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
@ -86,21 +86,22 @@ function setLaunchEnabled(val){
|
|||||||
// Bind launch button
|
// Bind launch button
|
||||||
document.getElementById('launch_button').addEventListener('click', function(e){
|
document.getElementById('launch_button').addEventListener('click', function(e){
|
||||||
loggerLanding.log('Launching game..')
|
loggerLanding.log('Launching game..')
|
||||||
|
const mcVersion = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()).getMinecraftVersion()
|
||||||
const jExe = ConfigManager.getJavaExecutable()
|
const jExe = ConfigManager.getJavaExecutable()
|
||||||
if(jExe == null){
|
if(jExe == null){
|
||||||
asyncSystemScan()
|
asyncSystemScan(mcVersion)
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
setLaunchDetails('Please wait..')
|
setLaunchDetails('Please wait..')
|
||||||
toggleLaunchArea(true)
|
toggleLaunchArea(true)
|
||||||
setLaunchPercentage(0, 100)
|
setLaunchPercentage(0, 100)
|
||||||
|
|
||||||
AssetGuard._validateJavaBinary(jExe).then((v) => {
|
AssetGuard._validateJavaBinary(jExe, mcVersion).then((v) => {
|
||||||
loggerLanding.log('Java version meta', v)
|
loggerLanding.log('Java version meta', v)
|
||||||
if(v.valid){
|
if(v.valid){
|
||||||
dlAsync()
|
dlAsync()
|
||||||
} else {
|
} else {
|
||||||
asyncSystemScan()
|
asyncSystemScan(mcVersion)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -260,7 +261,13 @@ let scanAt
|
|||||||
|
|
||||||
let extractListener
|
let extractListener
|
||||||
|
|
||||||
function asyncSystemScan(launchAfter = true){
|
/**
|
||||||
|
* Asynchronously scan the system for valid Java installations.
|
||||||
|
*
|
||||||
|
* @param {string} mcVersion The Minecraft version we are scanning for.
|
||||||
|
* @param {boolean} launchAfter Whether we should begin to launch after scanning.
|
||||||
|
*/
|
||||||
|
function asyncSystemScan(mcVersion, launchAfter = true){
|
||||||
|
|
||||||
setLaunchDetails('Please wait..')
|
setLaunchDetails('Please wait..')
|
||||||
toggleLaunchArea(true)
|
toggleLaunchArea(true)
|
||||||
@ -424,7 +431,7 @@ function asyncSystemScan(launchAfter = true){
|
|||||||
|
|
||||||
// Begin system Java scan.
|
// Begin system Java scan.
|
||||||
setLaunchDetails('Checking system info..')
|
setLaunchDetails('Checking system info..')
|
||||||
sysAEx.send({task: 'execute', function: 'validateJava', argsArr: [ConfigManager.getLauncherDirectory()]})
|
sysAEx.send({task: 'execute', function: 'validateJava', argsArr: [ConfigManager.getLauncherDirectory(), mcVersion]})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,6 +237,8 @@ loginCancelButton.onclick = (e) => {
|
|||||||
loginCancelEnabled(false)
|
loginCancelEnabled(false)
|
||||||
if(loginViewCancelHandler != null){
|
if(loginViewCancelHandler != null){
|
||||||
loginViewCancelHandler()
|
loginViewCancelHandler()
|
||||||
|
loginUsername.value = ''
|
||||||
|
loginPassword.value = ''
|
||||||
loginViewCancelHandler = null
|
loginViewCancelHandler = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1107,7 +1107,11 @@ settingsJavaExecSel.onchange = (e) => {
|
|||||||
function populateJavaExecDetails(execPath){
|
function populateJavaExecDetails(execPath){
|
||||||
AssetGuard._validateJavaBinary(execPath).then(v => {
|
AssetGuard._validateJavaBinary(execPath).then(v => {
|
||||||
if(v.valid){
|
if(v.valid){
|
||||||
|
if(v.version.major < 9) {
|
||||||
settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major} Update ${v.version.update} (x${v.arch})`
|
settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major} Update ${v.version.update} (x${v.arch})`
|
||||||
|
} else {
|
||||||
|
settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major}.${v.version.minor}.${v.version.revision} (x${v.arch})`
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
settingsJavaExecDetails.innerHTML = 'Invalid Selection'
|
settingsJavaExecDetails.innerHTML = 'Invalid Selection'
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user