#!/usr/bin/env python3

# ***********************IMPORTANT NMAP LICENSE TERMS************************
# *
# * The Nmap Security Scanner is (C) 1996-2024 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 gi

gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GObject


# 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()
        else:
            self.ops[self.option] = None

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


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()]
        else:
            self.ops.extras = []

    def changed_cb(self, widget):
        self.check.set_active(True)
        self.ops.extras = [self.get_text()]


class OptionLevel(Gtk.SpinButton):
    def __init__(self, option, ops, check):
        adjustment = Gtk.Adjustment.new(0, 0, 10, 1, 0, 0)
        Gtk.SpinButton.__init__(self, adjustment=adjustment, climb_rate=0.0, digits=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.Box):
    __gsignals__ = {
        "changed": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ())
    }

    def __init__(self, option, ops, check):
        Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)

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

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

        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()
        else:
            self.ops[self.option] = None

    def changed_cb(self, widget):
        self.check.set_active(True)
        self.ops[self.option] = self.entry.get_text()

    def clicked_cb(self, button):
        dialog = AllFilesFileChooserDialog(_("Choose file"))
        if dialog.run() == Gtk.ResponseType.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(" ".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())


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('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('option')

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

        for child in children:
            option = child.getAttribute('option')
            argument = child.getAttribute('argument')
            label = _(child.getAttribute('label'))
            option_list_widget.append(option, argument, label)
            self.profilehelp.add_label(option, label)
            self.profilehelp.add_shortdesc(
                    option, _(child.getAttribute('short_desc')))
            self.profilehelp.add_example(
                    option, child.getAttribute('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('option')
        label = _(option_check.getAttribute('label'))
        short_desc = _(option_check.getAttribute('short_desc'))
        example = option_check.getAttribute('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.AttachOptions.EXPAND | Gtk.AttachOptions.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('label')
        return dic

    def __parse_groups(self):
        return [g_name.getAttribute('name') for g_name in
                self.xml.getElementsByTagName('groups')[0].getElementsByTagName('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.new([str, str, str])
        Gtk.ComboBox.__init__(self, model=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, label=opt, use_underline=False)

        self.option = option
