/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2012 The ZAP development team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.zaproxy.zap.extension.script;
import java.awt.EventQueue;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.InvalidParameterException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.swing.ImageIcon;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.table.AbstractTableModel;
import org.apache.log4j.Logger;
import org.jdesktop.swingx.JXTable;
import org.parosproxy.paros.CommandLine;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.control.Control.Mode;
import org.parosproxy.paros.extension.CommandLineArgument;
import org.parosproxy.paros.extension.CommandLineListener;
import org.parosproxy.paros.extension.ExtensionAdaptor;
import org.parosproxy.paros.extension.ExtensionHook;
import org.parosproxy.paros.extension.SessionChangedListener;
import org.parosproxy.paros.model.Model;
import org.parosproxy.paros.model.Session;
import org.parosproxy.paros.network.HttpMessage;
import org.parosproxy.paros.network.HttpSender;
import org.parosproxy.paros.view.View;
import org.zaproxy.zap.ZAP;
import org.zaproxy.zap.control.ExtensionFactory;
public class ExtensionScript extends ExtensionAdaptor implements CommandLineListener {
public static final int EXTENSION_ORDER = 60;
public static final String NAME = "ExtensionScript";
public static final ImageIcon ICON = new ImageIcon(ZAP.class.getResource("/resource/icon/16/059.png")); // Script icon
public static final String SCRIPTS_DIR = "scripts";
public static final String TEMPLATES_DIR = SCRIPTS_DIR + File.separator + "templates";
private static final String LANG_ENGINE_SEP = " : ";
protected static final String SCRIPT_CONSOLE_HOME_PAGE = "https://github.com/zaproxy/zaproxy/wiki/ScriptConsole";
protected static final String SCRIPT_NAME_ATT = "zap.script.name";
public static final String TYPE_HTTP_SENDER = "httpsender";
public static final String TYPE_PROXY = "proxy";
public static final String TYPE_STANDALONE = "standalone";
public static final String TYPE_TARGETED = "targeted";
private static final ImageIcon HTTP_SENDER_ICON =
new ImageIcon(ZAP.class.getResource("/resource/icon/16/script-httpsender.png"));
private static final ImageIcon PROXY_ICON =
new ImageIcon(ZAP.class.getResource("/resource/icon/16/script-proxy.png"));
private static final ImageIcon STANDALONE_ICON =
new ImageIcon(ZAP.class.getResource("/resource/icon/16/script-standalone.png"));
private static final ImageIcon TARGETED_ICON =
new ImageIcon(ZAP.class.getResource("/resource/icon/16/script-targeted.png"));
private ScriptEngineManager mgr = new ScriptEngineManager();
private ScriptParam scriptParam = null;
private OptionsScriptPanel optionsScriptPanel = null;
private ScriptTreeModel treeModel = null;
private List <ScriptEngineWrapper> engineWrappers = new ArrayList<>();
private Map<String, ScriptType> typeMap = new HashMap<>();
private ProxyListenerScript proxyListener = null;
private HttpSenderScriptListener httpSenderScriptListener;
private List<ScriptEventListener> listeners = new ArrayList<>();
private MultipleWriters writers = new MultipleWriters();
private ScriptUI scriptUI = null;
private CommandLineArgument[] arguments = new CommandLineArgument[1];
private static final int ARG_SCRIPT_IDX = 0;
private static final Logger logger = Logger.getLogger(ExtensionScript.class);
/**
* Flag that indicates if the script templates should be loaded when a new script type is registered.
* <p>
* This is to prevent loading templates of already installed scripts (ones that are registered during ZAP initialisation)
* twice, while allowing to load the templates of scripts registered after initialisation (e.g. from installed add-ons).
*
* @since 2.4.0
* @see #registerScriptType(ScriptType)
* @see #optionsLoaded()
*/
private boolean shouldLoadTemplatesOnScriptTypeRegistration;
public ExtensionScript() {
super(NAME);
this.setOrder(EXTENSION_ORDER);
ScriptEngine se = mgr.getEngineByName("ECMAScript");
if (se != null) {
this.registerScriptEngineWrapper(new JavascriptEngineWrapper(se));
} else {
logger.error("No Javascript/ECMAScript engine found");
}
}
@Override
public void hook(ExtensionHook extensionHook) {
super.hook(extensionHook);
this.registerScriptType(new ScriptType(TYPE_PROXY, "script.type.proxy", PROXY_ICON, true));
this.registerScriptType(new ScriptType(TYPE_STANDALONE, "script.type.standalone", STANDALONE_ICON, false,
new String[] {ScriptType.CAPABILITY_APPEND}));
this.registerScriptType(new ScriptType(TYPE_TARGETED, "script.type.targeted", TARGETED_ICON, false));
this.registerScriptType(new ScriptType(TYPE_HTTP_SENDER, "script.type.httpsender", HTTP_SENDER_ICON, true));
extensionHook.addSessionListener(new ClearScriptVarsOnSessionChange());
extensionHook.addProxyListener(this.getProxyListener());
HttpSender.addListener(getHttpSenderScriptListener());
extensionHook.addOptionsParamSet(getScriptParam());
extensionHook.addCommandLine(getCommandLineArguments());
if (View.isInitialised()) {
extensionHook.getHookView().addOptionPanel(getOptionsScriptPanel());
} else {
// No GUI so add stdout as a writer
this.addWriter(new PrintWriter(System.out));
}
extensionHook.addApiImplementor(new ScriptAPI(this));
}
private OptionsScriptPanel getOptionsScriptPanel() {
if (optionsScriptPanel == null) {
optionsScriptPanel = new OptionsScriptPanel(this);
}
return optionsScriptPanel;
}
private ProxyListenerScript getProxyListener() {
if (this.proxyListener == null) {
this.proxyListener = new ProxyListenerScript(this);
}
return this.proxyListener;
}
private HttpSenderScriptListener getHttpSenderScriptListener() {
if (this.httpSenderScriptListener == null) {
this.httpSenderScriptListener = new HttpSenderScriptListener(this);
}
return this.httpSenderScriptListener;
}
public List<String> getScriptingEngines() {
List <String> engineNames = new ArrayList<>();
List<ScriptEngineFactory> engines = mgr.getEngineFactories();
for (ScriptEngineFactory engine : engines) {
engineNames.add(engine.getLanguageName() + LANG_ENGINE_SEP + engine.getEngineName());
}
for (ScriptEngineWrapper sew : this.engineWrappers) {
if (! engines.contains(sew.getEngine().getFactory())) {
engineNames.add(sew.getLanguageName() + LANG_ENGINE_SEP + sew.getEngineName());
}
}
Collections.sort(engineNames);
return engineNames;
}
/**
* Registers a new script engine wrapper.
* <p>
* The templates of the wrapped script engine are loaded, if any.
* <p>
* The engine is set to existing scripts targeting the given engine.
*
* @param wrapper the script engine wrapper that will be added, must not be {@code null}
* @see #removeScriptEngineWrapper(ScriptEngineWrapper)
* @see ScriptWrapper#setEngine(ScriptEngineWrapper)
*/
public void registerScriptEngineWrapper(ScriptEngineWrapper wrapper) {
logger.debug("registerEngineWrapper " + wrapper.getLanguageName() + " : " + wrapper.getEngineName());
this.engineWrappers.add(wrapper);
setScriptEngineWrapper(getTreeModel().getScriptsNode(), wrapper, wrapper);
setScriptEngineWrapper(getTreeModel().getTemplatesNode(), wrapper, wrapper);
// Templates for this engine might not have been loaded
this.loadTemplates(wrapper);
if (scriptUI != null) {
try {
scriptUI.engineAdded(wrapper);
} catch (Exception e) {
logger.error("An error occurred while notifying ScriptUI:", e);
}
}
}
/**
* Sets the given {@code newEngineWrapper} to all children of {@code baseNode} that targets the given {@code engineWrapper}.
*
* @param baseNode the node whose child nodes will have the engine set, must not be {@code null}
* @param engineWrapper the script engine of the targeting scripts, must not be {@code null}
* @param newEngineWrapper the script engine that will be set to the targeting scripts
* @see ScriptWrapper#setEngine(ScriptEngineWrapper)
*/
private void setScriptEngineWrapper(
ScriptNode baseNode,
ScriptEngineWrapper engineWrapper,
ScriptEngineWrapper newEngineWrapper) {
for (@SuppressWarnings("unchecked")
Enumeration<ScriptNode> e = baseNode.depthFirstEnumeration(); e.hasMoreElements();) {
ScriptNode node = e.nextElement();
if (node.getUserObject() != null && (node.getUserObject() instanceof ScriptWrapper)) {
ScriptWrapper scriptWrapper = (ScriptWrapper) node.getUserObject();
if (hasSameScriptEngine(scriptWrapper, engineWrapper)) {
scriptWrapper.setEngine(newEngineWrapper);
if (newEngineWrapper == null) {
if (scriptWrapper.isEnabled()) {
setEnabled(scriptWrapper, false);
scriptWrapper.setPreviouslyEnabled(true);
}
} else if (scriptWrapper.isPreviouslyEnabled()) {
setEnabled(scriptWrapper, true);
scriptWrapper.setPreviouslyEnabled(false);
}
}
}
}
}
/**
* Tells whether or not the given {@code scriptWrapper} has the given {@code engineWrapper}.
* <p>
* If the given {@code scriptWrapper} has an engine set it's checked by reference (operator {@code ==}), otherwise it's
* used the engine names.
*
* @param scriptWrapper the script wrapper that will be checked
* @param engineWrapper the engine that will be checked against the engine of the script
* @return {@code true} if the given script has the given engine, {@code false} otherwise.
* @since 2.4.0
* @see #isSameScriptEngine(String, String, String)
*/
public static boolean hasSameScriptEngine(ScriptWrapper scriptWrapper, ScriptEngineWrapper engineWrapper) {
if (scriptWrapper.getEngine() != null) {
return scriptWrapper.getEngine() == engineWrapper;
}
return isSameScriptEngine(scriptWrapper.getEngineName(), engineWrapper.getEngineName(), engineWrapper.getLanguageName());
}
/**
* Tells whether or not the given {@code name} matches the given {@code engineName} and {@code engineLanguage}.
*
* @param name the name that will be checked against the given {@code engineName} and {@code engineLanguage}.
* @param engineName the name of the script engine.
* @param engineLanguage the language of the script engine.
* @return {@code true} if the {@code name} matches the given engine's name and language, {@code false} otherwise.
* @since 2.4.0
* @see #hasSameScriptEngine(ScriptWrapper, ScriptEngineWrapper)
*/
public static boolean isSameScriptEngine(String name, String engineName, String engineLanguage) {
// In the configs we just use the engine name, in the UI we use the language name as well
if (name.indexOf(LANG_ENGINE_SEP) > 0) {
if (name.equals(engineLanguage + LANG_ENGINE_SEP + engineName)) {
return true;
}
return false;
}
if (name.equals(engineName)) {
return true;
}
// Nasty, but sometime the engine names are reported differently, eg 'Mozilla Rhino' vs 'Rhino'
if (name.endsWith(engineName)) {
return true;
}
if (engineName.endsWith(name)) {
return true;
}
return false;
}
/**
* Removes the given script engine wrapper.
* <p>
* The user's templates and scripts associated with the given engine are not removed but its engine is set to {@code null}.
* Default templates are removed.
* <p>
* The call to this method has no effect if the given type is not registered.
*
* @param wrapper the script engine wrapper that will be removed, must not be {@code null}
* @since 2.4.0
* @see #registerScriptEngineWrapper(ScriptEngineWrapper)
* @see ScriptWrapper#setEngine(ScriptEngineWrapper)
*/
public void removeScriptEngineWrapper(ScriptEngineWrapper wrapper) {
logger.debug("Removing script engine: " + wrapper.getLanguageName() + " : " + wrapper.getEngineName());
if (this.engineWrappers.remove(wrapper)) {
if (scriptUI != null) {
try {
scriptUI.engineRemoved(wrapper);
} catch (Exception e) {
logger.error("An error occurred while notifying ScriptUI:", e);
}
}
setScriptEngineWrapper(getTreeModel().getScriptsNode(), wrapper, null);
processTemplatesOfRemovedEngine(getTreeModel().getTemplatesNode(), wrapper);
}
}
private void processTemplatesOfRemovedEngine(ScriptNode baseNode, ScriptEngineWrapper engineWrapper) {
@SuppressWarnings("unchecked")
List<ScriptNode> templateNodes = Collections.list(baseNode.depthFirstEnumeration());
for (ScriptNode node : templateNodes) {
if (node.getUserObject() != null && (node.getUserObject() instanceof ScriptWrapper)) {
ScriptWrapper scriptWrapper = (ScriptWrapper) node.getUserObject();
if (hasSameScriptEngine(scriptWrapper, engineWrapper)) {
if (engineWrapper.isDefaultTemplate(scriptWrapper)) {
removeTemplate(scriptWrapper);
} else {
scriptWrapper.setEngine(null);
this.getTreeModel().nodeStructureChanged(scriptWrapper);
}
}
}
}
}
public ScriptEngineWrapper getEngineWrapper(String name) {
for (ScriptEngineWrapper sew : this.engineWrappers) {
if (isSameScriptEngine(name, sew.getEngineName(), sew.getLanguageName())) {
return sew;
}
}
// Not one we know of, create a default wrapper
List<ScriptEngineFactory> engines = mgr.getEngineFactories();
ScriptEngine engine = null;
for (ScriptEngineFactory e : engines) {
if (isSameScriptEngine(name, e.getEngineName(), e.getLanguageName())) {
engine = e.getScriptEngine();
break;
}
}
if (engine != null) {
DefaultEngineWrapper dew = new DefaultEngineWrapper(engine);
this.registerScriptEngineWrapper(dew);
return dew;
}
throw new InvalidParameterException("No such engine: " + name);
}
public String getEngineNameForExtension(String ext) {
ScriptEngine engine = mgr.getEngineByExtension(ext);
if (engine != null) {
return engine.getFactory().getLanguageName() + LANG_ENGINE_SEP + engine.getFactory().getEngineName();
}
for (ScriptEngineWrapper sew : this.engineWrappers) {
if (sew.getExtensions() != null) {
for (String extn : sew.getExtensions()) {
if (ext.equals(extn)) {
return sew.getLanguageName() + LANG_ENGINE_SEP + sew.getEngineName();
}
}
}
}
return null;
}
protected ScriptParam getScriptParam() {
if (this.scriptParam == null) {
this.scriptParam = new ScriptParam();
// NASTY! Need to find a cleaner way of getting the configs to load before the UI
this.scriptParam.load(Model.getSingleton().getOptionsParam().getConfig());
}
return this.scriptParam;
}
public ScriptTreeModel getTreeModel() {
if (this.treeModel == null) {
this.treeModel = new ScriptTreeModel();
}
return this.treeModel;
}
/**
* Registers a new type of script.
* <p>
* The script is added to the tree of scripts and its templates loaded, if any.
*
* @param type the new type of script
* @throws InvalidParameterException if a script type with same name is already registered
* @see #removeScripType(ScriptType)
*/
public void registerScriptType(ScriptType type) {
if (typeMap.containsKey(type.getName())) {
throw new InvalidParameterException("ScriptType already registered: " + type.getName());
}
this.typeMap.put(type.getName(), type);
this.getTreeModel().addType(type);
if (shouldLoadTemplatesOnScriptTypeRegistration) {
loadScriptTemplates(type);
}
}
/**
* Removes the given script type.
* <p>
* The templates and scripts associated with the given type are also removed, if any.
* <p>
* The call to this method has no effect if the given type is not registered.
*
* @param type the script type that will be removed
* @since 2.4.0
* @see #registerScriptType(ScriptType)
*/
public void removeScripType(ScriptType type) {
ScriptType scriptType = typeMap.remove(type.getName());
if (scriptType != null) {
getTreeModel().removeType(scriptType);
}
}
public ScriptType getScriptType (String name) {
return this.typeMap.get(name);
}
public Collection<ScriptType> getScriptTypes() {
return typeMap.values();
}
@Override
public String getAuthor() {
return Constant.ZAP_TEAM;
}
@Override
public String getDescription() {
return Constant.messages.getString("script.desc");
}
@Override
public URL getURL() {
try {
return new URL(Constant.ZAP_HOMEPAGE);
} catch (MalformedURLException e) {
return null;
}
}
private void refreshScript(ScriptWrapper script) {
for (ScriptEventListener listener : this.listeners) {
try {
listener.refreshScript(script);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
public ScriptWrapper getScript(String name) {
ScriptWrapper script = this.getTreeModel().getScript(name);
refreshScript(script);
return script;
}
public ScriptNode addScript(ScriptWrapper script) {
return this.addScript(script, true);
}
public ScriptNode addScript(ScriptWrapper script, boolean display) {
if (script == null) {
return null;
}
ScriptNode node = this.getTreeModel().addScript(script);
for (ScriptEventListener listener : this.listeners) {
try {
listener.scriptAdded(script, display);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
if (script.isLoadOnStart() && script.getFile() != null) {
this.getScriptParam().addScript(script);
this.getScriptParam().saveScripts();
}
return node;
}
public void saveScript(ScriptWrapper script) throws IOException {
refreshScript(script);
try ( BufferedWriter bw = Files.newBufferedWriter(script.getFile().toPath(), StandardCharsets.UTF_8)) {
bw.append(script.getContents());
}
this.setChanged(script, false);
// The removal is required for script that use wrappers, like Zest
this.getScriptParam().removeScript(script);
this.getScriptParam().addScript(script);
this.getScriptParam().saveScripts();
for (ScriptEventListener listener : this.listeners) {
try {
listener.scriptSaved(script);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
public void removeScript(ScriptWrapper script) {
script.setLoadOnStart(false);
this.getScriptParam().removeScript(script);
this.getScriptParam().saveScripts();
this.getTreeModel().removeScript(script);
for (ScriptEventListener listener : this.listeners) {
try {
listener.scriptRemoved(script);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
public void removeTemplate(ScriptWrapper template) {
this.getTreeModel().removeTemplate(template);
for (ScriptEventListener listener : this.listeners) {
try {
listener.templateRemoved(template);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
public ScriptNode addTemplate(ScriptWrapper template) {
return this.addTemplate(template, true);
}
public ScriptNode addTemplate(ScriptWrapper template, boolean display) {
if (template == null) {
return null;
}
ScriptNode node = this.getTreeModel().addTemplate(template);
for (ScriptEventListener listener : this.listeners) {
try {
listener.templateAdded(template, display);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
return node;
}
@Override
public void postInit() {
final List<String[]> scriptsNotAdded = new ArrayList<>(1);
for (ScriptWrapper script : this.getScriptParam().getScripts()) {
try {
this.loadScript(script);
if (script.getType() != null) {
this.addScript(script, false);
} else {
logger.warn(
"Failed to add script \"" + script.getName() + "\", script type not found: "
+ script.getTypeName());
scriptsNotAdded.add(new String[] { script.getName(), script.getEngineName(),
Constant.messages.getString("script.info.scriptsNotAdded.error.missingType", script.getTypeName()) });
}
} catch (MalformedInputException e) {
logger.warn("Failed to add script \"" + script.getName() + "\", contains invalid character sequence (UTF-8).");
scriptsNotAdded.add(new String[] { script.getName(), script.getEngineName(),
Constant.messages.getString("script.info.scriptsNotAdded.error.invalidChars") });
} catch (InvalidParameterException | IOException e) {
logger.error(e.getMessage(), e);
scriptsNotAdded.add(new String[] { script.getName(), script.getEngineName(),
Constant.messages.getString("script.info.scriptsNotAdded.error.other") });
}
}
informScriptsNotAdded(scriptsNotAdded);
this.loadTemplates();
for (File dir : this.getScriptParam().getScriptDirs()) {
// Load the scripts from subdirectories of each directory configured
int numAdded = addScriptsFromDir(dir);
logger.debug("Added " + numAdded + " scripts from dir: " + dir.getAbsolutePath());
}
shouldLoadTemplatesOnScriptTypeRegistration = true;
Path defaultScriptsDir = Paths.get(Constant.getZapHome(), SCRIPTS_DIR, SCRIPTS_DIR);
for (ScriptType scriptType : typeMap.values()) {
Path scriptTypeDir = defaultScriptsDir.resolve(scriptType.getName());
if (Files.notExists(scriptTypeDir)) {
try {
Files.createDirectories(scriptTypeDir);
} catch (IOException e) {
logger.warn("Failed to create directory for script type: " + scriptType.getName(), e);
}
}
}
}
private static void informScriptsNotAdded(final List<String[]> scriptsNotAdded) {
if (!View.isInitialised() || scriptsNotAdded.isEmpty()) {
return;
}
final List<Object> optionPaneContents = new ArrayList<>(2);
optionPaneContents.add(Constant.messages.getString("script.info.scriptsNotAdded.message"));
JXTable table = new JXTable(new AbstractTableModel() {
private static final long serialVersionUID = -457689656746030560L;
@Override
public String getColumnName(int column) {
if (column == 0) {
return Constant.messages.getString("script.info.scriptsNotAdded.table.column.scriptName");
} else if (column == 1) {
return Constant.messages.getString("script.info.scriptsNotAdded.table.column.scriptEngine");
}
return Constant.messages.getString("script.info.scriptsNotAdded.table.column.errorCause");
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
return scriptsNotAdded.get(rowIndex)[columnIndex];
}
@Override
public int getRowCount() {
return scriptsNotAdded.size();
}
@Override
public int getColumnCount() {
return 3;
}
});
table.setColumnControlVisible(true);
table.setVisibleRowCount(Math.min(scriptsNotAdded.size() + 1, 5));
table.packAll();
optionPaneContents.add(new JScrollPane(table));
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JOptionPane.showMessageDialog(
View.getSingleton().getMainFrame(),
optionPaneContents.toArray(),
Constant.PROGRAM_NAME,
JOptionPane.INFORMATION_MESSAGE);
}
});
}
public int addScriptsFromDir (File dir) {
logger.debug("Adding scripts from dir: " + dir.getAbsolutePath());
int addedScripts = 0;
for (ScriptType type : this.getScriptTypes()) {
File locDir = new File(dir, type.getName());
if (locDir.exists()) {
for (File f : locDir.listFiles()) {
String ext = f.getName().substring(f.getName().lastIndexOf(".") + 1);
String engineName = this.getEngineNameForExtension(ext);
if (engineName != null) {
try {
if (f.canWrite()) {
String scriptName = this.getUniqueScriptName(f.getName(), ext);
logger.debug("Loading script " + scriptName);
ScriptWrapper sw = new ScriptWrapper(scriptName, "",
this.getEngineWrapper(engineName), type, false, f);
this.loadScript(sw);
this.addScript(sw, false);
} else {
// Cant write so add as a template
String scriptName = this.getUniqueTemplateName(f.getName(), ext);
logger.debug("Loading script " + scriptName);
ScriptWrapper sw = new ScriptWrapper(scriptName, "",
this.getEngineWrapper(engineName), type, false, f);
this.loadScript(sw);
this.addTemplate(sw, false);
}
addedScripts++;
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
} else {
logger.debug("Ignoring " + f.getName());
}
}
}
}
return addedScripts;
}
public int removeScriptsFromDir (File dir) {
logger.debug("Removing scripts from dir: " + dir.getAbsolutePath());
int removedScripts = 0;
for (ScriptType type : this.getScriptTypes()) {
File locDir = new File(dir, type.getName());
if (locDir.exists()) {
// Loop through all of the known scripts and templates
// removing any from this directory
for (ScriptWrapper sw : this.getScripts(type)) {
if (sw.getFile().getParentFile().equals(locDir)) {
this.removeScript(sw);
removedScripts++;
}
}
for (ScriptWrapper sw : this.getTemplates(type)) {
if (sw.getFile().getParentFile().equals(locDir)) {
this.removeTemplate(sw);
removedScripts++;
}
}
}
}
return removedScripts;
}
public int getScriptCount(File dir) {
int scripts = 0;
for (ScriptType type : this.getScriptTypes()) {
File locDir = new File(dir, type.getName());
if (locDir.exists()) {
for (File f : locDir.listFiles()) {
String ext = f.getName().substring(f.getName().lastIndexOf(".") + 1);
String engineName = this.getEngineNameForExtension(ext);
if (engineName != null) {
scripts++;
}
}
}
}
return scripts;
}
/*
* Returns a unique name for the given script name
*/
private String getUniqueScriptName(String name, String ext) {
if (this.getScript(name) == null) {
// Its unique
return name;
}
// Its not unique, add a suitable index...
String stub = name.substring(0, name.length() - ext.length() - 1);
int index = 1;
do {
index++;
name = stub + "(" + index + ")." + ext;
}
while (this.getScript(name) != null);
return name;
}
/*
* Returns a unique name for the given template name
*/
private String getUniqueTemplateName(String name, String ext) {
if (this.getTreeModel().getTemplate(name) == null) {
// Its unique
return name;
}
// Its not unique, add a suitable index...
String stub = name.substring(0, name.length() - ext.length() - 1);
int index = 1;
do {
index++;
name = stub + "(" + index + ")." + ext;
}
while (this.getTreeModel().getTemplate(name) != null);
return name;
}
private void loadTemplates() {
this.loadTemplates(null);
}
private void loadTemplates(ScriptEngineWrapper engine) {
for (ScriptType type : this.getScriptTypes()) {
loadScriptTemplates(type, engine);
}
}
/**
* Loads script templates of the given {@code type}, for all script engines.
*
* @param type the script type whose templates will be loaded
* @since 2.4.0
* @see #loadScriptTemplates(ScriptType, ScriptEngineWrapper)
*/
private void loadScriptTemplates(ScriptType type) {
loadScriptTemplates(type, null);
}
/**
* Loads script templates of the given {@code type} for the given {@code engine}.
*
* @param type the script type whose templates will be loaded
* @param engine the script engine whose templates will be loaded for the given {@code script}
* @since 2.4.0
* @see #loadScriptTemplates(ScriptType)
*/
private void loadScriptTemplates(ScriptType type, ScriptEngineWrapper engine) {
File locDir = new File(Constant.getZapHome() + File.separator + TEMPLATES_DIR + File.separator + type.getName());
File stdDir = new File(Constant.getZapInstall() + File.separator + TEMPLATES_DIR + File.separator + type.getName());
// Load local files first, as these override any one included in the release
if (locDir.exists()) {
for (File f : locDir.listFiles()) {
loadTemplate(f, type, engine, false);
}
}
if (stdDir.exists()) {
for (File f : stdDir.listFiles()) {
// Dont log errors on duplicates - 'local' templates should take presidence
loadTemplate(f, type, engine, true);
}
}
}
private void loadTemplate(File f, ScriptType type, ScriptEngineWrapper engine, boolean ignoreDuplicates) {
if (f.getName().indexOf(".") > 0) {
if (this.getTreeModel().getTemplate(f.getName()) == null) {
String ext = f.getName().substring(f.getName().lastIndexOf(".") + 1);
String engineName = this.getEngineNameForExtension(ext);
if (engineName != null && (engine == null || engine.getEngine().getFactory().getExtensions().contains(ext))) {
try {
ScriptWrapper template = new ScriptWrapper(f.getName(), "",
this.getEngineWrapper(engineName), type, false, f);
this.loadScript(template);
this.addTemplate(template);
} catch (InvalidParameterException e) {
if (! ignoreDuplicates) {
logger.error(e.getMessage(), e);
}
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
}
}
}
}
public ScriptWrapper loadScript(ScriptWrapper script) throws IOException {
StringBuilder sb = new StringBuilder();
try (BufferedReader br = Files.newBufferedReader(script.getFile().toPath(), StandardCharsets.UTF_8)) {
int len;
char[] buf = new char[1024];
while ((len = br.read(buf)) != -1) {
sb.append(buf, 0, len);
}
}
script.setContents(sb.toString());
script.setChanged(false);
if (script.getType() == null) {
// This happens when scripts are loaded from the configs as the types
// may well not have been registered at that stage
script.setType(this.getScriptType(script.getTypeName()));
}
return script;
}
public List<ScriptWrapper> getScripts(String type) {
return this.getScripts(this.getScriptType(type));
}
public List<ScriptWrapper> getScripts(ScriptType type) {
List<ScriptWrapper> scripts = new ArrayList<>();
if (type == null) {
return scripts;
}
for (ScriptNode node : this.getTreeModel().getNodes(type.getName())) {
ScriptWrapper script = (ScriptWrapper) node.getUserObject();
refreshScript(script);
scripts.add((ScriptWrapper) node.getUserObject());
}
return scripts;
}
public List<ScriptWrapper> getTemplates(ScriptType type) {
List<ScriptWrapper> scripts = new ArrayList<>();
if (type == null) {
return scripts;
}
for (ScriptWrapper script : this.getTreeModel().getTemplates(type)) {
scripts.add(script);
}
return scripts;
}
/*
* This extension supports any number of writers to be registered which all get written to for
* ever script. It also supports script specific writers.
*/
private Writer getWriters(ScriptWrapper script) {
Writer writer = script.getWriter();
if (writer == null) {
// No script specific writer, just return the std one
return this.writers;
} else {
// Return the script specific writer in addition to the std one
MultipleWriters scriptWriters = new MultipleWriters();
scriptWriters.addWriter(writer);
scriptWriters.addWriter(writers);
return scriptWriters;
}
}
/**
* Invokes the given {@code script}, handling any {@code Exception} thrown during the invocation.
* <p>
* The context class loader of caller thread is replaced with the class loader {@code AddOnLoader} to allow the script to
* access classes of add-ons. If this behaviour is not desired call the method {@code invokeScriptWithOutAddOnLoader}
* instead.
*
* @param script the script that will be invoked
* @return an {@code Invocable} for the {@code script}, or {@code null} if none.
* @throws ScriptException if the engine of the given {@code script} was not found.
* @see #invokeScriptWithOutAddOnLoader(ScriptWrapper)
* @see Invocable
*/
public Invocable invokeScript(ScriptWrapper script) throws ScriptException {
logger.debug("invokeScript " + script.getName());
preInvokeScript(script);
ClassLoader previousContextClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(ExtensionFactory.getAddOnLoader());
try {
return invokeScriptImpl(script);
} finally {
Thread.currentThread().setContextClassLoader(previousContextClassLoader);
}
}
/**
* Notifies the {@code ScriptEventListener}s that the given {@code script} should be refreshed, resets the error and
* output states of the given {@code script} and notifies {@code ScriptEventListener}s of the pre-invocation.
* <p>
* If the script does not have an engine it will be set one (if found).
*
* @param script the script that will be invoked after the call to this method
* @throws ScriptException if the engine of the given {@code script} was not found.
* @see ScriptEventListener#refreshScript(ScriptWrapper)
* @see ScriptEventListener#preInvoke(ScriptWrapper)
*/
private void preInvokeScript(ScriptWrapper script) throws ScriptException {
if (script.getEngine() == null) {
// Scripts loaded from the configs my have loaded before all of the engines
script.setEngine(this.getEngineWrapper(script.getEngineName()));
}
if (script.getEngine() == null) {
throw new ScriptException("Failed to find script engine: " + script.getEngineName());
}
refreshScript(script);
script.setLastErrorDetails("");
script.setLastException(null);
script.setLastOutput("");
for (ScriptEventListener listener : this.listeners) {
try {
listener.preInvoke(script);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
/**
* Invokes the given {@code script}, handling any {@code Exception} thrown during the invocation.
* <p>
* Script's (or default) {@code Writer} is set to the {@code ScriptContext} of the {@code ScriptEngine} before the
* invocation.
*
* @param script the script that will be invoked
* @return an {@code Invocable} for the {@code script}, or {@code null} if none.
* @see #getWriters(ScriptWrapper)
* @see Invocable
*/
private Invocable invokeScriptImpl(ScriptWrapper script) {
ScriptEngine se = script.getEngine().getEngine();
Writer writer = getWriters(script);
se.getContext().setWriter(writer);
// Set the script name as a context attribute - this is used for script level variables
se.getContext().setAttribute(SCRIPT_NAME_ATT, script.getName(), ScriptContext.ENGINE_SCOPE);
try {
se.eval(script.getContents());
} catch (Exception e) {
handleScriptException(script, writer, e);
}
if (se instanceof Invocable) {
return (Invocable) se;
}
return null;
}
/**
* Invokes the given {@code script}, handling any {@code Exception} thrown during the invocation.
*
* @param script the script that will be invoked/evaluated
* @return an {@code Invocable} for the {@code script}, or {@code null} if none.
* @throws ScriptException if the engine of the given {@code script} was not found.
* @see #invokeScript(ScriptWrapper)
* @see Invocable
*/
public Invocable invokeScriptWithOutAddOnLoader(ScriptWrapper script) throws ScriptException {
logger.debug("invokeScriptWithOutAddOnLoader " + script.getName());
preInvokeScript(script);
return invokeScriptImpl(script);
}
/**
* Handles exceptions thrown by scripts.
* <p>
* The given {@code exception} (if of type {@code ScriptException} the cause will be used instead) will be written to the
* the writer(s) associated with the given {@code script}, moreover it will be disabled and flagged that has an error.
*
* @param script the script that resulted in an exception, must not be {@code null}
* @param exception the exception thrown , must not be {@code null}
* @since 2.5.0
* @see #setEnabled(ScriptWrapper, boolean)
* @see #setError(ScriptWrapper, Exception)
* @see #handleFailedScriptInterface(ScriptWrapper, String)
* @see ScriptException
*/
public void handleScriptException(ScriptWrapper script, Exception exception) {
handleScriptException(script, getWriters(script), exception);
}
/**
* Handles exceptions thrown by scripts.
* <p>
* The given {@code exception} (if of type {@code ScriptException} the cause will be used instead) will be written to the
* given {@code writer} and the given {@code script} will be disabled and flagged that has an error.
*
* @param script the script that resulted in an exception, must not be {@code null}
* @param writer the writer associated with the script, must not be {@code null}
* @param exception the exception thrown , must not be {@code null}
* @see #setError(ScriptWrapper, Exception)
* @see #setEnabled(ScriptWrapper, boolean)
* @see ScriptException
*/
private void handleScriptException(ScriptWrapper script, Writer writer, Exception exception) {
Exception cause = exception;
if (cause instanceof ScriptException && cause.getCause() instanceof Exception) {
// Dereference one level
cause = (Exception) cause.getCause();
}
try {
writer.append(cause.toString());
} catch (IOException ignore) {
logger.error(cause.getMessage(), cause);
}
this.setError(script, cause);
this.setEnabled(script, false);
}
public void invokeTargetedScript(ScriptWrapper script, HttpMessage msg) {
validateScriptType(script, TYPE_TARGETED);
Writer writer = getWriters(script);
try {
// Dont need to check if enabled as it can only be invoked manually
TargetedScript s = this.getInterface(script, TargetedScript.class);
if (s != null) {
s.invokeWith(msg);
} else {
handleFailedScriptInterface(script, writer, Constant.messages.getString("script.interface.targeted.error"));
}
} catch (Exception e) {
handleScriptException(script, writer, e);
}
}
/**
* Validates that the given {@code script} is of the given {@code scriptType}, throwing an {@code IllegalArgumentException}
* if not.
*
* @param script the script that will be checked, must not be {@code null}
* @param scriptType the expected type of the script, must not be {@code null}
* @throws IllegalArgumentException if the given {@code script} is not the given {@code scriptType}.
* @see ScriptWrapper#getTypeName()
*/
private static void validateScriptType(ScriptWrapper script, String scriptType) throws IllegalArgumentException {
if (!scriptType.equals(script.getTypeName())) {
throw new IllegalArgumentException("Script " + script.getName() + " is not a '" + scriptType + "' script: "
+ script.getTypeName());
}
}
/**
* Handles a failed attempt to convert a script into an interface.
* <p>
* The given {@code errorMessage} will be written to the writer(s) associated with the given {@code script}, moreover it
* will be disabled and flagged that has an error.
*
* @param script the script that resulted in an exception, must not be {@code null}
* @param errorMessage the message that will be written to the writer(s)
* @since 2.5.0
* @see #setEnabled(ScriptWrapper, boolean)
* @see #setError(ScriptWrapper, Exception)
* @see #handleScriptException(ScriptWrapper, Exception)
*/
public void handleFailedScriptInterface(ScriptWrapper script, String errorMessage) {
handleFailedScriptInterface(script, getWriters(script), errorMessage);
}
/**
* Handles a failed attempt to convert a script into an interface.
* <p>
* The given {@code errorMessage} will be written to the given {@code writer} and the given {@code script} will be disabled
* and flagged that has an error.
*
* @param script the script that failed to be converted to an interface, must not be {@code null}
* @param writer the writer associated with the script, must not be {@code null}
* @param errorMessage the message that will be written to the given {@code writer}
* @see #setError(ScriptWrapper, String)
* @see #setEnabled(ScriptWrapper, boolean)
*/
private void handleFailedScriptInterface(ScriptWrapper script, Writer writer, String errorMessage) {
try {
writer.append(errorMessage);
} catch (IOException e) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to append script error message because of an exception:", e);
}
logger.warn("Failed to append error message: " + errorMessage);
}
this.setError(script, errorMessage);
this.setEnabled(script, false);
}
public boolean invokeProxyScript(ScriptWrapper script, HttpMessage msg, boolean request) {
validateScriptType(script, TYPE_PROXY);
Writer writer = getWriters(script);
try {
// Dont need to check if enabled as it can only be invoked manually
ProxyScript s = this.getInterface(script, ProxyScript.class);
if (s != null) {
if (request) {
return s.proxyRequest(msg);
} else {
return s.proxyResponse(msg);
}
} else {
handleFailedScriptInterface(script, writer, Constant.messages.getString("script.interface.proxy.error"));
}
} catch (Exception e) {
handleScriptException(script, writer, e);
}
// Return true so that the request is submitted - if we returned false all proxying would fail on script errors
return true;
}
public void invokeSenderScript(ScriptWrapper script, HttpMessage msg, int initiator, HttpSender sender, boolean request) {
validateScriptType(script, TYPE_HTTP_SENDER);
Writer writer = getWriters(script);
try {
HttpSenderScript senderScript = this.getInterface(script, HttpSenderScript.class);
if (senderScript != null) {
if (request) {
senderScript.sendingRequest(msg, initiator, new HttpSenderScriptHelper(sender));
} else {
senderScript.responseReceived(msg, initiator, new HttpSenderScriptHelper(sender));
}
} else {
handleFailedScriptInterface(script, writer, Constant.messages.getString("script.interface.httpsender.error"));
}
} catch (Exception e) {
handleScriptException(script, writer, e);
}
}
public void setChanged(ScriptWrapper script, boolean changed) {
script.setChanged(changed);
ScriptNode node = this.getTreeModel().getNodeForScript(script);
if (node.getNodeName().equals(script.getName())) {
// The name is the same
this.getTreeModel().nodeStructureChanged(script);
} else {
// The name has changed
node.setNodeName(script.getName());
this.getTreeModel().nodeStructureChanged(node.getParent());
}
notifyScriptChanged(script);
}
/**
* Notifies the {@code ScriptEventListener}s that the given {@code script} was changed.
*
* @param script the script that was changed, must not be {@code null}
* @see #listeners
* @see ScriptEventListener#scriptChanged(ScriptWrapper)
*/
private void notifyScriptChanged(ScriptWrapper script) {
for (ScriptEventListener listener : this.listeners) {
try {
listener.scriptChanged(script);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
public void setEnabled(ScriptWrapper script, boolean enabled) {
if (!script.getType().isEnableable()) {
return;
}
if (enabled && script.getEngine() == null) {
return;
}
script.setEnabled(enabled);
this.getTreeModel().nodeStructureChanged(script);
notifyScriptChanged(script);
}
public void setError(ScriptWrapper script, String details) {
script.setError(true);
script.setLastErrorDetails(details);
script.setLastOutput(details);
this.getTreeModel().nodeStructureChanged(script);
for (ScriptEventListener listener : this.listeners) {
try {
listener.scriptError(script);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
public void setError(ScriptWrapper script, Exception e) {
script.setLastException(e);
setError(script, e.getMessage());
}
public void addListener(ScriptEventListener listener) {
this.listeners.add(listener);
}
public void removeListener(ScriptEventListener listener) {
this.listeners.remove(listener);
}
public void addWriter(Writer writer) {
this.writers.addWriter(writer);
}
public void removeWriter(Writer writer) {
this.writers.removeWriter(writer);
}
public ScriptUI getScriptUI() {
return scriptUI;
}
public void setScriptUI(ScriptUI scriptUI) {
if (this.scriptUI != null) {
throw new InvalidParameterException("A script UI has already been set - only one is supported");
}
this.scriptUI = scriptUI;
}
public void removeScriptUI() {
this.scriptUI = null;
}
/**
* Gets the interface {@code class1} from the given {@code script}. Might return {@code null} if the {@code script} does not
* implement the interface.
* <p>
* First tries to get the interface directly from the {@code script} by calling the method
* {@code ScriptWrapper.getInterface(Class)}, if it returns {@code null} the interface will be extracted from the script
* after invoking it, using the method {@code Invocable.getInterface(Class)}.
* <p>
* The context class loader of caller thread is replaced with the class loader {@code AddOnLoader} to allow the script to
* access classes of add-ons. If this behaviour is not desired call the method {@code getInterfaceWithOutAddOnLoader(}
* instead.
*
* @param script the script that will be invoked
* @param class1 the interface that will be obtained from the script
* @return the interface implemented by the script, or {@code null} if the {@code script} does not implement the interface.
* @throws ScriptException if the engine of the given {@code script} was not found.
* @throws IOException if an error occurred while obtaining the interface directly from the script (
* {@code ScriptWrapper.getInterface(Class)})
* @see #getInterfaceWithOutAddOnLoader(ScriptWrapper, Class)
* @see ScriptWrapper#getInterface(Class)
* @see Invocable#getInterface(Class)
*/
public <T> T getInterface(ScriptWrapper script, Class<T> class1) throws ScriptException, IOException {
ClassLoader previousContextClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(ExtensionFactory.getAddOnLoader());
try {
T iface = script.getInterface(class1);
if (iface != null) {
// the script wrapper has overriden the usual scripting mechanism
return iface;
}
} finally {
Thread.currentThread().setContextClassLoader(previousContextClassLoader);
}
if (script.isRunableStandalone()) {
return null;
}
Invocable invocable = invokeScript(script);
if (invocable != null) {
return invocable.getInterface(class1);
}
return null;
}
/**
* Gets the interface {@code clasz} from the given {@code script}. Might return {@code null} if the {@code script} does not
* implement the interface.
* <p>
* First tries to get the interface directly from the {@code script} by calling the method
* {@code ScriptWrapper.getInterface(Class)}, if it returns {@code null} the interface will be extracted from the script
* after invoking it, using the method {@code Invocable.getInterface(Class)}.
*
* @param script the script that will be invoked
* @param clasz the interface that will be obtained from the script
* @return the interface implemented by the script, or {@code null} if the {@code script} does not implement the interface.
* @throws ScriptException if the engine of the given {@code script} was not found.
* @throws IOException if an error occurred while obtaining the interface directly from the script (
* {@code ScriptWrapper.getInterface(Class)})
* @see #getInterface(ScriptWrapper, Class)
* @see ScriptWrapper#getInterface(Class)
* @see Invocable#getInterface(Class)
*/
public <T> T getInterfaceWithOutAddOnLoader(ScriptWrapper script, Class<T> clasz) throws ScriptException, IOException {
T iface = script.getInterface(clasz);
if (iface != null) {
// the script wrapper has overriden the usual scripting mechanism
return iface;
}
return invokeScriptWithOutAddOnLoader(script).getInterface(clasz);
}
@Override
public List<String> getUnsavedResources() {
// Report all of the unsaved scripts
List<String> list = new ArrayList<>();
for (ScriptType type : this.getScriptTypes()) {
for (ScriptWrapper script : this.getScripts(type)) {
if (script.isChanged()) {
list.add(MessageFormat.format(Constant.messages.getString("script.resource"), script.getName()));
}
}
}
return list;
}
private void openCmdLineFile(File f) throws IOException, ScriptException {
if (! f.exists()) {
CommandLine.info(MessageFormat.format(
Constant.messages.getString("script.cmdline.nofile"), f.getAbsolutePath()));
return;
}
if (! f.canRead()) {
CommandLine.info(MessageFormat.format(
Constant.messages.getString("script.cmdline.noread"), f.getAbsolutePath()));
return;
}
int dotIndex = f.getName().lastIndexOf(".");
if (dotIndex <= 0) {
CommandLine.info(MessageFormat.format(
Constant.messages.getString("script.cmdline.noext"), f.getAbsolutePath()));
return;
}
String ext = f.getName().substring(dotIndex+1);
String engineName = this.getEngineNameForExtension(ext);
if (engineName == null) {
CommandLine.info(MessageFormat.format(
Constant.messages.getString("script.cmdline.noengine"), ext));
return;
}
ScriptWrapper sw = new ScriptWrapper(f.getName(), "", engineName,
this.getScriptType(TYPE_STANDALONE), true, f);
this.loadScript(sw);
this.addScript(sw);
if (! View.isInitialised()) {
// Only invoke if run from the command line
// if the GUI is present then its up to the user to invoke it
this.invokeScript(sw);
}
}
@Override
public void execute(CommandLineArgument[] args) {
if (arguments[ARG_SCRIPT_IDX].isEnabled()) {
for (CommandLineArgument arg : args) {
Vector<String> params = arg.getArguments();
if (params != null) {
for (String script : params) {
try {
openCmdLineFile(new File(script));
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
}
} else {
return;
}
}
private CommandLineArgument[] getCommandLineArguments() {
arguments[ARG_SCRIPT_IDX] = new CommandLineArgument("-script", 1, null, "",
"-script <script> " + Constant.messages.getString("script.cmdline.help"));
return arguments;
}
@Override
public boolean handleFile(File file) {
int dotIndex = file.getName().lastIndexOf(".");
if (dotIndex <= 0) {
// No extension, cant work out which engine
return false;
}
String ext = file.getName().substring(dotIndex+1);
String engineName = this.getEngineNameForExtension(ext);
if (engineName == null) {
// No engine for this extension, we cant handle this
return false;
}
try {
openCmdLineFile(file);
} catch (Exception e) {
logger.error(e.getMessage(), e);
return false;
}
return true;
}
@Override
public List<String> getHandledExtensions() {
// The list of all of the script extensions that can be handled from the command line
List<String> exts = new ArrayList<String>();
for (ScriptEngineWrapper sew : this.engineWrappers) {
exts.addAll(sew.getExtensions());
}
return exts;
}
/**
* No database tables used, so all supported
*/
@Override
public boolean supportsDb(String type) {
return true;
}
private static class ClearScriptVarsOnSessionChange implements SessionChangedListener {
@Override
public void sessionChanged(Session session) {
}
@Override
public void sessionAboutToChange(Session session) {
ScriptVars.clear();
}
@Override
public void sessionScopeChanged(Session session) {
}
@Override
public void sessionModeChanged(Mode mode) {
}
}
}