diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index 9404edba0..87fe2550e 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -386,6 +386,7 @@ simedtdlg.but.ttip.resettodefault = Reset the time step to its default value ( simedtdlg.border.SimExt = Simulation extensions simedtdlg.SimExt.desc = Simulation extensions 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 diff --git a/core/src/net/sf/openrocket/simulation/extension/AbstractSimulationExtension.java b/core/src/net/sf/openrocket/simulation/extension/AbstractSimulationExtension.java index 8261cee1d..fbed23a43 100644 --- a/core/src/net/sf/openrocket/simulation/extension/AbstractSimulationExtension.java +++ b/core/src/net/sf/openrocket/simulation/extension/AbstractSimulationExtension.java @@ -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} + *

+ * 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} + *

+ * 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} + *

+ * 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} + *

+ * By default, this method returns an empty list. */ @Override public List getFlightDataTypes() { @@ -74,7 +85,18 @@ public abstract class AbstractSimulationExtension extends AbstractChangeSource i } /** - * By default, returns a new object obtained by calling Object.clone(). + * {@inheritDoc} + *

+ * 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() { diff --git a/core/src/net/sf/openrocket/simulation/extension/SimulationExtension.java b/core/src/net/sf/openrocket/simulation/extension/SimulationExtension.java index 26fd6a137..f0b6c93c2 100644 --- a/core/src/net/sf/openrocket/simulation/extension/SimulationExtension.java +++ b/core/src/net/sf/openrocket/simulation/extension/SimulationExtension.java @@ -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 diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingExtension.java b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingExtension.java index 03785c036..d3d3380b3 100644 --- a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingExtension.java +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingExtension.java @@ -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(); diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java index 9ca6edcd6..5eab2dfa8 100644 --- a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java @@ -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 invoke(T def, String method, Object... args) throws SimulationException { + private T invoke(Class 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); diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingUtil.java b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingUtil.java new file mode 100644 index 000000000..ac205ab59 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingUtil.java @@ -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 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 getLanguages() { + List langs = new ArrayList(); + 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; + } + +} diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/TestScriptingUtil.java b/core/src/net/sf/openrocket/simulation/extension/impl/TestScriptingUtil.java new file mode 100644 index 000000000..bd20a5fa8 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/TestScriptingUtil.java @@ -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")); + } +} diff --git a/core/src/net/sf/openrocket/startup/Preferences.java b/core/src/net/sf/openrocket/startup/Preferences.java index 0f9c7f3e7..c72008bf6 100644 --- a/core/src/net/sf/openrocket/startup/Preferences.java +++ b/core/src/net/sf/openrocket/startup/Preferences.java @@ -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); + /* * ****************************************************************************************** */ diff --git a/core/src/net/sf/openrocket/util/ScriptingUtil.java b/core/src/net/sf/openrocket/util/ScriptingUtil.java deleted file mode 100644 index 35653255c..000000000 --- a/core/src/net/sf/openrocket/util/ScriptingUtil.java +++ /dev/null @@ -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 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 getLanguages() { - List langs = new ArrayList(); - 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(); - } -} diff --git a/core/test/net/sf/openrocket/ServicesForTesting.java b/core/test/net/sf/openrocket/ServicesForTesting.java index 2d6ec6743..f80300cd5 100644 --- a/core/test/net/sf/openrocket/ServicesForTesting.java +++ b/core/test/net/sf/openrocket/ServicesForTesting.java @@ -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; + } + } } diff --git a/core/test/net/sf/openrocket/startup/MockPreferences.java b/core/test/net/sf/openrocket/startup/MockPreferences.java new file mode 100644 index 000000000..eabfe18d9 --- /dev/null +++ b/core/test/net/sf/openrocket/startup/MockPreferences.java @@ -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 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 getComponentFavorites(Type type) { + throw new UnsupportedOperationException("Not yet implemented"); + } + +} diff --git a/core/test/net/sf/openrocket/util/TestScriptingUtil.java b/core/test/net/sf/openrocket/util/TestScriptingUtil.java deleted file mode 100644 index 1d657f507..000000000 --- a/core/test/net/sf/openrocket/util/TestScriptingUtil.java +++ /dev/null @@ -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")); - } - -} diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java index d91776808..a4b3587f3 100644 --- a/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java +++ b/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java @@ -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 configurators = Application.getInjector().getInstance(new Key>() { - }); - 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); - // //// Unable to instantiate listener due to exception:
- // setToolTipText("Unable to instantiate listener due to exception:
" + - // 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 configurators = Application.getInjector().getInstance(new Key>() { + }); + for (SwingSimulationExtensionConfigurator c : configurators) { + if (c.support(extension)) { + return c; + } + } + return null; + } } diff --git a/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java b/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java index 499620c9b..b4bd15cdf 100644 --- a/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java +++ b/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java @@ -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(); diff --git a/swing/src/net/sf/openrocket/simulation/extension/AbstractSwingSimulationExtensionConfigurator.java b/swing/src/net/sf/openrocket/simulation/extension/AbstractSwingSimulationExtensionConfigurator.java index 9c6af0ba8..8eb2ea1ae 100644 --- a/swing/src/net/sf/openrocket/simulation/extension/AbstractSwingSimulationExtensionConfigurator.java +++ b/swing/src/net/sf/openrocket/simulation/extension/AbstractSwingSimulationExtensionConfigurator.java @@ -57,6 +57,7 @@ public abstract class AbstractSwingSimulationExtensionConfigurator { + @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 = "";