#!/usr/bin/env python
# -*- coding: utf-8 -*-

# ***********************IMPORTANT NMAP LICENSE TERMS************************
# *                                                                         *
# * The Nmap Security Scanner is (C) 1996-2022 Nmap Software LLC ("The Nmap *
# * Project"). Nmap is also a registered trademark of the Nmap Project.     *
# *                                                                         *
# * This program is distributed under the terms of the Nmap Public Source   *
# * License (NPSL). The exact license text applying to a particular Nmap    *
# * release or source code control revision is contained in the LICENSE     *
# * file distributed with that version of Nmap or source code control       *
# * revision. More Nmap copyright/legal information is available from       *
# * https://nmap.org/book/man-legal.html, and further information on the    *
# * NPSL license itself can be found at https://nmap.org/npsl/ . This       *
# * header summarizes some key points from the Nmap license, but is no      *
# * substitute for the actual license text.                                 *
# *                                                                         *
# * Nmap is generally free for end users to download and use themselves,    *
# * including commercial use. It is available from https://nmap.org.        *
# *                                                                         *
# * The Nmap license generally prohibits companies from using and           *
# * redistributing Nmap in commercial products, but we sell a special Nmap  *
# * OEM Edition with a more permissive license and special features for     *
# * this purpose. See https://nmap.org/oem/                                 *
# *                                                                         *
# * If you have received a written Nmap license agreement or contract       *
# * stating terms other than these (such as an Nmap OEM license), you may   *
# * choose to use and redistribute Nmap under those terms instead.          *
# *                                                                         *
# * The official Nmap Windows builds include the Npcap software             *
# * (https://npcap.com) for packet capture and transmission. It is under    *
# * separate license terms which forbid redistribution without special      *
# * permission. So the official Nmap Windows builds may not be              *
# * redistributed without special permission (such as an Nmap OEM           *
# * license).                                                               *
# *                                                                         *
# * Source is provided to this software because we believe users have a     *
# * right to know exactly what a program is going to do before they run it. *
# * This also allows you to audit the software for security holes.          *
# *                                                                         *
# * Source code also allows you to port Nmap to new platforms, fix bugs,    *
# * and add new features.  You are highly encouraged to submit your         *
# * changes as a Github PR or by email to the dev@nmap.org mailing list     *
# * for possible incorporation into the main distribution. Unless you       *
# * specify otherwise, it is understood that you are offering us very       *
# * broad rights to use your submissions as described in the Nmap Public    *
# * Source License Contributor Agreement. This is important because we      *
# * fund the project by selling licenses with various terms, and also       *
# * because the inability to relicense code has caused devastating          *
# * problems for other Free Software projects (such as KDE and NASM).       *
# *                                                                         *
# * The free version of Nmap is distributed in the hope that it will be     *
# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of  *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties,        *
# * indemnification and commercial support are all available through the    *
# * Npcap OEM program--see https://nmap.org/oem/                            *
# *                                                                         *
# ***************************************************************************/

import gobject
import gtk


# Prevent loading PyXML
import xml
xml.__path__ = [x for x in xml.__path__ if "_xmlplus" not in x]

from xml.dom import minidom

from zenmapGUI.higwidgets.higlabels import HIGEntryLabel
from zenmapGUI.higwidgets.higbuttons import HIGButton

from zenmapGUI.FileChoosers import AllFilesFileChooserDialog
from zenmapGUI.ProfileHelp import ProfileHelp

from zenmapCore.NmapOptions import NmapOptions, split_quoted, join_quoted
import zenmapCore.I18N  # lgtm[py/unused-import]
from zenmapGUI.ScriptInterface import ScriptInterface


def get_option_check_auxiliary_widget(option, ops, check):
    if option in ("-sI", "-b", "--script", "--script-args", "--exclude", "-p",
            "-D", "-S", "--source-port", "-e", "--ttl", "-iR", "--max-retries",
            "--host-timeout", "--max-rtt-timeout", "--min-rtt-timeout",
            "--initial-rtt-timeout", "--max-hostgroup", "--min-hostgroup",
            "--max-parallelism", "--min-parallelism", "--max-scan-delay",
            "--scan-delay", "-PA", "-PS", "-PU", "-PO", "-PY"):
        return OptionEntry(option, ops, check)
    elif option in ("-d", "-v"):
        return OptionLevel(option, ops, check)
    elif option in ("--excludefile", "-iL"):
        return OptionFile(option, ops, check)
    elif option in ("-A", "-O", "-sV", "-n", "-6", "-Pn", "-PE", "-PP", "-PM",
            "-PB", "-sC", "--script-trace", "-F", "-f", "--packet-trace", "-r",
            "--traceroute"):
        return None
    elif option in ("",):
        return OptionExtras(option, ops, check)
    else:
        assert False, "Unknown option %s" % option


class OptionEntry(gtk.Entry):
    def __init__(self, option, ops, check):
        gtk.Entry.__init__(self)
        self.option = option
        self.ops = ops
        self.check = check
        self.connect("changed", self.changed_cb)
        self.check.connect("toggled", self.check_toggled_cb)
        self.update()

    def update(self):
        if self.ops[self.option] is not None:
            self.set_text(str(self.ops[self.option]))
            self.check.set_active(True)
        else:
            self.set_text("")
            self.check.set_active(False)

    def check_toggled_cb(self, check):
        if check.get_active():
            self.ops[self.option] = self.get_text().decode("UTF-8")
        else:
            self.ops[self.option] = None

    def changed_cb(self, widget):
        self.check.set_active(True)
        self.ops[self.option] = self.get_text().decode("UTF-8")


class OptionExtras(gtk.Entry):
    def __init__(self, option, ops, check):
        gtk.Entry.__init__(self)
        self.ops = ops
        self.check = check
        self.connect("changed", self.changed_cb)
        self.check.connect("toggled", self.check_toggled_cb)
        self.update()

    def update(self):
        if len(self.ops.extras) > 0:
            self.set_text(" ".join(self.ops.extras))
            self.check.set_active(True)
        else:
            self.set_text("")
            self.check.set_active(False)

    def check_toggled_cb(self, check):
        if check.get_active():
            self.ops.extras = [self.get_text().decode("UTF-8")]
        else:
            self.ops.extras = []

    def changed_cb(self, widget):
        self.check.set_active(True)
        self.ops.extras = [self.get_text().decode("UTF-8")]


class OptionLevel(gtk.SpinButton):
    def __init__(self, option, ops, check):
        gtk.SpinButton.__init__(self, gtk.Adjustment(0, 0, 10, 1), 0.0, 0)
        self.option = option
        self.ops = ops
        self.check = check
        self.connect("changed", self.changed_cb)
        self.check.connect("toggled", self.check_toggled_cb)
        self.update()

    def update(self):
        level = self.ops[self.option]
        if level is not None and level > 0:
            self.get_adjustment().set_value(int(level))
            self.check.set_active(True)
        else:
            self.get_adjustment().set_value(0)
            self.check.set_active(False)

    def check_toggled_cb(self, check):
        if check.get_active():
            self.ops[self.option] = int(self.get_adjustment().get_value())
        else:
            self.ops[self.option] = 0

    def changed_cb(self, widget):
        self.check.set_active(True)
        self.ops[self.option] = int(self.get_adjustment().get_value())


class OptionFile(gtk.HBox):
    __gsignals__ = {
        "changed": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ())
    }

    def __init__(self, option, ops, check):
        gtk.HBox.__init__(self)

        self.option = option
        self.ops = ops
        self.check = check

        self.entry = gtk.Entry()
        self.pack_start(self.entry, True, True)
        button = HIGButton(stock=gtk.STOCK_OPEN)
        self.pack_start(button, False)

        button.connect("clicked", self.clicked_cb)

        self.entry.connect("changed", lambda x: self.emit("changed"))
        self.entry.connect("changed", self.changed_cb)
        self.check.connect("toggled", self.check_toggled_cb)
        self.update()

    def update(self):
        if self.ops[self.option] is not None:
            self.entry.set_text(self.ops[self.option])
            self.check.set_active(True)
        else:
            self.entry.set_text("")
            self.check.set_active(False)

    def check_toggled_cb(self, check):
        if check.get_active():
            self.ops[self.option] = self.entry.get_text().decode("UTF-8")
        else:
            self.ops[self.option] = None

    def changed_cb(self, widget):
        self.check.set_active(True)
        self.ops[self.option] = self.entry.get_text().decode("UTF-8")

    def clicked_cb(self, button):
        dialog = AllFilesFileChooserDialog(_("Choose file"))
        if dialog.run() == gtk.RESPONSE_OK:
            self.entry.set_text(dialog.get_filename())
        dialog.destroy()


class TargetEntry(gtk.Entry):
    def __init__(self, ops):
        gtk.Entry.__init__(self)
        self.ops = ops
        self.connect("changed", self.changed_cb)
        self.update()

    def update(self):
        self.set_text(u" ".join(self.ops.target_specs))

    def changed_cb(self, widget):
        self.ops.target_specs = self.get_targets()

    def get_targets(self):
        return split_quoted(self.get_text().decode("UTF-8"))


class OptionTab(object):
    def __init__(self, root_tab, ops, update_command, help_buf):
        actions = {'target': self.__parse_target,
                   'option_list': self.__parse_option_list,
                   'option_check': self.__parse_option_check}

        self.ops = ops
        self.update_command = update_command
        self.help_buf = help_buf

        self.profilehelp = ProfileHelp()
        self.notscripttab = False  # assume every tab is scripting tab
        self.widgets_list = []
        for option_element in root_tab.childNodes:
            if (hasattr(option_element, "tagName") and
                    option_element.tagName in actions.keys()):
                parse_func = actions[option_element.tagName]
                widget = parse_func(option_element)
                self.widgets_list.append(widget)

    def __parse_target(self, target_element):
        label = _(target_element.getAttribute(u'label'))
        label_widget = HIGEntryLabel(label)
        target_widget = TargetEntry(self.ops)
        target_widget.connect("changed", self.update_target)
        return label_widget, target_widget

    def __parse_option_list(self, option_list_element):
        children = option_list_element.getElementsByTagName(u'option')

        label_widget = HIGEntryLabel(
                _(option_list_element.getAttribute(u'label')))
        option_list_widget = OptionList(self.ops)

        for child in children:
            option = child.getAttribute(u'option')
            argument = child.getAttribute(u'argument')
            label = _(child.getAttribute(u'label'))
            option_list_widget.append(option, argument, label)
            self.profilehelp.add_label(option, label)
            self.profilehelp.add_shortdesc(
                    option, _(child.getAttribute(u'short_desc')))
            self.profilehelp.add_example(
                    option, child.getAttribute(u'example'))

        option_list_widget.update()

        option_list_widget.connect("changed", self.update_list_option)

        return label_widget, option_list_widget

    def __parse_option_check(self, option_check):
        option = option_check.getAttribute(u'option')
        label = _(option_check.getAttribute(u'label'))
        short_desc = _(option_check.getAttribute(u'short_desc'))
        example = option_check.getAttribute(u'example')

        self.profilehelp.add_label(option, label)
        self.profilehelp.add_shortdesc(option, short_desc)
        self.profilehelp.add_example(option, example)

        check = OptionCheck(option, label)
        auxiliary_widget = get_option_check_auxiliary_widget(
                option, self.ops, check)
        if auxiliary_widget is not None:
            auxiliary_widget.connect("changed", self.update_auxiliary_widget)
            auxiliary_widget.connect(
                    'enter-notify-event', self.enter_notify_event_cb, option)
        else:
            check.set_active(not not self.ops[option])

        check.connect('toggled', self.update_check, auxiliary_widget)
        check.connect('enter-notify-event', self.enter_notify_event_cb, option)

        return check, auxiliary_widget

    def fill_table(self, table, expand_fill=True):
        yopt = (0, gtk.EXPAND | gtk.FILL)[expand_fill]
        for y, widget in enumerate(self.widgets_list):
            if widget[1] is None:
                table.attach(widget[0], 0, 2, y, y + 1, yoptions=yopt)
            else:
                table.attach(widget[0], 0, 1, y, y + 1, yoptions=yopt)
                table.attach(widget[1], 1, 2, y, y + 1, yoptions=yopt)

    def update_auxiliary_widget(self, auxiliary_widget):
        self.update_command()

    def update(self):
        for check, auxiliary_widget in self.widgets_list:
            if auxiliary_widget is not None:
                auxiliary_widget.update()
            else:
                check.set_active(not not self.ops[check.option])

    def update_target(self, entry):
        self.ops.target_specs = entry.get_targets()
        self.update_command()

    def update_check(self, check, auxiliary_widget):
        if auxiliary_widget is None:
            if check.get_active():
                self.ops[check.option] = True
            else:
                self.ops[check.option] = False
        self.update_command()

    def update_list_option(self, widget):
        if widget.last_selected:
            self.ops[widget.last_selected] = None

        opt, arg, label = widget.list[widget.get_active()]
        if opt:
            if arg:
                self.ops[opt] = arg
            else:
                self.ops[opt] = True

        widget.last_selected = opt

        self.show_help_for_option(opt)

        self.update_command()

    def show_help_for_option(self, option):
        self.profilehelp.handler(option)
        text = ""
        if self.profilehelp.get_currentstate() == "Default":
            text = ""
        else:
            text += self.profilehelp.get_label()
            text += "\n\n"
            text += self.profilehelp.get_shortdesc()
            if self.profilehelp.get_example():
                text += "\n\nExample input:\n"
                text += self.profilehelp.get_example()
        self.help_buf.set_text(text)

    def enter_notify_event_cb(self, event, widget, option):
        self.show_help_for_option(option)


class OptionBuilder(object):
    def __init__(self, xml_file, ops, update_func, help_buf):
        """
        xml_file is a UI description xml-file
        ops is an NmapOptions instance
        """
        xml_desc = open(xml_file)
        self.xml = minidom.parse(xml_desc)
        # Closing file to avoid problems with file descriptors
        xml_desc.close()

        self.ops = ops
        self.help_buf = help_buf
        self.update_func = update_func

        self.root_tag = "interface"

        self.xml = self.xml.getElementsByTagName(self.root_tag)[0]

        self.groups = self.__parse_groups()
        self.section_names = self.__parse_section_names()
        self.tabs = self.__parse_tabs()

    def update(self):
        for tab in self.tabs.values():
            tab.update()

    def __parse_section_names(self):
        dic = {}
        for group in self.groups:
            grp = self.xml.getElementsByTagName(group)[0]
            dic[group] = grp.getAttribute(u'label')
        return dic

    def __parse_groups(self):
        return [g_name.getAttribute(u'name') for g_name in
                self.xml.getElementsByTagName(u'groups')[0].getElementsByTagName(u'group')]  # noqa

    def __parse_tabs(self):
        dic = {}
        for tab_name in self.groups:
            if tab_name != "Scripting":
                dic[tab_name] = OptionTab(
                        self.xml.getElementsByTagName(tab_name)[0], self.ops,
                        self.update_func, self.help_buf)
                dic[tab_name].notscripttab = True
            else:
                dic[tab_name] = ScriptInterface(
                        None, self.ops, self.update_func, self.help_buf)
        return dic


class OptionList(gtk.ComboBox):
    def __init__(self, ops):
        self.ops = ops

        self.list = gtk.ListStore(str, str, str)
        gtk.ComboBox.__init__(self, self.list)

        cell = gtk.CellRendererText()
        self.pack_start(cell, True)
        self.add_attribute(cell, 'text', 2)

        self.last_selected = None
        self.options = []

    def update(self):
        selected = 0
        for i, row in enumerate(self.list):
            opt, arg = row[0], row[1]
            if opt == "":
                continue
            if ((not arg and self.ops[opt]) or
                    (arg and str(self.ops[opt]) == arg)):
                selected = i
        self.set_active(selected)

    def append(self, option, argument, label):
        opt = label
        ops = NmapOptions()
        if option is not None and option != "":
            if argument:
                ops[option] = argument
            else:
                ops[option] = True
            opt += " (%s)" % join_quoted(ops.render()[1:])

        self.list.append([option, argument, opt])
        self.options.append(option)


class OptionCheck(gtk.CheckButton):
    def __init__(self, option, label):
        opt = label
        if option is not None and option != "":
            opt += " (%s)" % option

        gtk.CheckButton.__init__(self, opt, use_underline=False)

        self.option = option
