/*
 * 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.addon.spider;

import java.awt.Dimension;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import org.apache.commons.httpclient.URI;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.control.Control;
import org.parosproxy.paros.model.Model;
import org.parosproxy.paros.model.Session;
import org.zaproxy.addon.spider.filters.HttpPrefixFetchFilter;
import org.zaproxy.zap.extension.users.ExtensionUserManagement;
import org.zaproxy.zap.model.Context;
import org.zaproxy.zap.model.StructuralNode;
import org.zaproxy.zap.model.StructuralSiteNode;
import org.zaproxy.zap.model.Target;
import org.zaproxy.zap.users.User;
import org.zaproxy.zap.view.StandardFieldsDialog;

@SuppressWarnings("serial")
public class SpiderDialog extends StandardFieldsDialog {

    private static final String FIELD_START = "spider.custom.label.start";
    private static final String FIELD_CONTEXT = "spider.custom.label.context";
    private static final String FIELD_USER = "spider.custom.label.user";
    private static final String FIELD_RECURSE = "spider.custom.label.recurse";
    private static final String FIELD_SUBTREE_ONLY = "spider.custom.label.spiderSubtreeOnly";
    private static final String FIELD_ADVANCED = "spider.custom.label.adv";
    private static final String FIELD_MAX_DEPTH = "spider.custom.label.maxDepth";
    private static final String FIELD_MAX_CHILDREN = "spider.custom.label.maxChildren";
    private static final String FIELD_MAX_DURATION = "spider.custom.label.maxDuration";
    private static final String FIELD_MAX_PARSE_SIZE_BYTES =
            "spider.custom.label.maxParseSizeBytes";
    private static final String FIELD_SEND_REFERER = "spider.custom.label.sendReferer";
    private static final String FIELD_ACCEPT_COOKIES = "spider.custom.label.acceptcookies";
    private static final String FIELD_PROCESS_FORMS = "spider.custom.label.processForms";
    private static final String FIELD_POST_FORMS = "spider.custom.label.postForms";
    private static final String FIELD_PARSE_COMMENTS = "spider.custom.label.parseComments";
    private static final String FIELD_PARSE_ROBOTS = "spider.custom.label.parseRobots";
    private static final String FIELD_PARSE_SITEMAP = "spider.custom.label.sitemap";
    private static final String FIELD_PARSE_SVN = "spider.custom.label.parseSvn";
    private static final String FIELD_PARSE_GIT = "spider.custom.label.parseGit";
    private static final String FIELD_PARSE_DS_STORE = "spider.custom.label.parseDsStore";
    private static final String FIELD_HANDLE_ODATA = "spider.custom.label.handleOdata";

    private static final Logger LOGGER = LogManager.getLogger(SpiderDialog.class);

    private static final long serialVersionUID = 1L;

    private JButton[] extraButtons;

    private ExtensionSpider2 extension;
    private SpiderParam spiderParam;

    /**
     * Flag that holds the previous checked state of the "Subtree Only" checkbox.
     *
     * <p>Used to restore the previous checked state between dialogue invocations.
     *
     * @see #FIELD_SUBTREE_ONLY
     */
    private boolean subtreeOnlyPreviousCheckedState;

    private ExtensionUserManagement extUserMgmt =
            Control.getSingleton().getExtensionLoader().getExtension(ExtensionUserManagement.class);

    private Target target;

    public SpiderDialog(ExtensionSpider2 ext, Frame owner, Dimension dim) {
        super(
                owner,
                "spider.custom.title",
                dim,
                new String[] {"spider.custom.tab.scope", "spider.custom.tab.adv"});
        this.extension = ext;

        // The first time init to the default options set, after that keep own copies
        reset(false);
    }

    public void init(Target target) {
        if (target != null) {
            // If one isn't specified then leave the previously selected one
            this.target = target;
        }
        LOGGER.debug("Init {}", this.target);

        this.removeAllFields();

        this.addTargetSelectField(0, FIELD_START, this.target, true, false);
        this.addComboField(0, FIELD_CONTEXT, new String[] {}, "");
        this.addComboField(0, FIELD_USER, new String[] {}, "");
        setUsers();
        this.addCheckBoxField(0, FIELD_RECURSE, true);
        this.addCheckBoxField(0, FIELD_SUBTREE_ONLY, subtreeOnlyPreviousCheckedState);
        // This option is always read from the 'global' options
        this.addCheckBoxField(0, FIELD_ADVANCED, extension.getSpiderParam().isShowAdvancedDialog());
        this.addPadding(0);

        // Advanced options
        this.addNumberField(1, FIELD_MAX_DEPTH, 0, 19, getSpiderParam().getMaxDepth());
        this.addNumberField(
                1, FIELD_MAX_CHILDREN, 0, Integer.MAX_VALUE, getSpiderParam().getMaxChildren());
        this.addNumberField(
                1, FIELD_MAX_DURATION, 0, Integer.MAX_VALUE, getSpiderParam().getMaxDuration());
        this.addNumberField(
                1,
                FIELD_MAX_PARSE_SIZE_BYTES,
                0,
                Integer.MAX_VALUE,
                getSpiderParam().getMaxParseSizeBytes());
        this.addCheckBoxField(1, FIELD_SEND_REFERER, getSpiderParam().isSendRefererHeader());
        this.addCheckBoxField(1, FIELD_ACCEPT_COOKIES, getSpiderParam().isAcceptCookies());
        this.addCheckBoxField(1, FIELD_PROCESS_FORMS, getSpiderParam().isProcessForm());
        this.addCheckBoxField(1, FIELD_POST_FORMS, getSpiderParam().isPostForm());
        this.addCheckBoxField(1, FIELD_PARSE_COMMENTS, getSpiderParam().isParseComments());
        this.addCheckBoxField(1, FIELD_PARSE_ROBOTS, getSpiderParam().isParseRobotsTxt());
        this.addCheckBoxField(1, FIELD_PARSE_SITEMAP, getSpiderParam().isParseSitemapXml());
        this.addCheckBoxField(1, FIELD_PARSE_SVN, getSpiderParam().isParseSVNEntries());
        this.addCheckBoxField(1, FIELD_PARSE_GIT, getSpiderParam().isParseGit());
        this.addCheckBoxField(1, FIELD_PARSE_DS_STORE, getSpiderParam().isParseDsStore());
        this.addCheckBoxField(
                1, FIELD_HANDLE_ODATA, getSpiderParam().isHandleODataParametersVisited());
        this.addPadding(1);

        if (!getBoolValue(FIELD_PROCESS_FORMS)) {
            setFieldValue(FIELD_POST_FORMS, false);
            getField(FIELD_POST_FORMS).setEnabled(false);
        }

        this.addFieldListener(
                FIELD_CONTEXT,
                new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        setUsers();
                    }
                });
        this.addFieldListener(
                FIELD_PROCESS_FORMS,
                new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        if (getBoolValue(FIELD_PROCESS_FORMS)) {
                            getField(FIELD_POST_FORMS).setEnabled(true);
                        } else {
                            setFieldValue(FIELD_POST_FORMS, false);
                            getField(FIELD_POST_FORMS).setEnabled(false);
                        }
                    }
                });
        this.addFieldListener(
                FIELD_ADVANCED,
                new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        setAdvancedTabs(getBoolValue(FIELD_ADVANCED));
                    }
                });

        if (target != null) {
            // Set up the fields if a node has been specified, otherwise leave as previously set
            this.targetSelected(FIELD_START, this.target);
            this.setUsers();
        }

        if (!extension.getSpiderParam().isShowAdvancedDialog()) {
            // Remove all but the first tab
            this.setAdvancedTabs(false);
        }

        setTabScrollable("spider.custom.tab.adv", true);

        this.pack();
    }

    private SpiderParam getSpiderParam() {
        if (spiderParam == null) {
            // First time in clone the global options, after that keep the last ones the user set
            spiderParam = (SpiderParam) extension.getSpiderParam().clone();
        }
        return spiderParam;
    }

    private void setAdvancedTabs(boolean visible) {
        // Show/hide all except from the first tab
        this.setTabsVisible(new String[] {"spider.custom.tab.adv"}, visible);
    }

    @Override
    public String getHelpIndex() {
        return "addon.spider.dialog";
    }

    @Override
    public void targetSelected(String field, Target node) {
        List<String> ctxNames = new ArrayList<>();
        if (node != null) {
            // The user has selected a new node
            this.target = node;
            if (node.getStartNode() != null) {
                Session session = Model.getSingleton().getSession();
                List<Context> contexts = session.getContextsForNode(node.getStartNode());
                for (Context context : contexts) {
                    ctxNames.add(context.getName());
                }

            } else if (node.getContext() != null) {
                ctxNames.add(node.getContext().getName());
            }
        }
        this.setComboFields(FIELD_CONTEXT, ctxNames, "");
        this.getField(FIELD_CONTEXT).setEnabled(ctxNames.size() > 0);
    }

    private Context getSelectedContext() {
        String ctxName = this.getStringValue(FIELD_CONTEXT);
        if (this.extUserMgmt != null && !this.isEmptyField(FIELD_CONTEXT)) {
            Session session = Model.getSingleton().getSession();
            return session.getContext(ctxName);
        }
        return null;
    }

    private User getSelectedUser() {
        Context context = this.getSelectedContext();
        if (context != null) {
            String userName = this.getStringValue(FIELD_USER);
            List<User> users =
                    this.extUserMgmt.getContextUserAuthManager(context.getId()).getUsers();
            for (User user : users) {
                if (userName.equals(user.getName())) {
                    return user;
                }
            }
        }
        return null;
    }

    private void setUsers() {
        Context context = this.getSelectedContext();
        List<String> userNames = new ArrayList<>();
        if (context != null) {
            List<User> users =
                    this.extUserMgmt.getContextUserAuthManager(context.getId()).getUsers();
            userNames.add(""); // The default should always be 'not specified'
            for (User user : users) {
                userNames.add(user.getName());
            }
        }
        this.setComboFields(FIELD_USER, userNames, "");
        this.getField(FIELD_USER).setEnabled(userNames.size() > 1); // Theres always 1..
    }

    private void reset(boolean refreshUi) {
        // Reset to the global options
        spiderParam = null;
        subtreeOnlyPreviousCheckedState = false;

        if (refreshUi) {
            init(target);
            repaint();
        }
    }

    @Override
    public String getSaveButtonText() {
        return Constant.messages.getString("spider.custom.button.scan");
    }

    @Override
    public JButton[] getExtraButtons() {
        if (extraButtons == null) {
            JButton resetButton =
                    new JButton(Constant.messages.getString("spider.custom.button.reset"));
            resetButton.addActionListener(
                    new java.awt.event.ActionListener() {
                        @Override
                        public void actionPerformed(java.awt.event.ActionEvent e) {
                            reset(true);
                        }
                    });

            extraButtons = new JButton[] {resetButton};
        }
        return extraButtons;
    }

    @Override
    public void save() {
        List<Object> contextSpecificObjects = new ArrayList<>();
        URI startUri = null;
        try {
            // Always include the startUri, this has the side effect
            // of handling URLs that have not been accessed
            startUri = new URI(this.getStringValue(FIELD_START), true);
        } catch (Exception e1) {
            // Ignore - will have been checked in validateParams
        }
        if (this.getBoolValue(FIELD_ADVANCED)) {
            // Set the advanced options
            spiderParam.setMaxDepth(this.getIntValue(FIELD_MAX_DEPTH));
            spiderParam.setMaxDuration(this.getIntValue(FIELD_MAX_DURATION));
            spiderParam.setMaxChildren(this.getIntValue(FIELD_MAX_CHILDREN));
            spiderParam.setMaxParseSizeBytes(this.getIntValue(FIELD_MAX_PARSE_SIZE_BYTES));
            spiderParam.setSendRefererHeader(this.getBoolValue(FIELD_SEND_REFERER));
            spiderParam.setAcceptCookies(this.getBoolValue(FIELD_ACCEPT_COOKIES));
            spiderParam.setProcessForm(this.getBoolValue(FIELD_PROCESS_FORMS));
            spiderParam.setPostForm(this.getBoolValue(FIELD_POST_FORMS));
            spiderParam.setParseComments(this.getBoolValue(FIELD_PARSE_COMMENTS));
            spiderParam.setParseRobotsTxt(this.getBoolValue(FIELD_PARSE_ROBOTS));
            spiderParam.setParseSitemapXml(this.getBoolValue(FIELD_PARSE_SITEMAP));
            spiderParam.setParseSVNEntries(this.getBoolValue(FIELD_PARSE_SVN));
            spiderParam.setParseGit(this.getBoolValue(FIELD_PARSE_GIT));
            spiderParam.setParseDsStore(this.getBoolValue(FIELD_PARSE_DS_STORE));
            spiderParam.setHandleODataParametersVisited(this.getBoolValue(FIELD_HANDLE_ODATA));
            spiderParam.setThreadCount(extension.getSpiderParam().getThreadCount());

            contextSpecificObjects.add(spiderParam);
        }

        if (startUri != null) {
            contextSpecificObjects.add(startUri);

            if (getBoolValue(FIELD_SUBTREE_ONLY)) {
                contextSpecificObjects.add(new HttpPrefixFetchFilter(startUri));
            }
        }

        if (target == null || !this.getStringValue(FIELD_START).equals(getTargetText(target))) {
            // Clear the target as it doesn't match the value entered manually
            target = new Target((StructuralNode) null);
        }

        // Save the adv option permanently for next time
        extension.getSpiderParam().setShowAdvancedDialog(this.getBoolValue(FIELD_ADVANCED));

        target.setRecurse(this.getBoolValue(FIELD_RECURSE));

        if (target.getContext() == null && getSelectedContext() != null) {
            target.setContext(getSelectedContext());
        }

        subtreeOnlyPreviousCheckedState = getBoolValue(FIELD_SUBTREE_ONLY);

        this.extension.startScan(target, getSelectedUser(), contextSpecificObjects.toArray());
    }

    @Override
    public String validateFields() {
        if (Control.Mode.safe == Control.getSingleton().getMode()) {
            // The dialogue shouldn't be shown when in safe mode but if it is warn.
            return Constant.messages.getString("spider.custom.notSafe.error");
        }

        if (this.isEmptyField(FIELD_START)) {
            return Constant.messages.getString("spider.custom.nostart.error");
        }

        boolean noStartUri = true;
        if (!getStringValue(FIELD_START).equals(getTargetText(target))) {
            String url = this.getStringValue(FIELD_START);
            try {
                // Need both constructors as they catch slightly different issues ;)
                new URI(url, true);
                new URL(url);
            } catch (Exception e) {
                return Constant.messages.getString("spider.custom.nostart.error");
            }

            if (Control.getSingleton().getMode() == Control.Mode.protect) {
                if (!extension.isTargetUriInScope(url)) {
                    return Constant.messages.getString("spider.custom.targetNotInScope.error", url);
                }
            }
            noStartUri = false;
        }

        if (this.target != null) {
            if (!this.target.isValid()) {
                return Constant.messages.getString("spider.custom.nostart.error");
            }

            if (Control.getSingleton().getMode() == Control.Mode.protect) {
                String uri = extension.getTargetUriOutOfScope(target);
                if (uri != null) {
                    return Constant.messages.getString("spider.custom.targetNotInScope.error", uri);
                }
            }

            List<StructuralNode> nodes = target.getStartNodes();
            if (nodes != null) {
                for (StructuralNode node : nodes) {
                    if (node instanceof StructuralSiteNode) {
                        noStartUri = false;
                        break;
                    }
                }
            }
        }

        if (getBoolValue(FIELD_SUBTREE_ONLY) && noStartUri) {
            return Constant.messages.getString("spider.custom.noStartSubtreeOnly.error");
        }

        return null;
    }

    /** Resets the spider dialogue to its default state. */
    void reset() {
        target = null;
        reset(true);
    }

    @Override
    public void cancelPressed() {
        this.target = null;
        super.cancelPressed();
    }
}
