Disable untrusted scripts on loading files
This commit is contained in:
parent
a39a3fce15
commit
a1f6782195
@ -386,6 +386,7 @@ simedtdlg.but.ttip.resettodefault = Reset the time step to its default value (
|
||||
simedtdlg.border.SimExt = Simulation extensions
|
||||
simedtdlg.SimExt.desc = <html><i>Simulation extensions</i> enable advanced features and custom functionality during flight simulations. You can for example do hardware-in-the-loop testing with them.
|
||||
simedtdlg.SimExt.noExtensions = No simulation extensions defined
|
||||
simedtdlg.SimExt.add = Add extension
|
||||
simedtdlg.lbl.Noflightdata = No flight data available.
|
||||
simedtdlg.lbl.runsimfirst = Please run the simulation first.
|
||||
simedtdlg.chart.Simflight = Simulated flight
|
||||
@ -408,6 +409,15 @@ SimulationExtension.javacode.className = Fully-qualified Java class name:
|
||||
SimulationExtension.scripting.name = {language} script
|
||||
SimulationExtension.scripting.desc = Extend OpenRocket simulations by custom scripts.
|
||||
SimulationExtension.scripting.language.label = Language:
|
||||
SimulationExtension.scripting.warning.disabled = Untrusted scripts have been disabled. You need to manually enable them.
|
||||
SimulationExtension.scripting.text.enabled = Enable script
|
||||
SimulationExtension.scripting.text.enabled.ttip = The script is run only when enabled.
|
||||
SimulationExtension.scripting.text.trusted = Trust this script on this computer
|
||||
SimulationExtension.scripting.text.trusted.msg = Untrusted scripts are disabled when loading the document
|
||||
SimulationExtension.scripting.text.trusted.clear = Clear trusted scripts
|
||||
SimulationExtension.scripting.text.trusted.clear.ttip = Clear the trusted status of all scripts on this computer
|
||||
SimulationExtension.scripting.text.trusted.cleared = All scripts are now untrusted on this computer.
|
||||
SimulationExtension.scripting.text.trusted.cleared.title = Cleared
|
||||
|
||||
SimulationEditDialog.btn.plot = Plot
|
||||
SimulationEditDialog.btn.export = Export
|
||||
|
@ -3,6 +3,9 @@ package net.sf.openrocket.simulation.extension;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import net.sf.openrocket.aerodynamics.WarningSet;
|
||||
import net.sf.openrocket.document.OpenRocketDocument;
|
||||
import net.sf.openrocket.document.Simulation;
|
||||
import net.sf.openrocket.l10n.Translator;
|
||||
import net.sf.openrocket.simulation.FlightDataType;
|
||||
import net.sf.openrocket.util.AbstractChangeSource;
|
||||
@ -42,7 +45,9 @@ public abstract class AbstractSimulationExtension extends AbstractChangeSource i
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, returns the canonical name of this class.
|
||||
* {@inheritDoc}
|
||||
* <p>
|
||||
* By default, this method returns the canonical name of this class.
|
||||
*/
|
||||
@Override
|
||||
public String getId() {
|
||||
@ -50,7 +55,9 @@ public abstract class AbstractSimulationExtension extends AbstractChangeSource i
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, returns the name provided to the constructor.
|
||||
* {@inheritDoc}
|
||||
* <p>
|
||||
* By default, this method returns the name provided to the constructor.
|
||||
*/
|
||||
@Override
|
||||
public String getName() {
|
||||
@ -58,7 +65,9 @@ public abstract class AbstractSimulationExtension extends AbstractChangeSource i
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, returns null.
|
||||
* {@inheritDoc}
|
||||
* <p>
|
||||
* By default, this method returns null.
|
||||
*/
|
||||
@Override
|
||||
public String getDescription() {
|
||||
@ -66,7 +75,9 @@ public abstract class AbstractSimulationExtension extends AbstractChangeSource i
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, returns an empty list.
|
||||
* {@inheritDoc}
|
||||
* <p>
|
||||
* By default, this method returns an empty list.
|
||||
*/
|
||||
@Override
|
||||
public List<FlightDataType> getFlightDataTypes() {
|
||||
@ -74,7 +85,18 @@ public abstract class AbstractSimulationExtension extends AbstractChangeSource i
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, returns a new object obtained by calling Object.clone().
|
||||
* {@inheritDoc}
|
||||
* <p>
|
||||
* By default, this method does nothing.
|
||||
*/
|
||||
@Override
|
||||
public void documentLoaded(OpenRocketDocument document, Simulation simulation, WarningSet warnings) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, returns a new object obtained by calling Object.clone() and
|
||||
* cloning the config object.
|
||||
*/
|
||||
@Override
|
||||
public SimulationExtension clone() {
|
||||
|
@ -2,6 +2,9 @@ package net.sf.openrocket.simulation.extension;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import net.sf.openrocket.aerodynamics.WarningSet;
|
||||
import net.sf.openrocket.document.OpenRocketDocument;
|
||||
import net.sf.openrocket.document.Simulation;
|
||||
import net.sf.openrocket.simulation.FlightDataType;
|
||||
import net.sf.openrocket.simulation.SimulationConditions;
|
||||
import net.sf.openrocket.simulation.exception.SimulationException;
|
||||
@ -35,6 +38,16 @@ public interface SimulationExtension {
|
||||
*/
|
||||
public String getDescription();
|
||||
|
||||
/**
|
||||
* Called once for each simulation this extension is attached to when loading a document.
|
||||
* This may perform necessary changes to the document at load time.
|
||||
*
|
||||
* @param document the loaded document
|
||||
* @param simulation the simulation this extension is attached to
|
||||
* @param warnings the document loading warnings
|
||||
*/
|
||||
public void documentLoaded(OpenRocketDocument document, Simulation simulation, WarningSet warnings);
|
||||
|
||||
/**
|
||||
* Initialize this simulation extension for running within a simulation.
|
||||
* This method is called before running a simulation. It can either modify
|
||||
|
@ -5,18 +5,30 @@ import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptEngineManager;
|
||||
import javax.script.ScriptException;
|
||||
|
||||
import net.sf.openrocket.aerodynamics.Warning;
|
||||
import net.sf.openrocket.aerodynamics.WarningSet;
|
||||
import net.sf.openrocket.document.OpenRocketDocument;
|
||||
import net.sf.openrocket.document.Simulation;
|
||||
import net.sf.openrocket.l10n.L10N;
|
||||
import net.sf.openrocket.simulation.SimulationConditions;
|
||||
import net.sf.openrocket.simulation.exception.SimulationException;
|
||||
import net.sf.openrocket.simulation.extension.AbstractSimulationExtension;
|
||||
import net.sf.openrocket.simulation.listeners.SimulationListener;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
public class ScriptingExtension extends AbstractSimulationExtension {
|
||||
|
||||
private static final String DEFAULT_LANGUAGE = "JavaScript";
|
||||
|
||||
@Inject
|
||||
private ScriptingUtil util;
|
||||
|
||||
|
||||
public ScriptingExtension() {
|
||||
setLanguage(DEFAULT_LANGUAGE);
|
||||
setScript("");
|
||||
setEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -31,9 +43,25 @@ public class ScriptingExtension extends AbstractSimulationExtension {
|
||||
return trans.get("SimulationExtension.scripting.desc");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void documentLoaded(OpenRocketDocument document, Simulation simulation, WarningSet warnings) {
|
||||
/*
|
||||
* Scripts that the user has not explicitly indicated as trusted are disabled
|
||||
* when loading from a file. This is to prevent trojans.
|
||||
*/
|
||||
if (isEnabled()) {
|
||||
if (!util.isTrustedScript(getLanguage(), getScript())) {
|
||||
setEnabled(false);
|
||||
warnings.add(Warning.fromString(trans.get("SimulationExtension.scripting.warning.disabled")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(SimulationConditions conditions) throws SimulationException {
|
||||
conditions.getSimulationListenerList().add(getListener());
|
||||
if (isEnabled()) {
|
||||
conditions.getSimulationListenerList().add(getListener());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -53,6 +81,14 @@ public class ScriptingExtension extends AbstractSimulationExtension {
|
||||
config.put("language", language);
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return config.getBoolean("enabled", false);
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
config.put("enabled", enabled);
|
||||
}
|
||||
|
||||
|
||||
SimulationListener getListener() throws SimulationException {
|
||||
ScriptEngineManager manager = new ScriptEngineManager();
|
||||
|
@ -18,6 +18,7 @@ import net.sf.openrocket.simulation.FlightEvent;
|
||||
import net.sf.openrocket.simulation.MassData;
|
||||
import net.sf.openrocket.simulation.SimulationStatus;
|
||||
import net.sf.openrocket.simulation.exception.SimulationException;
|
||||
import net.sf.openrocket.simulation.exception.SimulationListenerException;
|
||||
import net.sf.openrocket.simulation.listeners.SimulationComputationListener;
|
||||
import net.sf.openrocket.simulation.listeners.SimulationEventListener;
|
||||
import net.sf.openrocket.simulation.listeners.SimulationListener;
|
||||
@ -68,25 +69,25 @@ public class ScriptingSimulationListener implements SimulationListener, Simulati
|
||||
|
||||
@Override
|
||||
public void startSimulation(SimulationStatus status) throws SimulationException {
|
||||
invoke(null, "startSimulation", status);
|
||||
invoke(Void.class, null, "startSimulation", status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endSimulation(SimulationStatus status, SimulationException exception) {
|
||||
try {
|
||||
invoke(null, "endSimulation", status, exception);
|
||||
invoke(Void.class, null, "endSimulation", status, exception);
|
||||
} catch (SimulationException e) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preStep(SimulationStatus status) throws SimulationException {
|
||||
return invoke(true, "preStep", status);
|
||||
return invoke(Boolean.class, true, "preStep", status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postStep(SimulationStatus status) throws SimulationException {
|
||||
invoke(null, "postStep", status);
|
||||
invoke(Void.class, null, "postStep", status);
|
||||
}
|
||||
|
||||
|
||||
@ -95,22 +96,22 @@ public class ScriptingSimulationListener implements SimulationListener, Simulati
|
||||
|
||||
@Override
|
||||
public boolean addFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException {
|
||||
return invoke(true, "addFlightEvent", status, event);
|
||||
return invoke(Boolean.class, true, "addFlightEvent", status, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException {
|
||||
return invoke(true, "handleFlightEvent", status, event);
|
||||
return invoke(Boolean.class, true, "handleFlightEvent", status, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean motorIgnition(SimulationStatus status, MotorId motorId, MotorMount mount, MotorInstance instance) throws SimulationException {
|
||||
return invoke(true, "motorIgnition", status, motorId, mount, instance);
|
||||
return invoke(Boolean.class, true, "motorIgnition", status, motorId, mount, instance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean recoveryDeviceDeployment(SimulationStatus status, RecoveryDevice recoveryDevice) throws SimulationException {
|
||||
return invoke(true, "recoveryDeviceDeployment", status, recoveryDevice);
|
||||
return invoke(Boolean.class, true, "recoveryDeviceDeployment", status, recoveryDevice);
|
||||
}
|
||||
|
||||
|
||||
@ -119,90 +120,99 @@ public class ScriptingSimulationListener implements SimulationListener, Simulati
|
||||
|
||||
@Override
|
||||
public AccelerationData preAccelerationCalculation(SimulationStatus status) throws SimulationException {
|
||||
return invoke(null, "preAccelerationCalculation", status);
|
||||
return invoke(AccelerationData.class, null, "preAccelerationCalculation", status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AerodynamicForces preAerodynamicCalculation(SimulationStatus status) throws SimulationException {
|
||||
return invoke(null, "preAerodynamicCalculation", status);
|
||||
return invoke(AerodynamicForces.class, null, "preAerodynamicCalculation", status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AtmosphericConditions preAtmosphericModel(SimulationStatus status) throws SimulationException {
|
||||
return invoke(null, "preAtmosphericModel", status);
|
||||
return invoke(AtmosphericConditions.class, null, "preAtmosphericModel", status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlightConditions preFlightConditions(SimulationStatus status) throws SimulationException {
|
||||
return invoke(null, "preFlightConditions", status);
|
||||
return invoke(FlightConditions.class, null, "preFlightConditions", status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double preGravityModel(SimulationStatus status) throws SimulationException {
|
||||
return invoke(Double.NaN, "preGravityModel", status);
|
||||
return invoke(Double.class, Double.NaN, "preGravityModel", status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MassData preMassCalculation(SimulationStatus status) throws SimulationException {
|
||||
return invoke(null, "preMassCalculation", status);
|
||||
return invoke(MassData.class, null, "preMassCalculation", status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double preSimpleThrustCalculation(SimulationStatus status) throws SimulationException {
|
||||
return invoke(Double.NaN, "preSimpleThrustCalculation", status);
|
||||
return invoke(Double.class, Double.NaN, "preSimpleThrustCalculation", status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Coordinate preWindModel(SimulationStatus status) throws SimulationException {
|
||||
return invoke(null, "preWindModel", status);
|
||||
return invoke(Coordinate.class, null, "preWindModel", status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccelerationData postAccelerationCalculation(SimulationStatus status, AccelerationData acceleration) throws SimulationException {
|
||||
return invoke(null, "postAccelerationCalculation", status, acceleration);
|
||||
return invoke(AccelerationData.class, null, "postAccelerationCalculation", status, acceleration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AerodynamicForces postAerodynamicCalculation(SimulationStatus status, AerodynamicForces forces) throws SimulationException {
|
||||
return invoke(null, "postAerodynamicCalculation", status, forces);
|
||||
return invoke(AerodynamicForces.class, null, "postAerodynamicCalculation", status, forces);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AtmosphericConditions postAtmosphericModel(SimulationStatus status, AtmosphericConditions atmosphericConditions) throws SimulationException {
|
||||
return invoke(null, "postAtmosphericModel", status, atmosphericConditions);
|
||||
return invoke(AtmosphericConditions.class, null, "postAtmosphericModel", status, atmosphericConditions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) throws SimulationException {
|
||||
return invoke(null, "postFlightConditions", status, flightConditions);
|
||||
return invoke(FlightConditions.class, null, "postFlightConditions", status, flightConditions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double postGravityModel(SimulationStatus status, double gravity) throws SimulationException {
|
||||
return invoke(Double.NaN, "postGravityModel", status, gravity);
|
||||
return invoke(Double.class, Double.NaN, "postGravityModel", status, gravity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MassData postMassCalculation(SimulationStatus status, MassData massData) throws SimulationException {
|
||||
return invoke(null, "postMassCalculation", status, massData);
|
||||
return invoke(MassData.class, null, "postMassCalculation", status, massData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double postSimpleThrustCalculation(SimulationStatus status, double thrust) throws SimulationException {
|
||||
return invoke(Double.NaN, "postSimpleThrustCalculation", status, thrust);
|
||||
return invoke(Double.class, Double.NaN, "postSimpleThrustCalculation", status, thrust);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Coordinate postWindModel(SimulationStatus status, Coordinate wind) throws SimulationException {
|
||||
return invoke(null, "postWindModel", status, wind);
|
||||
return invoke(Coordinate.class, null, "postWindModel", status, wind);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T invoke(T def, String method, Object... args) throws SimulationException {
|
||||
private <T> T invoke(Class<T> retType, T def, String method, Object... args) throws SimulationException {
|
||||
try {
|
||||
if (!missing.contains(method)) {
|
||||
return (T) invocable.invokeFunction(method, args);
|
||||
Object o = invocable.invokeFunction(method, args);
|
||||
if (o == null) {
|
||||
// Use default/null if function returns nothing
|
||||
return def;
|
||||
} else if (!o.getClass().equals(retType)) {
|
||||
throw new SimulationListenerException("Custom script function " + method + " returned type " +
|
||||
o.getClass().getSimpleName() + ", expected " + retType.getSimpleName());
|
||||
} else {
|
||||
return (T) o;
|
||||
}
|
||||
}
|
||||
} catch (NoSuchMethodException e) {
|
||||
missing.add(method);
|
||||
|
@ -0,0 +1,150 @@
|
||||
package net.sf.openrocket.simulation.extension.impl;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.prefs.BackingStoreException;
|
||||
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptEngineFactory;
|
||||
import javax.script.ScriptEngineManager;
|
||||
|
||||
import net.sf.openrocket.startup.Preferences;
|
||||
import net.sf.openrocket.util.ArrayList;
|
||||
import net.sf.openrocket.util.BugException;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
/**
|
||||
* Utility class used by the scripting extension and its configurator.
|
||||
*/
|
||||
public class ScriptingUtil {
|
||||
|
||||
static final String NODE_ID = ScriptingExtension.class.getCanonicalName();
|
||||
|
||||
/** The name to be chosen from a list of alternatives. If not found, will use the default name. */
|
||||
private static final List<String> PREFERRED_LANGUAGE_NAMES = Arrays.asList("JavaScript");
|
||||
|
||||
@Inject
|
||||
Preferences prefs;
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Return the preferred internal language name based on a script language name.
|
||||
*
|
||||
* @return the preferred language name, or null if the language is not supported.
|
||||
*/
|
||||
public String getLanguage(String language) {
|
||||
if (language == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ScriptEngineManager manager = new ScriptEngineManager();
|
||||
ScriptEngine engine = manager.getEngineByName(language);
|
||||
if (engine == null) {
|
||||
return null;
|
||||
}
|
||||
return getLanguage(engine.getFactory());
|
||||
}
|
||||
|
||||
|
||||
public List<String> getLanguages() {
|
||||
List<String> langs = new ArrayList<String>();
|
||||
ScriptEngineManager manager = new ScriptEngineManager();
|
||||
for (ScriptEngineFactory factory : manager.getEngineFactories()) {
|
||||
langs.add(getLanguage(factory));
|
||||
}
|
||||
return langs;
|
||||
}
|
||||
|
||||
|
||||
private String getLanguage(ScriptEngineFactory factory) {
|
||||
for (String name : factory.getNames()) {
|
||||
if (PREFERRED_LANGUAGE_NAMES.contains(name)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
return factory.getLanguageName();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Test whether the user has indicated this script to be trusted,
|
||||
* or if it is an internally trusted script.
|
||||
*/
|
||||
public boolean isTrustedScript(String language, String script) {
|
||||
if (language == null || script == null) {
|
||||
return false;
|
||||
}
|
||||
script = normalize(script);
|
||||
if (script.length() == 0) {
|
||||
return true;
|
||||
}
|
||||
String hash = hash(language, script);
|
||||
return prefs.getNode(NODE_ID).getBoolean(hash, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a script as trusted.
|
||||
*/
|
||||
public void setTrustedScript(String language, String script, boolean trusted) {
|
||||
script = normalize(script);
|
||||
String hash = hash(language, script);
|
||||
if (trusted) {
|
||||
prefs.getNode(NODE_ID).putBoolean(hash, true);
|
||||
} else {
|
||||
prefs.getNode(NODE_ID).remove(hash);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all trusted scripts.
|
||||
*/
|
||||
public void clearTrustedScripts() {
|
||||
try {
|
||||
prefs.getNode(NODE_ID).clear();
|
||||
} catch (BackingStoreException e) {
|
||||
throw new BugException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static String normalize(String script) {
|
||||
return script.replaceAll("\r", "").trim();
|
||||
}
|
||||
|
||||
static String hash(String language, String script) {
|
||||
/*
|
||||
* NOTE: Hash length must be max 80 chars, the max length of a key in a Properties object.
|
||||
*/
|
||||
|
||||
String output;
|
||||
MessageDigest digest;
|
||||
|
||||
try {
|
||||
digest = MessageDigest.getInstance("SHA-256");
|
||||
digest.update(language.getBytes("UTF-8"));
|
||||
digest.update((byte) '|');
|
||||
byte[] hash = digest.digest(script.getBytes("UTF-8"));
|
||||
BigInteger bigInt = new BigInteger(1, hash);
|
||||
output = bigInt.toString(16);
|
||||
while (output.length() < 64) {
|
||||
output = "0" + output;
|
||||
}
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new BugException("JRE does not support SHA-256 hash algorithm", e);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new BugException(e);
|
||||
}
|
||||
|
||||
return digest.getAlgorithm() + ":" + output;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
package net.sf.openrocket.simulation.extension.impl;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import net.sf.openrocket.startup.MockPreferences;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class TestScriptingUtil {
|
||||
|
||||
private static final String HASH_JavaScript_foobar = "SHA-256:8f06133e0235d239355b5ca8ca0b43dece803c29b2a563222519d982abd3fc43";
|
||||
|
||||
private ScriptingUtil util;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
util = new ScriptingUtil();
|
||||
util.prefs = new MockPreferences();
|
||||
}
|
||||
|
||||
/*
|
||||
* Note: This class assumes that the JRE supports JavaScript scripting.
|
||||
*/
|
||||
|
||||
@Test
|
||||
public void testGetLanguage() {
|
||||
assertEquals(null, util.getLanguage(null));
|
||||
assertEquals(null, util.getLanguage(""));
|
||||
assertEquals(null, util.getLanguage("foobar"));
|
||||
assertEquals("JavaScript", util.getLanguage("JavaScript"));
|
||||
assertEquals("JavaScript", util.getLanguage("javascript"));
|
||||
assertEquals("JavaScript", util.getLanguage("ECMAScript"));
|
||||
assertEquals("JavaScript", util.getLanguage("js"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testGetLanguages() {
|
||||
assertTrue(util.getLanguages().size() >= 1);
|
||||
assertTrue(util.getLanguages().contains("JavaScript"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsTrustedScript() {
|
||||
util.setTrustedScript("JavaScript", "foobar", true);
|
||||
assertTrue(util.isTrustedScript("JavaScript", "foobar"));
|
||||
assertTrue(util.isTrustedScript("JavaScript", " \n foobar \n\t\r"));
|
||||
assertFalse(util.isTrustedScript("JavaScript", "foo\nbar"));
|
||||
assertFalse(util.isTrustedScript("Javascript", "foobar"));
|
||||
|
||||
// Empty script is always considered trusted
|
||||
assertFalse(util.isTrustedScript("foo", null));
|
||||
assertTrue(util.isTrustedScript("foo", ""));
|
||||
assertTrue(util.isTrustedScript("foo", " \n\r\t "));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetTrustedScript() {
|
||||
util.setTrustedScript("JavaScript", " \n foobar \n\r ", true);
|
||||
assertTrue(util.prefs.getNode(ScriptingUtil.NODE_ID).getBoolean(HASH_JavaScript_foobar, false));
|
||||
util.setTrustedScript("JavaScript", " foobar ", false);
|
||||
assertTrue(util.prefs.getNode(ScriptingUtil.NODE_ID).getBoolean(HASH_JavaScript_foobar, true));
|
||||
assertFalse(util.prefs.getNode(ScriptingUtil.NODE_ID).getBoolean(HASH_JavaScript_foobar, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClearTrustedScripts() {
|
||||
util.setTrustedScript("JavaScript", "foobar", true);
|
||||
assertTrue(util.isTrustedScript("JavaScript", "foobar"));
|
||||
util.clearTrustedScripts();
|
||||
assertFalse(util.isTrustedScript("JavaScript", "foobar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalize() {
|
||||
assertEquals("foo", ScriptingUtil.normalize("foo"));
|
||||
assertEquals("foo bar", ScriptingUtil.normalize(" \n\r\t foo \r bar \n\t\r "));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHash() {
|
||||
assertEquals("SHA-256:12e6a78889b96a16d305b8e4af81119545f89eccba5fb37cc3a1ec2c53eab514", ScriptingUtil.hash("JS", ""));
|
||||
assertEquals("SHA-256:000753e5deb2d8fa80e602ca03bcdb8e12a6b14b2b4a4d0abecdc976ad26e3ef", ScriptingUtil.hash("foo", "1165"));
|
||||
assertEquals(HASH_JavaScript_foobar, ScriptingUtil.hash("JavaScript", "foobar"));
|
||||
}
|
||||
}
|
@ -92,6 +92,8 @@ public abstract class Preferences {
|
||||
|
||||
public abstract void putString(String directory, String key, String value);
|
||||
|
||||
public abstract java.util.prefs.Preferences getNode(String nodeName);
|
||||
|
||||
/*
|
||||
* ******************************************************************************************
|
||||
*/
|
||||
|
@ -1,53 +0,0 @@
|
||||
package net.sf.openrocket.util;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptEngineFactory;
|
||||
import javax.script.ScriptEngineManager;
|
||||
|
||||
public class ScriptingUtil {
|
||||
|
||||
/** The name to be chosen from a list of alternatives. If not found, will use the default name. */
|
||||
private static final List<String> PREFERRED_LANGUAGE_NAMES = Arrays.asList("JavaScript");
|
||||
|
||||
/**
|
||||
* Return the preferred internal language name based on a script language name.
|
||||
*
|
||||
* @return the preferred language name, or null if the language is not supported.
|
||||
*/
|
||||
public static String getLanguage(String language) {
|
||||
if (language == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ScriptEngineManager manager = new ScriptEngineManager();
|
||||
ScriptEngine engine = manager.getEngineByName(language);
|
||||
if (engine == null) {
|
||||
return null;
|
||||
}
|
||||
return getLanguage(engine.getFactory());
|
||||
}
|
||||
|
||||
|
||||
public static List<String> getLanguages() {
|
||||
List<String> langs = new ArrayList<String>();
|
||||
ScriptEngineManager manager = new ScriptEngineManager();
|
||||
for (ScriptEngineFactory factory : manager.getEngineFactories()) {
|
||||
langs.add(getLanguage(factory));
|
||||
}
|
||||
return langs;
|
||||
}
|
||||
|
||||
|
||||
private static String getLanguage(ScriptEngineFactory factory) {
|
||||
for (String name : factory.getNames()) {
|
||||
if (PREFERRED_LANGUAGE_NAMES.contains(name)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
return factory.getLanguageName();
|
||||
}
|
||||
}
|
@ -151,5 +151,11 @@ public class ServicesForTesting extends AbstractModule {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.prefs.Preferences getNode(String nodeName) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
108
core/test/net/sf/openrocket/startup/MockPreferences.java
Normal file
108
core/test/net/sf/openrocket/startup/MockPreferences.java
Normal file
@ -0,0 +1,108 @@
|
||||
package net.sf.openrocket.startup;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.prefs.BackingStoreException;
|
||||
|
||||
import net.sf.openrocket.material.Material;
|
||||
import net.sf.openrocket.preset.ComponentPreset;
|
||||
import net.sf.openrocket.preset.ComponentPreset.Type;
|
||||
import net.sf.openrocket.util.BugException;
|
||||
|
||||
public class MockPreferences extends Preferences {
|
||||
|
||||
private final String NODENAME = "OpenRocket-test-mock";
|
||||
private final java.util.prefs.Preferences NODE;
|
||||
|
||||
public MockPreferences() {
|
||||
java.util.prefs.Preferences root = java.util.prefs.Preferences.userRoot();
|
||||
try {
|
||||
if (root.nodeExists(NODENAME)) {
|
||||
root.node(NODENAME).removeNode();
|
||||
}
|
||||
} catch (BackingStoreException e) {
|
||||
throw new BugException("Unable to clear preference node", e);
|
||||
}
|
||||
NODE = root.node(NODENAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean def) {
|
||||
return NODE.getBoolean(key, def);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putBoolean(String key, boolean value) {
|
||||
NODE.putBoolean(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(String key, int def) {
|
||||
return NODE.getInt(key, def);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putInt(String key, int value) {
|
||||
NODE.putInt(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(String key, double def) {
|
||||
return NODE.getDouble(key, def);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putDouble(String key, double value) {
|
||||
NODE.putDouble(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(String key, String def) {
|
||||
return NODE.get(key, def);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putString(String key, String value) {
|
||||
NODE.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(String directory, String key, String def) {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putString(String directory, String key, String value) {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.prefs.Preferences getNode(String nodeName) {
|
||||
return NODE.node(nodeName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addUserMaterial(Material m) {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Material> getUserMaterials() {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUserMaterial(Material m) {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setComponentFavorite(ComponentPreset preset, Type type, boolean favorite) {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getComponentFavorites(Type type) {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
}
|
||||
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package net.sf.openrocket.util;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class TestScriptingUtil {
|
||||
|
||||
/*
|
||||
* Note: This class assumes that the JRE supports JavaScript scripting.
|
||||
*/
|
||||
|
||||
@Test
|
||||
public void testGetLanguage() {
|
||||
assertEquals(null, ScriptingUtil.getLanguage(null));
|
||||
assertEquals(null, ScriptingUtil.getLanguage(""));
|
||||
assertEquals(null, ScriptingUtil.getLanguage("foobar"));
|
||||
assertEquals("JavaScript", ScriptingUtil.getLanguage("JavaScript"));
|
||||
assertEquals("JavaScript", ScriptingUtil.getLanguage("javascript"));
|
||||
assertEquals("JavaScript", ScriptingUtil.getLanguage("ECMAScript"));
|
||||
assertEquals("JavaScript", ScriptingUtil.getLanguage("js"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testGetLanguages() {
|
||||
assertTrue(ScriptingUtil.getLanguages().size() >= 1);
|
||||
assertTrue(ScriptingUtil.getLanguages().contains("JavaScript"));
|
||||
}
|
||||
|
||||
}
|
@ -186,7 +186,7 @@ class SimulationOptionsPanel extends JPanel {
|
||||
sub.add(desc, "aligny 0, hmin 100lp, growx, wrap para");
|
||||
|
||||
|
||||
final JButton addExtension = new JButton("Add extension");
|
||||
final JButton addExtension = new JButton(trans.get("simedtdlg.SimExt.add"));
|
||||
final JPopupMenu menu = getExtensionMenu();
|
||||
addExtension.addActionListener(new ActionListener() {
|
||||
public void actionPerformed(ActionEvent ev) {
|
||||
@ -223,6 +223,10 @@ class SimulationOptionsPanel extends JPanel {
|
||||
SimulationExtension e = provider.getInstance(id);
|
||||
simulation.getSimulationExtensions().add(e);
|
||||
updateCurrentExtensions();
|
||||
SwingSimulationExtensionConfigurator configurator = findConfigurator(e);
|
||||
if (configurator != null) {
|
||||
configurator.configure(e, simulation, SwingUtilities.windowForComponent(SimulationOptionsPanel.this));
|
||||
}
|
||||
}
|
||||
});
|
||||
menu.add(item);
|
||||
@ -344,86 +348,17 @@ class SimulationOptionsPanel extends JPanel {
|
||||
this.add(button, "right");
|
||||
|
||||
}
|
||||
|
||||
private SwingSimulationExtensionConfigurator findConfigurator(SimulationExtension extension) {
|
||||
Set<SwingSimulationExtensionConfigurator> configurators = Application.getInjector().getInstance(new Key<Set<SwingSimulationExtensionConfigurator>>() {
|
||||
});
|
||||
for (SwingSimulationExtensionConfigurator c : configurators) {
|
||||
if (c.support(extension)) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
// private class ExtensionListModel extends AbstractListModel {
|
||||
// @Override
|
||||
// public SimulationExtensionConfiguration getElementAt(int index) {
|
||||
// if (index < 0 || index >= getSize())
|
||||
// return null;
|
||||
// return simulation.getSimulationExtensions().get(index);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public int getSize() {
|
||||
// return simulation.getSimulationExtensions().size();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// private class ExtensionCellRenderer extends JPanel implements ListCellRenderer {
|
||||
// private JLabel label;
|
||||
//
|
||||
// public ExtensionCellRenderer() {
|
||||
// super(new MigLayout("fill"));
|
||||
// label = new JLabel();
|
||||
//
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public Component getListCellRendererComponent(JList list, Object value,
|
||||
// int index, boolean isSelected, boolean cellHasFocus) {
|
||||
// SimulationExtensionConfiguration config = (SimulationExtensionConfiguration) value;
|
||||
//
|
||||
//
|
||||
//
|
||||
// String s = value.toString();
|
||||
// setText(s);
|
||||
//
|
||||
// // Attempt instantiating, catch any exceptions
|
||||
// Exception ex = null;
|
||||
// try {
|
||||
// Class<?> c = Class.forName(s);
|
||||
// @SuppressWarnings("unused")
|
||||
// SimulationListener l = (SimulationListener) c.newInstance();
|
||||
// } catch (Exception e) {
|
||||
// ex = e;
|
||||
// }
|
||||
//
|
||||
// if (ex == null) {
|
||||
// setIcon(Icons.SIMULATION_LISTENER_OK);
|
||||
// //// Listener instantiated successfully.
|
||||
// setToolTipText("Listener instantiated successfully.");
|
||||
// } else {
|
||||
// setIcon(Icons.SIMULATION_LISTENER_ERROR);
|
||||
// //// <html>Unable to instantiate listener due to exception:<br>
|
||||
// setToolTipText("<html>Unable to instantiate listener due to exception:<br>" +
|
||||
// ex.toString());
|
||||
// }
|
||||
//
|
||||
// if (isSelected) {
|
||||
// setBackground(list.getSelectionBackground());
|
||||
// setForeground(list.getSelectionForeground());
|
||||
// } else {
|
||||
// setBackground(list.getBackground());
|
||||
// setForeground(list.getForeground());
|
||||
// }
|
||||
// setOpaque(true);
|
||||
// return this;
|
||||
// }
|
||||
// }
|
||||
private SwingSimulationExtensionConfigurator findConfigurator(SimulationExtension extension) {
|
||||
Set<SwingSimulationExtensionConfigurator> configurators = Application.getInjector().getInstance(new Key<Set<SwingSimulationExtensionConfigurator>>() {
|
||||
});
|
||||
for (SwingSimulationExtensionConfigurator c : configurators) {
|
||||
if (c.support(extension)) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -45,8 +45,8 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences {
|
||||
for (String lang : new String[] { "en", "de", "es", "fr", "it", "ru", "cs", "pl", "ja", "pt", "tr" }) {
|
||||
list.add(new Locale(lang));
|
||||
}
|
||||
list.add(new Locale("zh","CN"));
|
||||
list.add(new Locale("uk","UA"));
|
||||
list.add(new Locale("zh", "CN"));
|
||||
list.add(new Locale("uk", "UA"));
|
||||
SUPPORTED_LOCALES = Collections.unmodifiableList(list);
|
||||
}
|
||||
|
||||
@ -200,6 +200,7 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences {
|
||||
* @param nodeName the node name
|
||||
* @return the preferences object for that node
|
||||
*/
|
||||
@Override
|
||||
public Preferences getNode(String nodeName) {
|
||||
return PREFNODE.node(nodeName);
|
||||
}
|
||||
@ -418,11 +419,11 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences {
|
||||
public boolean computeFlightInBackground() {
|
||||
return PREFNODE.getBoolean("backgroundFlight", true);
|
||||
}
|
||||
|
||||
|
||||
public void setComputeFlightInBackground(boolean b) {
|
||||
PREFNODE.putBoolean("backgroundFlight", b);
|
||||
}
|
||||
|
||||
|
||||
public Simulation getBackgroundSimulation(Rocket rocket) {
|
||||
Simulation s = new Simulation(rocket);
|
||||
SimulationOptions cond = s.getOptions();
|
||||
|
@ -57,6 +57,7 @@ public abstract class AbstractSwingSimulationExtensionConfigurator<E extends Sim
|
||||
dialog.add(panel);
|
||||
GUIUtil.setDisposableDialogOptions(dialog, close);
|
||||
dialog.setVisible(true);
|
||||
close();
|
||||
GUIUtil.setNullModels(dialog);
|
||||
dialog = null;
|
||||
}
|
||||
@ -75,6 +76,13 @@ public abstract class AbstractSwingSimulationExtensionConfigurator<E extends Sim
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the default dialog is closed. By default does nothing.
|
||||
*/
|
||||
protected void close() {
|
||||
|
||||
}
|
||||
|
||||
protected abstract JComponent getConfigurationComponent(E extension, Simulation simulation, JPanel panel);
|
||||
|
||||
}
|
||||
|
@ -9,27 +9,36 @@ import java.util.Set;
|
||||
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptEngineManager;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JCheckBox;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
|
||||
import net.sf.openrocket.document.Simulation;
|
||||
import net.sf.openrocket.gui.adaptors.BooleanModel;
|
||||
import net.sf.openrocket.gui.components.StyledLabel;
|
||||
import net.sf.openrocket.gui.components.StyledLabel.Style;
|
||||
import net.sf.openrocket.plugin.Plugin;
|
||||
import net.sf.openrocket.simulation.extension.AbstractSwingSimulationExtensionConfigurator;
|
||||
import net.sf.openrocket.util.ScriptingUtil;
|
||||
|
||||
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
|
||||
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
|
||||
import org.fife.ui.rsyntaxtextarea.TokenMakerFactory;
|
||||
import org.fife.ui.rtextarea.RTextScrollPane;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
@Plugin
|
||||
public class ScriptingConfigurator extends AbstractSwingSimulationExtensionConfigurator<ScriptingExtension> {
|
||||
|
||||
@Inject
|
||||
private ScriptingUtil util;
|
||||
|
||||
private JComboBox languageSelector;
|
||||
private RSyntaxTextArea text;
|
||||
private JCheckBox trusted;
|
||||
|
||||
private ScriptingExtension extension;
|
||||
private Simulation simulation;
|
||||
@ -43,9 +52,9 @@ public class ScriptingConfigurator extends AbstractSwingSimulationExtensionConfi
|
||||
this.extension = extension;
|
||||
this.simulation = simulation;
|
||||
|
||||
panel.add(new StyledLabel(trans.get("SimulationExtension.scripting.language.label"), Style.BOLD), "");
|
||||
panel.add(new StyledLabel(trans.get("SimulationExtension.scripting.language.label"), Style.BOLD), "spanx, split");
|
||||
|
||||
String[] languages = ScriptingUtil.getLanguages().toArray(new String[0]);
|
||||
String[] languages = util.getLanguages().toArray(new String[0]);
|
||||
languageSelector = new JComboBox(languages);
|
||||
languageSelector.setEditable(false);
|
||||
languageSelector.addActionListener(new ActionListener() {
|
||||
@ -79,13 +88,48 @@ public class ScriptingConfigurator extends AbstractSwingSimulationExtensionConfi
|
||||
});
|
||||
|
||||
RTextScrollPane scroll = new RTextScrollPane(text);
|
||||
panel.add(scroll, "spanx, grow");
|
||||
panel.add(scroll, "spanx, grow, wrap para");
|
||||
|
||||
setLanguage(ScriptingUtil.getLanguage(extension.getLanguage()));
|
||||
|
||||
BooleanModel enabled = new BooleanModel(extension, "Enabled");
|
||||
JCheckBox check = new JCheckBox(enabled);
|
||||
check.setText(trans.get("SimulationExtension.scripting.text.enabled"));
|
||||
check.setToolTipText(trans.get("SimulationExtension.scripting.text.enabled.ttip"));
|
||||
panel.add(check, "spanx, wrap rel");
|
||||
|
||||
trusted = new JCheckBox(trans.get("SimulationExtension.scripting.text.trusted"));
|
||||
trusted.setSelected(util.isTrustedScript(extension.getLanguage(), extension.getScript()));
|
||||
panel.add(trusted, "spanx, split");
|
||||
|
||||
panel.add(new JPanel(), "growx");
|
||||
|
||||
JButton button = new JButton(trans.get("SimulationExtension.scripting.text.trusted.clear"));
|
||||
button.setToolTipText(trans.get("SimulationExtension.scripting.text.trusted.clear.ttip"));
|
||||
button.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
util.clearTrustedScripts();
|
||||
JOptionPane.showMessageDialog(getDialog(), trans.get("SimulationExtension.scripting.text.trusted.cleared"),
|
||||
trans.get("SimulationExtension.scripting.text.trusted.cleared.title"), JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
});
|
||||
panel.add(button, "wrap rel");
|
||||
|
||||
|
||||
StyledLabel label = new StyledLabel(trans.get("SimulationExtension.scripting.text.trusted.msg"), -1, Style.ITALIC);
|
||||
panel.add(label);
|
||||
|
||||
setLanguage(util.getLanguage(extension.getLanguage()));
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void close() {
|
||||
util.setTrustedScript(extension.getLanguage(), extension.getScript(), trusted.isSelected());
|
||||
}
|
||||
|
||||
|
||||
private void setLanguage(String language) {
|
||||
if (language == null) {
|
||||
language = "";
|
||||
|
Loading…
x
Reference in New Issue
Block a user