org.zaproxy.zap.extension.zest.ExtensionZest.java Source code

Java tutorial

Introduction

Here is the source code for org.zaproxy.zap.extension.zest.ExtensionZest.java

Source

/*
 * Zed Attack Proxy (ZAP) and its related class files.
 *
 * ZAP is an HTTP/HTTPS proxy for assessing web application security.
 *
 * Copyright 2014 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.zest;

import java.awt.Component;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.swing.ImageIcon;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import net.htmlparser.jericho.Source;
import net.sf.json.JSONObject;
import org.apache.commons.httpclient.URI;
import org.apache.log4j.Logger;
import org.mozilla.zest.core.v1.ZestActionFail;
import org.mozilla.zest.core.v1.ZestAssertion;
import org.mozilla.zest.core.v1.ZestAssignFieldValue;
import org.mozilla.zest.core.v1.ZestClientElementClick;
import org.mozilla.zest.core.v1.ZestClientElementSendKeys;
import org.mozilla.zest.core.v1.ZestClientLaunch;
import org.mozilla.zest.core.v1.ZestClientWindowHandle;
import org.mozilla.zest.core.v1.ZestConditional;
import org.mozilla.zest.core.v1.ZestContainer;
import org.mozilla.zest.core.v1.ZestElement;
import org.mozilla.zest.core.v1.ZestExpression;
import org.mozilla.zest.core.v1.ZestExpressionLength;
import org.mozilla.zest.core.v1.ZestExpressionStatusCode;
import org.mozilla.zest.core.v1.ZestFieldDefinition;
import org.mozilla.zest.core.v1.ZestLoop;
import org.mozilla.zest.core.v1.ZestRequest;
import org.mozilla.zest.core.v1.ZestResponse;
import org.mozilla.zest.core.v1.ZestScript;
import org.mozilla.zest.core.v1.ZestStatement;
import org.mozilla.zest.core.v1.ZestStructuredExpression;
import org.mozilla.zest.core.v1.ZestVariables;
import org.mozilla.zest.impl.ZestScriptEngineFactory;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.control.Control;
import org.parosproxy.paros.control.Control.Mode;
import org.parosproxy.paros.core.proxy.ProxyListener;
import org.parosproxy.paros.core.scanner.Alert;
import org.parosproxy.paros.extension.Extension;
import org.parosproxy.paros.extension.ExtensionAdaptor;
import org.parosproxy.paros.extension.ExtensionHook;
import org.parosproxy.paros.extension.SessionChangedListener;
import org.parosproxy.paros.model.Session;
import org.parosproxy.paros.model.SiteNode;
import org.parosproxy.paros.network.HttpMessage;
import org.parosproxy.paros.view.View;
import org.zaproxy.zap.control.AddOn;
import org.zaproxy.zap.extension.anticsrf.AntiCsrfToken;
import org.zaproxy.zap.extension.anticsrf.ExtensionAntiCSRF;
import org.zaproxy.zap.extension.httppanel.Message;
import org.zaproxy.zap.extension.pscan.ExtensionPassiveScan;
import org.zaproxy.zap.extension.script.ExtensionScript;
import org.zaproxy.zap.extension.script.ScriptEngineWrapper;
import org.zaproxy.zap.extension.script.ScriptEventListener;
import org.zaproxy.zap.extension.script.ScriptNode;
import org.zaproxy.zap.extension.script.ScriptType;
import org.zaproxy.zap.extension.script.ScriptWrapper;
import org.zaproxy.zap.extension.zest.ZestResultsTableModel.ZestResultsTableEntry;
import org.zaproxy.zap.extension.zest.dialogs.ZestDialogManager;
import org.zaproxy.zap.extension.zest.menu.ZestMenuManager;
import org.zaproxy.zap.view.ZapToggleButton;

public class ExtensionZest extends ExtensionAdaptor implements ProxyListener, ScriptEventListener {

    public static final String NAME = "ExtensionZest";
    public static final ImageIcon ZEST_ICON = new ImageIcon(
            ExtensionZest.class.getResource("/org/zaproxy/zap/extension/zest/resources/icons/fruit-orange.png"));

    private static final ImageIcon RECORD_OFF_ICON = new ImageIcon(
            ExtensionZest.class.getResource("/org/zaproxy/zap/extension/zest/resources/icons/cassette.png"));
    private static final ImageIcon RECORD_ON_ICON = new ImageIcon(
            ExtensionZest.class.getResource("/org/zaproxy/zap/extension/zest/resources/icons/cassette-red.png"));

    public static final String HTTP_HEADER_X_SECURITY_PROXY = "X-Security-Proxy";
    public static final String VALUE_RECORD = "record";

    private static final Logger logger = Logger.getLogger(ExtensionZest.class);

    private static final List<Class<? extends Extension>> EXTENSION_DEPENDENCIES;

    private ZestParam param = null;
    private OptionsZestPanel optionsZestPanel = null;

    private ZestResultsPanel zestResultsPanel = null;
    private ZapToggleButton recordButton = null;
    private JToolBar.Separator toolbarSeparator;

    private ZestTreeModel zestTreeModel = null;
    private ZestDialogManager dialogManager = null;
    private ZestEngineWrapper zestEngineWrapper = null;
    private ZestScriptEngineFactory zestEngineFactory = null;

    private ExtensionScript extScript = null;
    private ExtensionAntiCSRF extAcsrf = null;
    private ZestScript lastRunScript = null;
    private HttpMessage lastSelectedMessage = null;
    private Map<String, String> acsrfTokenToVar = new HashMap<String, String>();

    private ZestFuzzerDelegate fuzzerMessenger = null;
    private ScriptNode scriptNodeRecording = null;

    // Cut-n-paste stuff
    private List<ScriptNode> cnpNodes = null;
    private boolean cutNodes = false;

    // Client side recording
    private Map<String, String> clientUrlToWindowHandle = new HashMap<String, String>();
    private String startRecordingUrl = null;
    private int recordingWinId = 0;
    private ScriptNode recordingNode = null;

    static {
        List<Class<? extends Extension>> dependencies = new ArrayList<>(1);
        dependencies.add(ExtensionScript.class);
        EXTENSION_DEPENDENCIES = Collections.unmodifiableList(dependencies);
    }

    public ExtensionZest() {
        super(NAME);
        this.setOrder(73); // Almost looks like ZE ;)
    }

    @Override
    public void hook(ExtensionHook extensionHook) {
        super.hook(extensionHook);

        extensionHook.addOptionsParamSet(getParam());

        if (getView() != null) {
            extensionHook.addProxyListener(this);
            extensionHook.addSessionListener(new ViewSessionChangedListener());

            extensionHook.getHookView().addStatusPanel(this.getZestResultsPanel());
            extensionHook.getHookView().addOptionPanel(getOptionsPanel());

            this.dialogManager = new ZestDialogManager(this, this.getExtScript().getScriptUI());
            new ZestMenuManager(this, extensionHook);

            View.getSingleton().addMainToolbarButton(getRecordButton());
            View.getSingleton().addMainToolbarSeparator(getToolbarSeparator());

            if (getExtScript().getScriptUI() != null) {
                ZestTreeTransferHandler th = new ZestTreeTransferHandler(this);
                getExtScript().getScriptUI().addScriptTreeTransferHandler(ZestElementWrapper.class, th);
            }
        }

        List<Path> defaultTemplates = getDefaultTemplates();

        ScriptEngineManager mgr = new ScriptEngineManager();
        ScriptEngine se = mgr.getEngineByName(ZestScriptEngineFactory.NAME);
        if (se != null) {
            // Looks like this only works if the Zest lib is in the top level
            // lib directory
            this.zestEngineFactory = (ZestScriptEngineFactory) se.getFactory();
        } else {
            // Needed for when the Zest lib is in an add-on (usual case)
            this.zestEngineFactory = new ZestScriptEngineFactory();
            se = zestEngineFactory.getScriptEngine();
        }
        zestEngineWrapper = new ZestEngineWrapper(se, defaultTemplates);
        this.getExtScript().registerScriptEngineWrapper(zestEngineWrapper);

        this.getExtScript().addListener(this);

        if (this.getExtScript().getScriptUI() != null) {
            ZestTreeCellRenderer renderer = new ZestTreeCellRenderer();
            this.getExtScript().getScriptUI().addRenderer(ZestElementWrapper.class, renderer);
            this.getExtScript().getScriptUI().addRenderer(ZestScriptWrapper.class, renderer);
            this.getExtScript().getScriptUI().disableScriptDialog(ZestScriptWrapper.class);
        }
    }

    private List<Path> getDefaultTemplates() {
        AddOn addOn = getAddOn();
        if (addOn == null) {
            // Probably running from source...
            return Collections.emptyList();
        }

        List<String> files = addOn.getFiles();
        if (files == null || files.isEmpty()) {
            return Collections.emptyList();
        }

        ArrayList<Path> defaultTemplates = new ArrayList<>(files.size());
        Path zapHome = Paths.get(Constant.getZapHome());
        for (String file : files) {
            if (file.startsWith(ExtensionScript.TEMPLATES_DIR)) {
                defaultTemplates.add(zapHome.resolve(file));
            }
        }
        defaultTemplates.trimToSize();
        return defaultTemplates;
    }

    public boolean isPlugNHackInstalled() {
        return Control.getSingleton().getExtensionLoader().getExtension("ExtensionPlugNHack") != null;
    }

    public void recordClientScript(String url) {
        Extension extPnh = Control.getSingleton().getExtensionLoader().getExtension("ExtensionPlugNHack");
        if (extPnh != null) {
            Method method = null;
            try {
                URI uri = new URI(url, true);

                startClientRecording(url);

                method = extPnh.getClass().getMethod("launchAndRecordClient", URI.class);

                method.invoke(extPnh, uri);

            } catch (Exception e) {
                // Its an older version, so just dont try to use it
                e.printStackTrace();
            }
        }
    }

    protected ZestScriptEngineFactory getZestScriptEngineFactory() {
        return this.zestEngineFactory;
    }

    public ZestFuzzerDelegate getFuzzerDelegate() {
        if (fuzzerMessenger == null) {
            fuzzerMessenger = new ZestFuzzerDelegate();
        }
        return fuzzerMessenger;
    }

    @Override
    public void optionsLoaded() {
        if (getView() == null || EventQueue.isDispatchThread()) {
            // Convert scripts loaded on start into real Zest scripts
            for (ScriptType type : this.getExtScript().getScriptTypes()) {
                for (ScriptWrapper script : this.getExtScript().getScripts(type)) {
                    if (ZestScriptEngineFactory.NAME.equals(script.getEngineName())) {
                        this.scriptAdded(script, false);
                    }
                }
            }
        } else {
            EventQueue.invokeLater(new Runnable() {

                @Override
                public void run() {
                    optionsLoaded();
                }
            });
        }
    }

    public ZestEngineWrapper getZestEngineWrapper() {
        return zestEngineWrapper;
    }

    @Override
    public boolean canUnload() {
        return true;
    }

    @Override
    public void unload() {
        if (getView() != null) {
            View view = View.getSingleton();
            view.removeMainToolbarButton(getRecordButton());
            view.removeMainToolbarSeparator(getToolbarSeparator());
            dialogManager.unload();
        }

        // Convert zest scripts into "plain" scripts
        for (ScriptType type : this.getExtScript().getScriptTypes()) {
            for (ScriptWrapper script : this.getExtScript().getScripts(type)) {
                if (script.getEngineName().equals(ZestScriptEngineFactory.NAME)) {
                    ScriptNode node = this.getExtScript().getTreeModel().getNodeForScript(script);
                    if (script instanceof ZestScriptWrapper) {
                        ZestScriptWrapper zsw = (ZestScriptWrapper) script;
                        ScriptWrapper original = zsw.getOriginal();
                        original.setEngine(zsw.getEngine());
                        original.setEnabled(zsw.isEnabled());
                        original.setFile(zsw.getFile());
                        original.setLoadOnStart(zsw.isLoadOnStart());
                        original.setContents(zsw.getContents());
                        original.setChanged(zsw.isChanged());

                        node.setUserObject(original);
                        node.removeAllChildren();
                        this.getExtScript().getTreeModel().nodeStructureChanged(node);
                    }
                }
            }
        }

        if (this.getExtScript().getScriptUI() != null) {
            this.getExtScript().getScriptUI().removeScriptTreeTransferHandler(ZestScriptWrapper.class);
            this.getExtScript().getScriptUI().removeScriptTreeTransferHandler(ZestElementWrapper.class);

            this.getExtScript().getScriptUI().removeRenderer(ZestElementWrapper.class);
            this.getExtScript().getScriptUI().removeRenderer(ZestScriptWrapper.class);
            this.getExtScript().getScriptUI().removeDisableScriptDialog(ZestScriptWrapper.class);
        }

        getExtScript().removeListener(this);
        getExtScript().removeScriptEngineWrapper(zestEngineWrapper);

        super.unload();
    }

    public ExtensionScript getExtScript() {
        if (extScript == null) {
            extScript = (ExtensionScript) Control.getSingleton().getExtensionLoader()
                    .getExtension(ExtensionScript.NAME);
        }
        return extScript;
    }

    public ExtensionAntiCSRF getExtACSRF() {
        if (extAcsrf == null) {
            extAcsrf = (ExtensionAntiCSRF) Control.getSingleton().getExtensionLoader()
                    .getExtension(ExtensionAntiCSRF.NAME);
        }
        return extAcsrf;
    }

    public ZestDialogManager getDialogManager() {
        return dialogManager;
    }

    private ZestResultsPanel getZestResultsPanel() {
        if (zestResultsPanel == null) {
            zestResultsPanel = new ZestResultsPanel(this);
        }
        return zestResultsPanel;
    }

    public ZestTreeModel getZestTreeModel() {
        if (zestTreeModel == null && getExtScript() != null) {
            zestTreeModel = new ZestTreeModel(this.getExtScript().getTreeModel());
        }
        return zestTreeModel;
    }

    private JToggleButton getRecordButton() {
        if (recordButton == null) {
            recordButton = new ZapToggleButton();
            recordButton.setIcon(RECORD_OFF_ICON);
            recordButton.setSelectedIcon(RECORD_ON_ICON);
            recordButton.setToolTipText(Constant.messages.getString("zest.toolbar.button.record.off"));
            recordButton.setSelectedToolTipText(Constant.messages.getString("zest.toolbar.button.record.on"));

            recordButton.addActionListener(new java.awt.event.ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (recordButton.isSelected()) {
                        getDialogManager().showZestRecordScriptDialog(null);
                    } else {
                        cancelScriptRecording();
                    }
                }
            });
        }
        return recordButton;
    }

    private JToolBar.Separator getToolbarSeparator() {
        if (toolbarSeparator == null) {
            toolbarSeparator = new JToolBar.Separator();
        }
        return toolbarSeparator;
    }

    public void cancelScriptRecording() {
        if (scriptNodeRecording != null) {
            // Turn recording off for the 'current' script being recording
            getZestTreeModel().getScriptWrapper(scriptNodeRecording).setRecording(false);
            getZestTreeModel().nodeChanged(scriptNodeRecording);
            scriptNodeRecording = null;
        }
        recordingNode = null;
        getRecordButton().setSelected(false);
    }

    public void setRecording(ScriptNode node, boolean record) {
        if (node != null && node.getUserObject() instanceof ZestScriptWrapper) {
            ZestScriptWrapper script = (ZestScriptWrapper) node.getUserObject();
            script.setRecording(record);
            getZestTreeModel().nodeChanged(node);
            if (node.equals(scriptNodeRecording)) {
                // User has cancelled the recording via the right click option,
                // keep the button in step
                cancelScriptRecording();
            }
        }
    }

    public void redact(ScriptNode node, String replace, String replaceWith, boolean recurse) {
        if (ZestZapUtils.getElement(node) instanceof ZestRequest) {
            ZestRequest request = (ZestRequest) ZestZapUtils.getElement(node);
            this.replaceInResponse(request, replace, replaceWith);
            this.updated(node);
        }
        if (recurse) {
            for (int i = 0; i < node.getChildCount(); i++) {
                this.redact((ScriptNode) node.getChildAt(i), replace, replaceWith, true);
            }
        }
        // Good chance the current response has been changed
        this.refreshMessage();
    }

    public void perameterize(ZestScriptWrapper script, ScriptNode node, ZestRequest request, String replace,
            String token, boolean replaceInCurrent, boolean replaceInAdded) {
        script.getZestScript().getParameters().addVariable(token, replace);
        token = script.getZestScript().getParameters().getTokenStart() + token
                + script.getZestScript().getParameters().getTokenEnd();
        if (replaceInCurrent) {
            ZestStatement stmt = script.getZestScript().getNext();
            while (stmt != null) {
                if (stmt instanceof ZestRequest) {
                    this.replaceInRequest((ZestRequest) stmt, replace, token);
                }
                stmt = stmt.getNext();
            }
            // All nodes could have changed
            this.refreshNode(this.getZestTreeModel().getScriptWrapperNode(node));

        } else {
            this.replaceInRequest(request, replace, token);
            this.updated(node);
        }
        if (replaceInAdded) {
            // TODO support tokens in added reqs
        }
        // Good chance the current response has been changed
        if (View.isInitialised()) {
            this.refreshMessage();
            // Show scripts dialog, selecting the Parameters tab
            this.dialogManager.showZestEditScriptDialog(node, script, false, 1);
        }
    }

    private void replaceInResponse(ZestRequest request, String replace, String replaceWith) {
        ZestResponse resp = request.getResponse();
        if (resp != null) {
            request.setResponse(new ZestResponse(request.getUrl(), resp.getHeaders().replace(replace, replaceWith),
                    resp.getBody().replace(replace, replaceWith), +resp.getStatusCode(),
                    resp.getResponseTimeInMs()));
        }
    }

    private void replaceInRequest(ZestRequest request, String replace, String replaceWith) {
        ZestResponse resp = request.getResponse();
        if (resp != null) {
            request.setUrlToken(request.getUrl().toString().replace(replace, replaceWith));
            request.setHeaders(request.getHeaders().replace(replace, replaceWith));
            request.setData(request.getData().replace(replace, replaceWith));
        }
    }

    @Override
    public String getAuthor() {
        return Constant.ZAP_TEAM;
    }

    @Override
    public String getDescription() {
        return Constant.messages.getString("zest.desc");
    }

    @Override
    public URL getURL() {
        try {
            return new URL(Constant.ZAP_HOMEPAGE);
        } catch (MalformedURLException e) {
            return null;
        }
    }

    public ScriptNode add(ZestScriptWrapper script, boolean display) {
        logger.debug("add script " + script.getName());
        ScriptNode node = this.getExtScript().addScript(script, display);
        this.display(script, node, true);
        if (script.isRecording()) {
            scriptNodeRecording = node;
            // OK, I admit I dont know why this line is required .. but it is ;)
            ((ZestScriptWrapper) node.getUserObject()).setRecording(true);
            getZestTreeModel().nodeChanged(scriptNodeRecording);
        }
        return node;
    }

    public void display(ZestScriptWrapper script, ScriptNode node, boolean expand) {
        if (View.isInitialised() && this.getExtScript().getScriptUI() != null) {
            this.getExtScript().getScriptUI().selectNode(node, expand);
            this.getExtScript().getScriptUI().displayScript(script);
        }
    }

    public void display(ScriptNode node, boolean expand) {
        if (node == null) {
            return;
        }
        logger.debug("Display node=" + node.getNodeName() + " expand=" + expand);
        if (View.isInitialised() && this.getExtScript().getScriptUI() != null) {
            this.getExtScript().getScriptUI().displayScript(this.getZestTreeModel().getScriptWrapper(node));
            this.getExtScript().getScriptUI().selectNode(node, expand);
        }
    }

    public void updated(ScriptNode node) {
        if (node == null) {
            return;
        }
        logger.debug("Updated node=" + node.getNodeName());
        this.getZestTreeModel().update(node);
        ZestScriptWrapper sw = this.getZestTreeModel().getScriptWrapper(node);
        sw.setChanged(true);

        if (this.getExtScript().getScriptUI() != null && this.getExtScript().getScriptUI().isScriptDisplayed(sw)) {
            // We need to do this to prevent the UI slating any changes
            this.getExtScript().getScriptUI().displayScript(sw);
        }
    }

    public List<ScriptNode> getAllZestScriptNodes() {
        List<ScriptNode> list = new ArrayList<ScriptNode>();

        for (ScriptType type : this.getExtScript().getScriptTypes()) {
            for (ScriptNode node : this.getExtScript().getTreeModel().getNodes(type.getName())) {
                if (ZestZapUtils.isZestNode(node)) {
                    list.add(node);
                }
            }
        }
        return Collections.unmodifiableList(list);
    }

    public List<ScriptNode> getZestScriptNodes(String type) {
        List<ScriptNode> list = new ArrayList<ScriptNode>();

        for (ScriptNode node : this.getExtScript().getTreeModel().getNodes(type)) {
            if (ZestZapUtils.isZestNode(node)) {
                list.add(node);
            }
        }
        return Collections.unmodifiableList(list);
    }

    public List<ScriptNode> getZestScriptNodesWithCapability(String capability) {
        List<ScriptNode> scriptNodes = new ArrayList<>();
        for (ScriptType scriptType : getExtScript().getScriptTypes()) {
            if (scriptType.hasCapability(capability)) {
                for (ScriptNode node : getZestScriptNodes(scriptType.getName())) {
                    scriptNodes.add(node);
                }
            }
        }
        return Collections.unmodifiableList(scriptNodes);
    }

    public List<ScriptWrapper> getZestScripts(String type) {
        List<ScriptWrapper> list = new ArrayList<ScriptWrapper>();
        for (ScriptWrapper sw : this.getExtScript().getScripts(type)) {
            if (sw.getEngineName().equals(ZestScriptEngineFactory.NAME)) {
                list.add(sw);
            }
        }
        return Collections.unmodifiableList(list);
    }

    public void addToParent(ScriptNode parent, SiteNode sn, String prefix) {
        try {
            this.addToParent(parent, sn.getHistoryReference().getHttpMessage(), prefix);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }

    public void addToParent(ScriptNode parent, HttpMessage msg, String prefix) {
        if (parent == null) {
            // They're gone for the 'new script' option...
            logger.debug("addToParent parent=null msg=" + msg.getRequestHeader().getURI());
            this.dialogManager.showZestEditScriptDialog(null, null, prefix, true);
            if (msg != null) {
                this.dialogManager.addDeferedMessage(msg);
            }
        } else {
            logger.debug("addToParent parent=" + parent.getNodeName() + " msg=" + msg.getRequestHeader().getURI());

            try {
                ZestRequest req = ZestZapUtils.toZestRequest(msg, false, this.getParam());
                ZestScriptWrapper zsw = this.getZestTreeModel().getScriptWrapper(parent);

                ZestScript script = zsw.getZestScript();
                ZestElement parentZe = ZestZapUtils.getElement(parent);

                if (parentZe instanceof ZestScript) {
                    script.add(req);
                } else if (parentZe instanceof ZestConditional) {
                    if (ZestZapUtils.getShadowLevel(parent) == 2) {
                        ((ZestConditional) ZestZapUtils.getElement(parent)).addElse(req);
                    } else {
                        ((ZestConditional) ZestZapUtils.getElement(parent)).addIf(req);
                    }
                } else if (parentZe instanceof ZestLoop<?>) {
                    ((ZestLoop<?>) ZestZapUtils.getElement(parent)).addStatement(req);
                } else {
                    throw new IllegalArgumentException(
                            "Unexpected parent node: " + parentZe.getElementType() + " " + parent.getNodeName());
                }

                if (zsw.isIncStatusCodeAssertion()) {
                    req.addAssertion(new ZestAssertion(
                            new ZestExpressionStatusCode(msg.getResponseHeader().getStatusCode())));
                }
                if (zsw.isIncLengthAssertion()) {
                    req.addAssertion(new ZestAssertion(new ZestExpressionLength(ZestVariables.RESPONSE_BODY,
                            getResponseBodyLength(msg), zsw.getLengthApprox())));
                }

                if (getExtACSRF() != null) {
                    // Identify and CSRF tokens being used
                    List<AntiCsrfToken> acsrfTokens = getExtACSRF().getTokens(msg);
                    for (AntiCsrfToken acsrf : acsrfTokens) {
                        String var = acsrfTokenToVar.get(acsrf.getValue());
                        if (var != null) {
                            logger.debug("Replacing ACSRF value " + acsrf.getValue() + " with variable " + var);
                            this.replaceInRequest(req, acsrf.getValue(), script.getParameters().getTokenStart()
                                    + var + script.getParameters().getTokenEnd());
                        }
                    }
                }

                // Update tree
                ScriptNode reqNode = this.getZestTreeModel().addToNode(parent, req);

                if (getExtACSRF() != null) {
                    // Create assignments for any ACSRF tokens
                    Source src = new Source(msg.getResponseBody().toString());
                    List<AntiCsrfToken> acsrfTokens = getExtACSRF().getTokensFromResponse(msg, src);
                    for (AntiCsrfToken acsrf : acsrfTokens) {
                        ZestAssignFieldValue zafv = new ZestAssignFieldValue();
                        int id = 1;
                        Set<String> names = script.getVariableNames();
                        while (names.contains("csrf" + id)) {
                            id++;
                        }
                        zafv.setVariableName("csrf" + id);
                        ZestFieldDefinition fd = new ZestFieldDefinition();
                        fd.setFormIndex(acsrf.getFormIndex());
                        fd.setFieldName(acsrf.getName());
                        // Record mapping of value to variable name for later
                        // replacement
                        logger.debug("Recording ACSRF value " + acsrf.getValue() + " against variable "
                                + zafv.getVariableName());
                        acsrfTokenToVar.put(acsrf.getValue(), zafv.getVariableName());
                        zafv.setFieldDefinition(fd);
                        this.addToParent(parent, zafv);
                    }
                }

                this.updated(reqNode);
                this.display(reqNode, false);

            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            }
        }
    }

    private static int getResponseBodyLength(HttpMessage message) {
        // The following code mimics the behaviour of HttpMethodBase.getResponseBodyAsString() which
        // is the method used by the
        // Zest engine to obtain the response body after sending a request.
        byte[] body = message.getResponseBody().getBytes();
        String charset = message.getResponseHeader().getCharset();
        if (charset == null) {
            charset = StandardCharsets.ISO_8859_1.name();
        }

        try {
            return new String(body, charset).length();
        } catch (UnsupportedEncodingException e) {
            return new String(body).length();
        }
    }

    public void addToRequest(ScriptNode node, ZestRequest req, ZestAssertion assertion) {
        req.addAssertion(assertion);
        if (node != null) {
            ScriptNode child = this.getZestTreeModel().addToNode(node, assertion);
            this.updated(child);
            this.display(child, false);
        } else {
            throw new IllegalArgumentException(
                    "Failed to find ZestRequest in tree " + ZestZapUtils.toUiString(req));
        }
    }

    private ScriptNode addAfterRequest(ZestScript script, ScriptNode parentNode, ScriptNode childNode,
            ZestStatement existingChild, ZestStatement newChild) {
        script.add(script.getIndex(existingChild) + 1, newChild);
        ScriptNode child = this.getZestTreeModel().addAfterNode(parentNode, childNode, newChild);
        this.updated(child);
        this.display(child, false);
        return child;
    }

    private ScriptNode addBeforeRequest(ZestScript script, ScriptNode parentNode, ScriptNode childNode,
            ZestStatement existingChild, ZestStatement newChild) {
        script.add(script.getIndex(existingChild), newChild);
        ScriptNode child = this.getZestTreeModel().addBeforeNode(parentNode, childNode, newChild);
        this.updated(child);
        this.display(child, false);
        return child;
    }

    public final ScriptNode addToParent(ScriptNode parent, ZestExpression newExp) {
        logger.debug("addToParent parent=" + parent.getNodeName() + " new=" + newExp.toString());
        ScriptNode node;
        ZestElement parentZe = ZestZapUtils.getElement(parent);
        if (parentZe instanceof ZestConditional) {
            ZestConditional pzc = (ZestConditional) parentZe;
            if (pzc.getRootExpression() != null) {
                this.delete((ScriptNode) parent.getChildAt(0));
            }
            pzc.setRootExpression(newExp); // removes the previous root
            // expression!
            node = this.getZestTreeModel().addToNode(parent, newExp);
            this.updated(parent);
            this.display(parent, true);
        } else if (parentZe instanceof ZestStructuredExpression) {
            ZestStructuredExpression pzse = (ZestStructuredExpression) parentZe;
            pzse.addChildCondition(newExp);
            node = this.getZestTreeModel().addToNode(parent, newExp);
        } else {
            throw new IllegalArgumentException("Unexpected parent node: "
                    + ZestZapUtils.getElement(parent).getElementType() + " ==> " + parent.getNodeName());
        }
        this.updated(node);
        this.display(node, true);
        return node;
    }

    public final ScriptNode addToParent(ScriptNode parent, ZestStatement newChild) {
        return this.addToParent(parent, newChild, true);
    }

    public final ScriptNode addToParent(ScriptNode parent, ZestStatement newChild, boolean display) {
        logger.debug("addToParent parent=" + parent.getNodeName() + " new=" + newChild.getElementType());
        ScriptNode node;
        ZestElement parentElement = ZestZapUtils.getElement(parent);
        if (parentElement instanceof ZestScript) {
            ZestScript zc = (ZestScript) parentElement;
            zc.add(newChild);
            node = this.getZestTreeModel().addToNode(parent, newChild);
        } else if (parentElement instanceof ZestConditional) {
            if (ZestZapUtils.getShadowLevel(parent) == 0) {
                parent = (ScriptNode) parent.getParent().getChildAfter(parent);
            }
            ZestConditional zc = (ZestConditional) parentElement;
            if (ZestZapUtils.getShadowLevel(parent) == 2) {
                zc.addElse(newChild);
            } else {
                zc.addIf(newChild);
            }
            node = this.getZestTreeModel().addToNode(parent, newChild);

        } else if (parentElement instanceof ZestLoop<?>) {
            ZestLoop<?> zl = (ZestLoop<?>) parentElement;
            zl.addStatement(newChild);
            node = this.getZestTreeModel().addToNode(parent, newChild);
        } else if (parentElement instanceof ZestStatement) {
            node = this.getZestTreeModel().addAfterNode(parent.getParent(), parent, newChild);
        } else {
            throw new IllegalArgumentException(
                    "Unexpected parent node: " + parentElement + " " + parent.getNodeName());
        }
        this.updated(node);
        if (display) {
            this.display(node, false);
        }
        return node;
    }

    public ScriptNode addAfterRequest(ScriptNode parent, ScriptNode childNode, ZestStatement existingChild,
            ZestStatement newChild) {
        logger.debug("addAfterRequest parent=" + parent.getNodeName() + " existing="
                + existingChild.getElementType() + " new=" + newChild.getElementType());

        if (ZestZapUtils.getElement(parent) instanceof ZestScript) {
            return this.addAfterRequest((ZestScript) ZestZapUtils.getElement(parent), parent, childNode,
                    existingChild, newChild);

        } else if (ZestZapUtils.getElement(parent) instanceof ZestConditional) {
            ZestConditional zc = (ZestConditional) ZestZapUtils.getElement(parent);

            if (ZestZapUtils.getShadowLevel(parent) == 2) {
                zc.addElse(zc.getIndex(existingChild) + 1, newChild);
            } else { // cannot be non shadow
                zc.addIf(zc.getIndex(existingChild) + 1, newChild);
            }
            ScriptNode child = this.getZestTreeModel().addToNodeAt(parent, newChild,
                    zc.getIndex(existingChild) + 1);
            this.updated(child);
            this.display(child, false);
            return child;
        } else if (ZestZapUtils.getElement(parent) instanceof ZestLoop<?>) {
            ZestLoop<?> zl = (ZestLoop<?>) ZestZapUtils.getElement(parent);
            zl.add(zl.getIndex(existingChild) + 1, newChild);
            ScriptNode child = this.getZestTreeModel().addToNodeAt(parent, newChild,
                    zl.getIndex(existingChild) + 1);
            this.updated(child);
            this.display(child, false);
            return child;
        } else {
            throw new IllegalArgumentException(
                    "Unexpected parent node: " + ZestZapUtils.getElement(parent) + " " + parent.getNodeName());
        }
    }

    public ScriptNode addBeforeRequest(ScriptNode parent, ScriptNode childNode, ZestStatement existingChild,
            ZestStatement newChild) {
        logger.debug("addAfterRequest parent=" + parent.getNodeName() + " existing="
                + existingChild.getElementType() + " new=" + newChild.getElementType());

        if (ZestZapUtils.getElement(parent) instanceof ZestScript) {
            return this.addBeforeRequest((ZestScript) ZestZapUtils.getElement(parent), parent, childNode,
                    existingChild, newChild);

        } else if (ZestZapUtils.getElement(parent) instanceof ZestConditional) {
            ZestConditional zc = (ZestConditional) ZestZapUtils.getElement(parent);

            if (ZestZapUtils.getShadowLevel(parent) == 2) {
                zc.addElse(zc.getIndex(existingChild), newChild);
            } else { // cannot be non shadow
                zc.addIf(zc.getIndex(existingChild), newChild);
            }
            ScriptNode child = this.getZestTreeModel().addBeforeNode(parent, childNode, newChild);
            this.updated(child);
            this.display(child, false);
            return child;
        } else if (ZestZapUtils.getElement(parent) instanceof ZestLoop<?>) {
            ZestLoop<?> zl = (ZestLoop<?>) ZestZapUtils.getElement(parent);
            zl.add(zl.getIndex(existingChild), newChild);
            ScriptNode child = this.getZestTreeModel().addBeforeNode(parent, childNode, newChild);
            this.updated(child);
            this.display(child, false);
            return child;
        } else {
            throw new IllegalArgumentException(
                    "Unexpected parent node: " + ZestZapUtils.getElement(parent) + " " + parent.getNodeName());
        }
    }

    public void notifyAlert(Alert alert) {
        if (View.isInitialised()) {
            int row = this.getZestResultsPanel().getModel().getIndex(alert.getMessage());
            if (row >= 0) {
                ZestResultsTableEntry entry = this.getZestResultsPanel().getModel().getEntry(row);
                if (entry != null) {
                    entry.setMessage(alert.getName());
                    entry.setPassed(false);
                    this.getZestResultsPanel().getModel().fireTableRowsUpdated(row, row);
                }
            }
        }
    }

    public void notifyChanged(ZestResultWrapper lastResult) {
        if (View.isInitialised()) {
            try {
                int row = this.getZestResultsPanel().getModel().getIndex(lastResult.getHttpMessage());
                if (row >= 0) {
                    this.getZestResultsPanel().getModel().fireTableRowsUpdated(row, row);
                }
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            }
        }
    }

    public void delete(ScriptNode node) {
        ScriptNode parent = node.getParent();
        this.getZestTreeModel().delete(node);
        this.updated(parent);
        this.display(parent, true);
    }

    public void moveNodeUp(ScriptNode node) {
        ScriptNode prev = (ScriptNode) node.getPreviousSibling();
        while (prev != null && ZestZapUtils.getShadowLevel(prev) > 0) {
            prev = (ScriptNode) prev.getPreviousSibling();
        }
        if (prev == null) {
            return;
        }
        if (ZestZapUtils.getElement(node) instanceof ZestScript) {
            // Ignore
        } else if (ZestZapUtils.getElement(node) instanceof ZestStatement) {
            ZestStatement req = (ZestStatement) ZestZapUtils.getElement(node);
            ZestContainer parent = (ZestContainer) ZestZapUtils.getElement(node.getParent());
            int index = parent.getIndex(req);
            parent.move(index - 1, req);
            this.getZestTreeModel().switchNodes(prev, node);
            if (View.isInitialised() && this.getExtScript().getScriptUI() != null) {
                this.getExtScript().getScriptUI().selectNode(node.getParent(), true);
            }
            this.updated(node);
            this.display(node, false);
        } else if (ZestZapUtils.getElement(node.getParent()) instanceof ZestRequest) {
            ZestRequest parent = (ZestRequest) ZestZapUtils.getElement(node.getParent());
            parent.moveUp(ZestZapUtils.getElement(node));
            this.getZestTreeModel().switchNodes(prev, node);
            if (View.isInitialised() && this.getExtScript().getScriptUI() != null) {
                this.getExtScript().getScriptUI().selectNode(node.getParent(), true);
            }
            this.updated(node);
            this.display(node, false);
        }
    }

    public void moveNodeDown(ScriptNode node) {
        ScriptNode next = (ScriptNode) node.getNextSibling();
        while (next != null && ZestZapUtils.getShadowLevel(next) > 0) {
            next = (ScriptNode) next.getNextSibling();
        }
        if (next == null) {
            logger.error("Cant move node down " + node.getNodeName());
            return;
        }
        if (ZestZapUtils.getElement(node) instanceof ZestScript) {
            // Ignore
        } else if (ZestZapUtils.getElement(node) instanceof ZestStatement) {
            ZestStatement req = (ZestStatement) ZestZapUtils.getElement(node);
            ZestContainer parent = (ZestContainer) ZestZapUtils.getElement(node.getParent());
            int index = parent.getIndex(req);
            parent.move(index + 1, req);
            this.getZestTreeModel().switchNodes(node, next);
            if (View.isInitialised() && this.getExtScript().getScriptUI() != null) {
                this.getExtScript().getScriptUI().selectNode(node.getParent(), true);
            }
            this.updated(node);
            this.display(node, false);

        } else if (ZestZapUtils.getElement(node.getParent()) instanceof ZestRequest) {
            ZestRequest parent = (ZestRequest) ZestZapUtils.getElement(node.getParent());
            parent.moveUp(ZestZapUtils.getElement(node));
            this.getZestTreeModel().switchNodes(node, next);
            if (View.isInitialised() && this.getExtScript().getScriptUI() != null) {
                this.getExtScript().getScriptUI().selectNode(node.getParent(), true);
            }
            this.updated(node);
            this.display(node, false);
        }
    }

    public boolean isSelectedZestOriginalRequestMessage(Message message) {
        if (message == null) {
            return false;
        }
        return View.getSingleton().getRequestPanel().getMessage() != null
                && View.getSingleton().getRequestPanel().getMessage().hashCode() == message.hashCode()
                && this.isSelectedMessage(message);
    }

    public boolean isSelectedZestOriginalResponseMessage(Message message) {
        if (message == null) {
            return false;
        }
        return View.getSingleton().getResponsePanel().getMessage() != null
                && View.getSingleton().getResponsePanel().getMessage().hashCode() == message.hashCode()
                && this.isSelectedMessage(message);
    }

    public ScriptNode getSelectedZestNode() {
        if (this.getExtScript().getScriptUI() == null) {
            return null;
        }
        if (ZestZapUtils.isZestNode(this.getExtScript().getScriptUI().getSelectedNode())) {
            return this.getExtScript().getScriptUI().getSelectedNode();
        }
        return null;
    }

    public ZestElement getSelectedZestElement() {
        if (this.getExtScript().getScriptUI() == null) {
            return null;
        }
        return ZestZapUtils.getElement(this.getExtScript().getScriptUI().getSelectedNode());
    }

    public List<ZestElement> getSelectedZestElements() {
        if (this.getExtScript().getScriptUI() == null) {
            return null;
        }
        List<ScriptNode> nodes = this.getExtScript().getScriptUI().getSelectedNodes();
        LinkedList<ZestElement> elems = new LinkedList<>();
        for (ScriptNode node : nodes) {
            elems.add(ZestZapUtils.getElement(node));
        }
        return Collections.unmodifiableList(elems);
    }

    public boolean isSelectedZestRequestMessage(Message message) {
        if (message == null) {
            return false;
        }
        return View.getSingleton().getRequestPanel().getMessage() != null
                && View.getSingleton().getRequestPanel().getMessage().hashCode() == message.hashCode()
                && this.getZestResultsPanel().isSelectedMessage(message);
    }

    public boolean isSelectedZestResponseMessage(Message message) {
        if (message == null) {
            return false;
        }
        return View.getSingleton().getResponsePanel().getMessage() != null
                && View.getSingleton().getResponsePanel().getMessage().hashCode() == message.hashCode()
                && this.getZestResultsPanel().isSelectedMessage(message);
    }

    public boolean isScriptTree(Component component) {
        return this.getExtScript().getScriptUI() != null && component != null
                && this.getExtScript().getScriptUI().getTreeName().equals(component.getName());
    }

    @Override
    public int getArrangeableListenerOrder() {
        return 0;
    }

    @Override
    public boolean onHttpRequestSend(HttpMessage msg) {
        return true;
    }

    private ScriptNode getDefaultStandAloneScript() {
        ScriptNode node = this.getSelectedZestNode();
        if (node != null) {
            // Theres a selected Zest node, is it a standalone one?
            ZestScriptWrapper script = this.getZestTreeModel().getScriptWrapper(node);
            if (script != null && ExtensionScript.TYPE_STANDALONE.equals(script.getTypeName())) {
                // right type, use if or the script if its not a container
                if (ZestZapUtils.getElement(node) instanceof ZestContainer) {
                    return node;
                } else {
                    return this.getZestTreeModel().getScriptWrapperNode(node);
                }
            }
        }
        // Is there already a default standalone Zest script
        for (ScriptNode zn : this.getZestScriptNodes(ExtensionScript.TYPE_STANDALONE)) {
            if (this.zestTreeModel.getScriptWrapper(zn).getName()
                    .equals(Constant.messages.getString("zest.targeted.script.default"))) {
                return zn;
            }
        }
        // No, create one
        ScriptWrapper sw = new ScriptWrapper();
        sw.setName(Constant.messages.getString("zest.targeted.script.default"));
        sw.setEngine(this.getZestEngineWrapper());
        sw.setEngineName(ZestScriptEngineFactory.NAME);
        sw.setType(this.getExtScript().getScriptType(ExtensionScript.TYPE_STANDALONE));
        ZestScriptWrapper script = new ZestScriptWrapper(sw);
        return this.add(script, false);
    }

    @Override
    public boolean onHttpResponseReceive(final HttpMessage msg) {
        String secProxyHeader = msg.getRequestHeader().getHeader(HTTP_HEADER_X_SECURITY_PROXY);
        if (secProxyHeader != null) {
            String[] vals = secProxyHeader.split(",");
            for (String val : vals) {
                if (VALUE_RECORD.equalsIgnoreCase(val.trim())) {
                    // TODO check script prefix??

                    EventQueue.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                addToParent(getRecordingNode(), msg, null);
                            } catch (Exception e) {
                                logger.error(e.getMessage(), e);
                            }
                        }
                    });

                    break;
                }
            }
        }
        for (final ScriptNode node : getZestScriptNodesWithCapability(ScriptType.CAPABILITY_APPEND)) {
            ZestScriptWrapper zsw = (ZestScriptWrapper) node.getUserObject();
            if (zsw.isRecording()) {
                if (msg.getRequestHeader().getURI().toString().startsWith(zsw.getZestScript().getPrefix())) {
                    EventQueue.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                addToParent(node, msg, null);
                            } catch (Exception e) {
                                logger.error(e.getMessage(), e);
                            }
                        }
                    });
                }
            }
        }

        return true;
    }

    public void setCnpNodes(List<ScriptNode> cnpNodes) {
        this.cnpNodes = cnpNodes;
    }

    public void setCut(boolean cut) {
        this.cutNodes = cut;
    }

    private void pasteExpressionsToNode(ScriptNode parent) {
        for (int i = 0; i < cnpNodes.size(); i++) {
            this.addToParent(parent, (ZestExpression) ZestZapUtils.getElement(cnpNodes.get(i)));
            if (cutNodes) {
                this.delete(cnpNodes.get(i));
            }
        }
    }

    public void pasteToNode(ScriptNode parent) {
        this.pasteToNode(parent, null);
    }

    public void pasteToNode(ScriptNode parent, ScriptNode afterChild) {
        this.pasteToNode(parent, this.cnpNodes, this.cutNodes, null, afterChild);
    }

    public void pasteToNode(ScriptNode parent, List<ScriptNode> cnpNodes, boolean cutNodes, ScriptNode beforeChild,
            ScriptNode afterChild) {
        if (cnpNodes != null && cnpNodes.size() > 0) {
            logger.debug("pasteToNode parent=" + parent.getNodeName() + " num children=" + cnpNodes.size() + " cut="
                    + cutNodes + " before=" + beforeChild + " after = " + afterChild);
            if (ZestZapUtils.getElement(cnpNodes.get(0)) instanceof ZestExpression) {
                pasteExpressionsToNode(parent);
            } else {
                ZestScriptWrapper script = this.getZestTreeModel().getScriptWrapper(parent);
                ScriptNode lastNode = null;
                for (int i = 0; i < cnpNodes.size(); i++) {
                    ZestStatement stmt = ((ZestStatement) ZestZapUtils.getElement(cnpNodes.get(i))).deepCopy();
                    if (cutNodes) {
                        this.delete(cnpNodes.get(i));
                    }
                    if (ZestZapUtils.getShadowLevel(cnpNodes.get(i)) == 0 && (stmt.isPassive()
                            || !ExtensionPassiveScan.SCRIPT_TYPE_PASSIVE.equals(script.getTypeName()))) {
                        // Dont paste non passive statements into a passive script
                        if (afterChild != null) {
                            lastNode = this.addAfterRequest(parent, afterChild,
                                    (ZestStatement) ZestZapUtils.getElement(afterChild), stmt);
                            // Dont want to reverse the order
                            afterChild = lastNode;
                        } else if (beforeChild != null) {
                            lastNode = this.addBeforeRequest(parent, beforeChild,
                                    (ZestStatement) ZestZapUtils.getElement(beforeChild), stmt);
                            // Deliberately using afterChild here so that subsequent nodes are added
                            // after this one
                            afterChild = lastNode;
                        } else {
                            lastNode = this.addToParent(parent, stmt);
                        }
                    }
                }
                refreshNode(parent); // refreshes the subtree starting from the parent
                // Display the last node, otherwise the parent will be displayed
                // if we've done a delete
                this.display(lastNode, false);
            }
        }
    }

    public void refreshNode(ScriptNode node) {
        if (node.isLeaf()) {
            return;
        } else {
            for (int i = 0; i < node.getChildCount(); i++) {
                this.getZestTreeModel().update((ScriptNode) node.getChildAt(i));
                refreshNode((ScriptNode) node.getChildAt(i));
            }
        }
    }

    private boolean canPasteIntoPassiveElement(ScriptNode node) {
        if (!(ZestZapUtils.getElement(node) instanceof ZestConditional)
                && !(ZestZapUtils.getElement(node) instanceof ZestActionFail)) {
            return false;
        }
        for (int i = 0; i < node.getChildCount(); i++) {
            if (!canPasteIntoPassiveElement((ScriptNode) node.getChildAt(i))) {
                return false;
            }
        }
        if (node.getNextSibling() != null && ZestZapUtils.getShadowLevel((ScriptNode) node.getNextSibling()) > 0) {
            // The next nodes are shadow ones, eg a then or an else node - need
            // to check
            // these too
            while (ZestZapUtils.getShadowLevel((ScriptNode) node.getNextSibling()) > 0) {
                if (!canPasteIntoPassiveElement(((ScriptNode) node.getNextSibling()))) {
                    return false;
                }
                node = (ScriptNode) node.getNextSibling();
            }
        }
        return true;
    }

    public boolean canPasteNodesTo(ScriptNode node) {
        if (this.cnpNodes == null) {
            return false;
        }
        boolean isPassive = false;

        ZestScriptWrapper script = this.getZestTreeModel().getScriptWrapper(node);

        if (ExtensionPassiveScan.SCRIPT_TYPE_PASSIVE.equals(script.getType().getName())) {
            isPassive = true;
        }

        for (ScriptNode cnpNode : this.cnpNodes) {
            if (cnpNode.isNodeDescendant(node)) {
                // Cant paste into a descendant of one of the cut/copied nodes
                return false;
            }
            if (isPassive && !this.canPasteIntoPassiveElement(cnpNode)) {
                return false;
            }
        }
        return true;
    }

    protected void refreshMessage() {
        ZestElement ze = this.getSelectedZestElement();
        if (ze != null && ze instanceof ZestRequest) {
            displayMessage((ZestRequest) ze);
        } else {
            clearMessage();
        }
    }

    public void displayMessage(ZestRequest ze) {
        if (!View.isInitialised()) {
            return;
        }
        try {
            lastSelectedMessage = ZestZapUtils.toHttpMessage(ze, ze.getResponse());
            if (lastSelectedMessage == null) {
                return;
            }

            View.getSingleton().displayMessage(lastSelectedMessage);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }

    private void clearMessage() {
        if (!View.isInitialised()) {
            return;
        }
        lastSelectedMessage = null;
        View.getSingleton().displayMessage(lastSelectedMessage);
    }

    public List<ScriptNode> getSelectedZestNodes() {
        List<ScriptNode> list = new ArrayList<ScriptNode>();
        if (this.getExtScript().getScriptUI() == null) {
            return list;
        }
        for (ScriptNode node : this.getExtScript().getScriptUI().getSelectedNodes()) {
            if (ZestZapUtils.isZestNode(node)) {
                list.add(node);
            }
        }
        return Collections.unmodifiableList(list);
    }

    public void addResultToList(ZestResultWrapper href) {
        this.getZestResultsPanel().getModel().add(href);
        this.getZestResultsPanel().setTabFocus();
    }

    public void failLastResult(Exception e) {
        int lastRow = this.getZestResultsPanel().getModel().getRowCount() - 1;
        ZestResultWrapper zrw = this.getZestResultsPanel().getModel().getHistoryReference(lastRow);
        zrw.setPassed(false);
        // TODO use toUiFailureString varient?
        // zrw.setMessage(ZestZapUtils.toUiFailureString(za, response));
        zrw.setMessage(e.getMessage());
        this.getZestResultsPanel().getModel().fireTableRowsUpdated(lastRow, lastRow);
    }

    public boolean isSelectedMessage(Message msg) {
        return lastSelectedMessage != null && lastSelectedMessage.equals(msg);
    }

    public void addMouseListener(MouseAdapter adapter) {
    }

    private void addWindowLaunch(ScriptNode node, String handle, String url) {
        ZestScriptWrapper sw = this.getZestTreeModel().getScriptWrapper(node);
        if (!sw.getZestScript().getClientWindowHandles().contains(handle)) {
            final ZestClientLaunch launch = new ZestClientLaunch(handle, "firefox", url);

            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        addToParent(getRecordingNode(), launch);
                    } catch (Exception e) {
                        logger.error(e.getMessage(), e);
                    }
                }
            });
        }
    }

    private void addWindowHandle(ScriptNode node, String handle, String url) {
        ZestScriptWrapper sw = this.getZestTreeModel().getScriptWrapper(node);
        if (!sw.getZestScript().getClientWindowHandles().contains(handle)) {
            final ZestClientWindowHandle winHandle = new ZestClientWindowHandle(handle, url, false);

            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        addToParent(getRecordingNode(), winHandle);
                    } catch (Exception e) {
                        logger.error(e.getMessage(), e);
                    }
                }
            });
        }
    }

    private String getWindowHandle(JSONObject clientMessage, ScriptNode node, String windowId, String url) {
        String windowHandle = this.clientUrlToWindowHandle.get(url);
        if (windowHandle != null) {
            return windowHandle;
        }
        windowHandle = "WIN-" + recordingWinId++;
        if (startRecordingUrl != null && startRecordingUrl.equals(url)) {
            this.addWindowLaunch(node, windowHandle, url);
            startRecordingUrl = null;
        } else {
            this.addWindowHandle(node, windowHandle, url);
        }
        this.clientUrlToWindowHandle.put(url, windowHandle);
        return windowHandle;
    }

    public void startClientRecording(String uri) {
        clientUrlToWindowHandle.clear();
        startRecordingUrl = uri;
        recordingWinId = 0;
        // And turn off the recording button, at least for now
        this.getRecordButton().setSelected(false);
    }

    public void setRecordingNode(ScriptNode node) {
        recordingNode = node;
    }

    private ScriptNode getRecordingNode() {
        if (recordingNode != null) {
            return recordingNode;
        }
        return this.getDefaultStandAloneScript();
    }

    public void clientMessageReceived(JSONObject clientMessage, String windowId, String url) {
        ZestStatement stmt = null;
        String windowHandle;
        final ScriptNode clientRecordingNode = getRecordingNode();

        if (clientMessage.getString("type").equals("heartbeat")) {
            // If this is a new window, get a handle to it
            this.getWindowHandle(clientMessage, getRecordingNode(), windowId, url);
            return;
        }

        if (!clientMessage.containsKey("data")) {
            return;
        }

        try {
            String data = clientMessage.getString("data");
            windowHandle = this.getWindowHandle(clientMessage, clientRecordingNode, windowId, url);

            if ("a click event happened!".equals(data)) {
                ZestClientElementClick clientStmt = new ZestClientElementClick();
                clientStmt.setWindowHandle(windowHandle);
                clientStmt.setType("xpath");
                clientStmt.setElement(clientMessage.getString("originalTargetPath"));
                stmt = clientStmt;
            } else if ("a keypress event happened!".equals(data)) {
                String element = clientMessage.getString("originalTargetPath");
                String eventDataStr = clientMessage.getString("eventData");
                JSONObject eventDataObj = JSONObject.fromObject(eventDataStr);
                String key = eventDataObj.getString("key");
                boolean appended = false;

                if (key.length() > 1) {
                    // Ignore all 'control' keys, at least initially
                    logger.debug("Client recording, ignoring " + key);
                } else {
                    if (getRecordingNode().getChildCount() > 0) {
                        // We get key presses one at a time - try to combine them up where possible
                        ScriptNode lastChild = (ScriptNode) clientRecordingNode.getLastChild();
                        ZestElement lastElement = ZestZapUtils.getElement(lastChild);
                        if (lastElement != null && lastElement instanceof ZestClientElementSendKeys) {
                            ZestClientElementSendKeys sk = (ZestClientElementSendKeys) lastElement;
                            if (sk.getWindowHandle().equals(windowHandle) && sk.getType().equals("xpath")
                                    && sk.getElement().equals(element)) {
                                sk.setValue(sk.getValue() + key);
                                getZestTreeModel().nodeChanged(lastChild);
                                this.refreshNode(clientRecordingNode);
                                appended = true;
                            }
                        }
                    }

                    if (!appended) {
                        ZestClientElementSendKeys sk = new ZestClientElementSendKeys();
                        sk.setWindowHandle(windowHandle);
                        sk.setType("xpath");
                        sk.setElement(clientMessage.getString("originalTargetPath"));
                        sk.setValue(key);
                        stmt = sk;
                    }
                }
            }
        } catch (Exception e1) {
            logger.error(e1.getMessage(), e1);
        }

        if (stmt != null) {
            final ZestStatement stmtFinal = stmt;

            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        addToParent(clientRecordingNode, stmtFinal, false);
                    } catch (Exception e) {
                        logger.error(e.getMessage(), e);
                    }
                }
            });
        }
    }

    /**/
    @Override
    public void preInvoke(ScriptWrapper script) {
        ScriptEngineWrapper ewrap = this.getExtScript().getEngineWrapper(ZestScriptEngineFactory.NAME);
        if (ewrap == null) {
            logger.error("Failed to find engine Mozilla Zest");
        } else if (script instanceof ZestScriptWrapper) {
            this.getZestScriptEngineFactory().setRunner(new ZestZapRunner(this, (ZestScriptWrapper) script));
            clearResults();
            this.lastRunScript = ((ZestScriptWrapper) script).getZestScript();
        }
    }

    public void clearResults() {
        if (View.isInitialised()) {
            // Clear the previous results
            this.getZestResultsPanel().getModel().clear();
        }
    }

    @Override
    public void refreshScript(ScriptWrapper script) {
        // Ignore
    }

    @Override
    public void scriptAdded(ScriptWrapper script, boolean display) {
        if (script.getEngineName().equals(ZestScriptEngineFactory.NAME)) {

            ScriptNode typeNode = this.getExtScript().getTreeModel().getTypeNode(script.getTypeName());
            if (typeNode == null) {
                logger.error("Failed to find type node: " + script.getTypeName());

                typeNode = this.getExtScript().getTreeModel().getTypeNode(ExtensionScript.TYPE_STANDALONE);
            }

            logger.debug("Adding Zest script to tree");

            ZestScriptWrapper zsw;
            if (script instanceof ZestScriptWrapper) {
                zsw = (ZestScriptWrapper) script;
            } else {
                zsw = new ZestScriptWrapper(script);
                if (zsw.getName() == null) {
                    zsw.setName(script.getName());
                }
            }

            ScriptNode parentNode = this.getExtScript().getTreeModel().getNodeForScript(script);
            parentNode.setUserObject(zsw);

            this.getZestTreeModel().addScript(parentNode, zsw);

            if (display && View.isInitialised()) {
                this.updated(parentNode);
                this.display(zsw, parentNode, true);
                this.dialogManager.showZestEditScriptDialog(parentNode, zsw, false);
            }
        }
    }

    @Override
    public void scriptRemoved(ScriptWrapper script) {
        // Ignore

    }

    @Override
    public void scriptChanged(ScriptWrapper script) {
        // Ignore
    }

    @Override
    public void scriptError(ScriptWrapper script) {
        // Ignore
    }

    @Override
    public void scriptSaved(ScriptWrapper script) {
        // Ignore
    }

    @Override
    public void templateAdded(ScriptWrapper script, boolean display) {
        /*
         * TODO ?? if (View.isInitialised() && this.getExtScript().getScriptUI()
         * != null &&
         * script.getEngineName().equals(ZestScriptEngineFactory.NAME)) {
         *
         * ScriptNode typeNode = this.getExtScript().getTreeModel()
         * .getTypeNode(script.getTypeName()); if (typeNode == null) {
         * logger.error("Failed to find type node: " + script.getTypeName());
         *
         * typeNode = this.getExtScript().getTreeModel()
         * .getTypeNode(ExtensionScript.TYPE_STANDALONE); }
         *
         * logger.debug("Adding Zest script to tree");
         *
         * ZestScriptWrapper zsw = new ZestScriptWrapper(script); if
         * (zsw.getName() == null) { zsw.setName(script.getName()); }
         *
         * ScriptNode parentNode = this.getExtScript().getTreeModel()
         * .getNodeForScript(script); parentNode.setUserObject(zsw);
         *
         * this.getZestTreeModel().addScript(parentNode, zsw); }
         */
    }

    @Override
    public void templateRemoved(ScriptWrapper script) {
        // Ignore
    }

    public ZestScript getLastRunScript() {
        return lastRunScript;
    }

    @Override
    public List<Class<? extends Extension>> getDependencies() {
        return EXTENSION_DEPENDENCIES;
    }

    /**
     * Set enabled for the specified node and all of its children
     *
     * @param node
     * @param enabled
     */
    public void setEnabled(ScriptNode node, boolean enabled) {
        if (ZestZapUtils.getElement(node) instanceof ZestStatement) {
            ZestStatement stmt = (ZestStatement) ZestZapUtils.getElement(node);
            stmt.setEnabled(enabled);
            for (int i = 0; i < node.getChildCount(); i++) {
                this.setEnabled((ScriptNode) node.getChildAt(i), enabled);
            }
            this.updated(node);
        }
    }

    public ZestParam getParam() {
        if (param == null) {
            param = new ZestParam();
        }
        return param;
    }

    private OptionsZestPanel getOptionsPanel() {
        if (optionsZestPanel == null) {
            optionsZestPanel = new OptionsZestPanel(this);
        }
        return optionsZestPanel;
    }

    /**
     * Return all of the requests in the script ScriptWrapper is deliberately used to make it easier
     * to call this method by reflection
     *
     * @param script
     * @return
     */
    public List<HttpMessage> getAllRequestsInScript(ScriptWrapper script) {
        ArrayList<HttpMessage> requests = new ArrayList<HttpMessage>();

        if (!(script instanceof ZestScriptWrapper)) {
            throw new InvalidParameterException(script.getClass().getCanonicalName());
        }

        ZestScriptWrapper sw = (ZestScriptWrapper) script;
        for (ZestStatement stmt : sw.getZestScript().getStatements()) {
            try {
                if (stmt.getElementType().equals("ZestRequest")) {
                    ZestRequest req = (ZestRequest) stmt;
                    HttpMessage scrMessage = ZestZapUtils.toHttpMessage(req, req.getResponse());
                    requests.add(scrMessage);
                }
            } catch (Exception e) {
                logger.debug(
                        "Exception occurred while fetching HttpMessages from sequence script: " + e.getMessage());
            }
        }
        return requests;
    }

    /** A {@code SessionChangedListener} for view/UI related functionalities. */
    private class ViewSessionChangedListener implements SessionChangedListener {

        @Override
        public void sessionAboutToChange(Session session) {
            clearResults();
        }

        @Override
        public void sessionChanged(Session session) {
            // Nothing to do.
        }

        @Override
        public void sessionModeChanged(Mode mode) {
            // Nothing to do.
        }

        @Override
        public void sessionScopeChanged(Session session) {
            // Nothing to do.
        }
    }
}