import re
from http import HTTPStatus
from .exploit import GlpiExploit
from .requirements import DefaultVersion
from .logger import Log
from bs4 import BeautifulSoup
from packaging import version
from rich.progress import track
from glpwnme.exploits.utils import GlpiUtils

class VulnerablePlugin:
    """
    Class for vulnerable plugins
    """
    def __init__(self, version_min, version_max, link, msg):
        self.min_version = version.parse(version_min)
        self.max_version = version.parse(version_max)
        self.link = link
        self.msg = msg

    def is_version_vuln(self, version_found):
        """
        Check if the given version is vulnerable

        :param version_found: The version to check
        :type version_found: str

        :return: Whether the version is vulnerable
        :rtype: bool
        """
        if not version_found:
            return (self.min_version == version.parse(DefaultVersion.minimal)
                    and self.max_version == version.parse(DefaultVersion.maximal))

        version_found = version.parse(version_found)
        return (version_found >= self.min_version
                and version_found <= self.max_version)

    def get_message(self):
        """
        Return the message formatted

        :return: The message to show
        :rtype: str
        """
        infos = "\t" + self.msg + ", @see:\n\t"
        infos += "\n\t".join(self.link)
        return infos

PLUGINS_VULNS = {"order": VulnerablePlugin("0",
                                           "2.10.0",
                                           ["https://github.com/pluginsGLPI/order/blob/2.7.6/ajax/dropdownValue.php",
                                            "https://github.com/pluginsGLPI/order/blob/2.10.0/ajax/dropdownContact.php"],
                                           "Unserialize call from POST Request"),
                 }

class PluginVersion:
    """
    Class that allows recovering version for some plugins
    """
    def __init__(self):
        self.plugins_version = {"fusioninventory": {"callbacks": [{"endpoint": "plugin.xml",
                                                                   "extract": PluginVersion.extract_version_from_xml},
                                                                  {"endpoint": "front/plugin_fusioninventory.communication.php",
                                                                   "extract": PluginVersion.extract_version_from_header}]},

                                "glpiinventory": {"callbacks": [{"endpoint": "front/plugin_fusioninventory.communication.php?action=yest&machineid=45645",
                                                                   "extract": PluginVersion.extract_version_from_header},
                                                                {"endpoint": "plugin.xml",
                                                                   "extract": PluginVersion.extract_version_from_xml}]},

                                "fields": {"callbacks": [{"endpoint": "plugin.xml",
                                                          "extract": PluginVersion.extract_version_from_xml}]},

                                "escalade": {"callbacks": [{"endpoint": "escalade.xml",
                                                            "extract": PluginVersion.extract_version_from_xml}]},

                                "metademands": {"callbacks": [{"endpoint": "metademands.xml",
                                                               "extract": PluginVersion.extract_version_from_xml}]},

                                "mreporting": {"callbacks": [{"endpoint": "mreporting.xml",
                                                              "extract": PluginVersion.extract_version_from_xml}]},

                                "oauthimap": {"callbacks": [{"endpoint": "oauthimap.xml",
                                                             "extract": PluginVersion.extract_version_from_xml}]},

                                "formcreator": {"callbacks": [{"endpoint": "plugin.xml",
                                                               "extract": PluginVersion.extract_version_from_xml}]},

                                "tasklists": {"callbacks": [{"endpoint": "tasklists.xml",
                                                             "extract": PluginVersion.extract_version_from_xml}]},

                                "shellcommands": {"callbacks": [{"endpoint": "shellcommands.xml",
                                                                 "extract": PluginVersion.extract_version_from_xml}]},

                                "archimap": {"callbacks": [{"endpoint": "archimap.xml",
                                                            "extract": PluginVersion.extract_version_from_xml}]},

                                "news": {"callbacks": [{"endpoint": "plugin.xml",
                                                        "extract": PluginVersion.extract_version_from_xml}]},

                                "accounts": {"callbacks": [{"endpoint": "accounts.xml",
                                                            "extract": PluginVersion.extract_version_from_xml}]},

                                "satisfaction": {"callbacks": [{"endpoint": "satisfaction.xml",
                                                                "extract": PluginVersion.extract_version_from_xml}]},

                                "order": {"callbacks": [{"endpoint": "plugin.xml",
                                                            "extract": PluginVersion.extract_version_from_xml}]},

                                "manufacturersimports": {"callbacks": [{"endpoint": "manufacturersimports.xml",
                                                            "extract": PluginVersion.extract_version_from_xml}]},

                                "treeview": {"callbacks": [{"endpoint": "treeview.xml",
                                                            "extract": PluginVersion.extract_version_from_xml}]},

                                "genericobject": {"callbacks": [{"endpoint": "genericobject.xml",
                                                                 "extract": PluginVersion.extract_version_from_xml}]}}

    @staticmethod
    def extract_version_from_header(result):
        """
        Retrieve the result of a plugin through the headers

        :param result: The result of the endpoint
        :type result: class:`Response`
        """
        header = result.headers.get("server-type")
        if header:
            version = header.strip().split(" ")[-1]
            return version
        return None

    @staticmethod
    def extract_version_from_xml(plugin_xml):
        """
        Extract the version of a plugin from its xml file

        :param plugin_xml: The plugin xml recovered
        :type plugin_xml: class:`Response`

        :return: The version found from the plugin file
        :rtype: str
        """
        plugin_xml = plugin_xml.text
        version_plugin = re.search(r'<num>((\d|\.)+?)</num>', plugin_xml)
        if version_plugin:
            return version_plugin.group(1)
        return None

class PluginEnums(GlpiExploit):
    """
    This class allows plugin enumeration a target

    @see https://plugins.glpi-project.org/#/
    """
    _impacts = 'Enumerate plugins in used'

    DEFAULT_PLUGIN_FILE = "README.md"
    VERSION_PLUGIN_FILE = "CHANGELOG.md"

    plugins_list = ['useditemsexport', 'glpiinventory', 'fusioninventory', 'addressing', 'archibp', 'collaborativetools',
                    'behaviors', 'datainjection', 'metademands', 'escalade', 'dashboard', 'mreporting', 'ocsinventoryng',
                    'jamf', 'fields', 'pdf', 'unread', 'formcreator', 'activity', 'purchaserequest', 'glpiwithbookstack',
                    'archidata', 'reports', 'dataflows', 'genericobject', 'order', 'archisw', 'news', 'oauthimap',
                    'tasklists', 'shellcommands', 'archimap', 'accounts', 'positions', 'satisfaction', 'ldaptools',
                    'advanceddashboard', 'agentconfig', 'manufacturersimports', 'mreporting', 'treeview', 'uninstall']

    endpoints = ["/plugins", "/marketplace"]

    exploit_implementations = {"fusioninventory": "FUSIONINVENTORY_PLUGIN_SQL",
                               "fields": "FIELDS_PLUGIN_SQL",
                               "genericobject": "GENERICOBJECT_PLUGIN_SQL",
                               "order": "PLUGIN_ORDER_2022"}

    def __init__(self, glpi_session):
        super().__init__(glpi_session)
        self.plugins_found = {}
        self.plugin_version = PluginVersion()

    @staticmethod
    def get_version(changelog):
        """
        Extract the version from a changelog
        """
        version_regex = re.compile(r"\[((\d+|\.)+)?\]")
        version = re.findall(version_regex, changelog)
        if version:
            tuple_res = version[0]
            if tuple_res:
                return tuple_res[0]

    def plugin_exists(self, plugin, expected_status=[HTTPStatus.OK, HTTPStatus.FORBIDDEN]):
        """
        Check if a plugin exists or not on the target

        :param plugin: The name of the plugin
        :type plugin: str

        :return: Whether or not the plugin exists on the target
        :rtype: bool
        """
        is_plugin_found = False
        for endpoint in self.endpoints:
            res = self.get(f"{endpoint}/{plugin}/", allow_redirects=False)
            if res.status_code in expected_status:
                self.plugins_found[plugin] = {"endpoint": endpoint}
                is_plugin_found = True
                break
        return is_plugin_found

    def recover_version_from_plugin(self, plugin):
        """
        Try to recover the version of a plugin, set the internal variable
        of the plugins found

        :param plugin: The name of the plugin
        :type plugin: str
        """
        endpoint = self.plugins_found[plugin]["endpoint"]

        # First try to get the version from implemented function
        callbacks_available = self.plugin_version.plugins_version.get(plugin)
        if callbacks_available:
            list_callbacks = callbacks_available["callbacks"]
            for procedure in list_callbacks:
                endpoint_version = procedure["endpoint"]
                callable_version = procedure["extract"]
                if callable(callable_version):
                    version = callable_version(self.get(f"{endpoint}/{plugin}/{endpoint_version}"))
                    if version:
                        self.plugins_found[plugin]["version"] = version
                        return
                else:
                    Log.err(f"Procedure not callable found for plugin: {plugin}")

        res = self.get(f"{endpoint}/{plugin}/{self.VERSION_PLUGIN_FILE}", allow_redirects=False)

        version = PluginEnums.get_version(res.text)
        if version:
            self.plugins_found[plugin]["version"] = version

    def show_plugins_found(self):
        """
        Display the result of the plugins found
        """
        if not self.plugins_found:
            Log.log(f"No plugins found")

        else:
            for plugin in self.plugins_found.keys():
                endpoint = self.plugins_found[plugin].get("endpoint")
                msg = f"Plugin [blue]{plugin}[/blue] found on [blue]{endpoint}[/blue]"
                version = self.plugins_found[plugin].get("version")
                if version:
                    msg += f", version ~= [blue]{version}[/blue]"

                vuln_plugin = PLUGINS_VULNS.get(plugin)
                if(vuln_plugin
                   and vuln_plugin.is_version_vuln(version)):
                    msg += " - [red b]Vulnerable[/red b]\n"
                    msg += vuln_plugin.get_message()

                    exploit_class = self.exploit_implementations.get(plugin)
                    if exploit_class:
                        msg += f"\n\t[i]You can exploit it by running: [gold3]--exploit {exploit_class} --run[/gold3][/i]\n"

                Log.msg(msg)

    def run(self):
        """
        Enums the plugins on the target
        """
        for plugin in track(self.plugins_list, description="[blue]Looting plugins...[/]"):
            if self.plugin_exists(plugin):
                self.recover_version_from_plugin(plugin)
