#!/usr/bin/env python

"""
Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""

import os
import re
import socket
import subprocess
import sys
import tempfile
import threading
import webbrowser

from lib.core.common import getSafeExString
from lib.core.common import saveConfig
from lib.core.data import paths
from lib.core.defaults import defaults
from lib.core.enums import MKSTEMP_PREFIX
from lib.core.exception import SqlmapMissingDependence
from lib.core.exception import SqlmapSystemException
from lib.core.settings import DEV_EMAIL_ADDRESS
from lib.core.settings import IS_WIN
from lib.core.settings import ISSUES_PAGE
from lib.core.settings import GIT_PAGE
from lib.core.settings import SITE
from lib.core.settings import VERSION_STRING
from lib.core.settings import WIKI_PAGE
from thirdparty.six.moves import queue as _queue

# 定义全局变量
alive = None
line = ""
process = None
queue = None

def runGui(parser):
    """
    运行GUI界面
    """
    try:
        from thirdparty.six.moves import tkinter as _tkinter
        from thirdparty.six.moves import tkinter_scrolledtext as _tkinter_scrolledtext
        from thirdparty.six.moves import tkinter_ttk as _tkinter_ttk
        from thirdparty.six.moves import tkinter_messagebox as _tkinter_messagebox
    except ImportError as ex:
        raise SqlmapMissingDependence("missing dependence ('%s')" % getSafeExString(ex))

    # Reference: https://www.reddit.com/r/learnpython/comments/985umy/limit_user_input_to_only_int_with_tkinter/e4dj9k9?utm_source=share&utm_medium=web2x
    class ConstrainedEntry(_tkinter.Entry):
        """
        限制用户输入的类
        """
        def __init__(self, master=None, **kwargs):
            self.var = _tkinter.StringVar()
            self.regex = kwargs["regex"]
            del kwargs["regex"]
            _tkinter.Entry.__init__(self, master, textvariable=self.var, **kwargs)
            self.old_value = ''
            self.var.trace('w', self.check)
            self.get, self.set = self.var.get, self.var.set

        def check(self, *args):
            if re.search(self.regex, self.get()):
                self.old_value = self.get()
            else:
                self.set(self.old_value)

    # Reference: https://code.activestate.com/recipes/580726-tkinter-notebook-that-fits-to-the-height-of-every-/
    class AutoresizableNotebook(_tkinter_ttk.Notebook):
        """
        自动调整大小的Notebook类
        """
        def __init__(self, master=None, **kw):
            _tkinter_ttk.Notebook.__init__(self, master, **kw)
            self.bind("<<NotebookTabChanged>>", self._on_tab_changed)

        def _on_tab_changed(self, event):
            event.widget.update_idletasks()

            tab = event.widget.nametowidget(event.widget.select())
            event.widget.configure(height=tab.winfo_reqheight())

    try:
        window = _tkinter.Tk()
    except Exception as ex:
        errMsg = "unable to create GUI window ('%s')" % getSafeExString(ex)
        raise SqlmapSystemException(errMsg)

    window.title(VERSION_STRING)

    # Reference: https://www.holadevs.com/pregunta/64750/change-selected-tab-color-in-ttknotebook
    style = _tkinter_ttk.Style()
    settings = {"TNotebook.Tab": {"configure": {"padding": [5, 1], "background": "#fdd57e"}, "map": {"background": [("selected", "#C70039"), ("active", "#fc9292")], "foreground": [("selected", "#ffffff"), ("active", "#000000")]}}}
    style.theme_create("custom", parent="alt", settings=settings)
    style.theme_use("custom")

    # Reference: https://stackoverflow.com/a/10018670
    def center(window):
        window.update_idletasks()
        width = window.winfo_width()
        frm_width = window.winfo_rootx() - window.winfo_x()
        win_width = width + 2 * frm_width
        height = window.winfo_height()
        titlebar_height = window.winfo_rooty() - window.winfo_y()
        win_height = height + titlebar_height + frm_width
        x = window.winfo_screenwidth() // 2 - win_width // 2
        y = window.winfo_screenheight() // 2 - win_height // 2
        window.geometry('{}x{}+{}+{}'.format(width, height, x, y))
        window.deiconify()

    def onKeyPress(event):
        # 当按键按下时，获取全局变量line和queue
        global line
        global queue

        # 如果process存在
        if process:
            # 如果按下的键是退格键
            if event.char == '\b':
                # 将line的最后一个字符删除
                line = line[:-1]
            else:
                # 否则将按下的键添加到line中
                line += event.char

    def onReturnPress(event):
        # 当回车键按下时，获取全局变量line和queue
        global line
        global queue

        # 如果process存在
        if process:
            try:
                # 将line写入process的stdin，并刷新
                process.stdin.write(("%s\n" % line.strip()).encode())
                process.stdin.flush()
            except socket.error:
                # 如果发生socket错误，将line置为空，关闭窗口，并返回"break"
                line = ""
                event.widget.master.master.destroy()
                return "break"
            except:
                # 如果发生其他错误，返回
                return

            # 在text中插入一个换行符
            event.widget.insert(_tkinter.END, "\n")

            # 返回"break"
            return "break"

    def run():
        # 获取全局变量alive、process和queue
        global alive
        global process
        global queue

        # 创建一个空字典config
        config = {}

        # 遍历window._widgets中的键值对
        for key in window._widgets:
            # 获取键值对中的dest和type
            dest, type = key
            # 获取键值对中的widget
            widget = window._widgets[key]

            # 如果widget有get方法且返回值为空，则将value置为None
            if hasattr(widget, "get") and not widget.get():
                value = None
            # 如果type为"string"，则将value置为widget.get()
            elif type == "string":
                value = widget.get()
            # 如果type为"float"，则将value置为float(widget.get())
            elif type == "float":
                value = float(widget.get())
            # 如果type为"int"，则将value置为int(widget.get())
            elif type == "int":
                value = int(widget.get())
            # 否则将value置为bool(widget.var.get())
            else:
                value = bool(widget.var.get())

            # 将value添加到config中
            config[dest] = value

        # 遍历parser.option_list中的option
        for option in parser.option_list:
            # 将option.dest添加到config中，如果defaults中有option.dest，则将defaults[option.dest]添加到config中，否则将None添加到config中
            config[option.dest] = defaults.get(option.dest, None)

        # 创建一个临时文件，并将config保存到该文件中
        handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True)
        os.close(handle)

        saveConfig(config, configFile)

        # 定义一个函数enqueue，用于将stream中的内容放入queue中
        def enqueue(stream, queue):
            # 获取全局变量alive
            global alive

            # 遍历stream中的每一行
            for line in iter(stream.readline, b''):
                # 将line放入queue中
                queue.put(line)

            alive = False
            stream.close()

        alive = True

        # 使用subprocess.Popen启动sqlmap.py
        process = subprocess.Popen([sys.executable or "python", os.path.join(paths.SQLMAP_ROOT_PATH, "sqlmap.py"), "-c", configFile], shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, bufsize=1, close_fds=not IS_WIN)

        # Reference: https://stackoverflow.com/a/4896288
        queue = _queue.Queue()
        thread = threading.Thread(target=enqueue, args=(process.stdout, queue))
        thread.daemon = True
        thread.start()

        top = _tkinter.Toplevel()
        top.title("Console")

        # Reference: https://stackoverflow.com/a/13833338
        text = _tkinter_scrolledtext.ScrolledText(top, undo=True)
        text.bind("<Key>", onKeyPress)
        text.bind("<Return>", onReturnPress)
        text.pack()
        text.focus()

        center(top)

        while True:
            line = ""
            try:
                # line = queue.get_nowait()
                line = queue.get(timeout=.1)
                text.insert(_tkinter.END, line)
            except _queue.Empty:
                text.see(_tkinter.END)
                text.update_idletasks()

                if not alive:
                    break

    # 创建菜单栏
    menubar = _tkinter.Menu(window)

    # 创建文件菜单
    filemenu = _tkinter.Menu(menubar, tearoff=0)
    filemenu.add_command(label="Open", state=_tkinter.DISABLED)
    filemenu.add_command(label="Save", state=_tkinter.DISABLED)
    filemenu.add_separator()
    filemenu.add_command(label="Exit", command=window.quit)
    menubar.add_cascade(label="File", menu=filemenu)

    # 添加运行按钮
    menubar.add_command(label="Run", command=run)

    # 创建帮助菜单
    helpmenu = _tkinter.Menu(menubar, tearoff=0)
    helpmenu.add_command(label="Official site", command=lambda: webbrowser.open(SITE))
    helpmenu.add_command(label="Github pages", command=lambda: webbrowser.open(GIT_PAGE))
    helpmenu.add_command(label="Wiki pages", command=lambda: webbrowser.open(WIKI_PAGE))
    helpmenu.add_command(label="Report issue", command=lambda: webbrowser.open(ISSUES_PAGE))
    helpmenu.add_separator()
    helpmenu.add_command(label="About", command=lambda: _tkinter_messagebox.showinfo("About", "Copyright (c) 2006-2024\n\n    (%s)" % DEV_EMAIL_ADDRESS))
    menubar.add_cascade(label="Help", menu=helpmenu)

    # 将菜单栏添加到窗口
    window.config(menu=menubar)
    window._widgets = {}

    # 创建可调整大小的Notebook
    notebook = AutoresizableNotebook(window)

    first = None
    frames = {}

    # 遍历所有选项组
    for group in parser.option_groups:
        frame = frames[group.title] = _tkinter.Frame(notebook, width=200, height=200)
        notebook.add(frames[group.title], text=group.title)

        _tkinter.Label(frame).grid(column=0, row=0, sticky=_tkinter.W)

        row = 1
        if group.get_description():
            _tkinter.Label(frame, text="%s:" % group.get_description()).grid(column=0, row=1, columnspan=3, sticky=_tkinter.W)
            _tkinter.Label(frame).grid(column=0, row=2, sticky=_tkinter.W)
            row += 2

        # 遍历每个选项
        for option in group.option_list:
            _tkinter.Label(frame, text="%s " % parser.formatter._format_option_strings(option)).grid(column=0, row=row, sticky=_tkinter.W)

            # 根据选项类型创建相应的控件
            if option.type == "string":
                widget = _tkinter.Entry(frame)
            elif option.type == "float":
                widget = ConstrainedEntry(frame, regex=r"\A\d*\.?\d*\Z")
            elif option.type == "int":
                widget = ConstrainedEntry(frame, regex=r"\A\d*\Z")
            else:
                var = _tkinter.IntVar()
                widget = _tkinter.Checkbutton(frame, variable=var)
                widget.var = var

            first = first or widget
            widget.grid(column=1, row=row, sticky=_tkinter.W)

            window._widgets[(option.dest, option.type)] = widget

            # 设置默认值
            default = defaults.get(option.dest)
            if default:
                if hasattr(widget, "insert"):
                    widget.insert(0, default)

            _tkinter.Label(frame, text=" %s" % option.help).grid(column=2, row=row, sticky=_tkinter.W)

            row += 1

        _tkinter.Label(frame).grid(column=0, row=row, sticky=_tkinter.W)

    # 将Notebook添加到窗口
    notebook.pack(expand=1, fill="both")
    notebook.enable_traversal()

    # 设置焦点
    first.focus()

    # 进入主循环
    window.mainloop()
