#!/usr/bin/env python

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

from __future__ import division

import os
import re
import subprocess
import time

from lib.controller.action import action
from lib.controller.checks import checkConnection
from lib.controller.checks import checkDynParam
from lib.controller.checks import checkInternet
from lib.controller.checks import checkNullConnection
from lib.controller.checks import checkSqlInjection
from lib.controller.checks import checkStability
from lib.controller.checks import checkWaf
from lib.controller.checks import heuristicCheckSqlInjection
from lib.core.agent import agent
from lib.core.common import dataToStdout
from lib.core.common import extractRegexResult
from lib.core.common import getFilteredPageContent
from lib.core.common import getPublicTypeMembers
from lib.core.common import getSafeExString
from lib.core.common import hashDBRetrieve
from lib.core.common import hashDBWrite
from lib.core.common import intersect
from lib.core.common import isDigit
from lib.core.common import isListLike
from lib.core.common import parseTargetUrl
from lib.core.common import popValue
from lib.core.common import pushValue
from lib.core.common import randomInt
from lib.core.common import randomStr
from lib.core.common import readInput
from lib.core.common import removePostHintPrefix
from lib.core.common import safeCSValue
from lib.core.common import showHttpErrorCodes
from lib.core.common import urldecode
from lib.core.common import urlencode
from lib.core.compat import xrange
from lib.core.data import conf
from lib.core.data import kb
from lib.core.data import logger
from lib.core.decorators import stackedmethod
from lib.core.enums import CONTENT_TYPE
from lib.core.enums import HASHDB_KEYS
from lib.core.enums import HEURISTIC_TEST
from lib.core.enums import HTTP_HEADER
from lib.core.enums import HTTPMETHOD
from lib.core.enums import NOTE
from lib.core.enums import PAYLOAD
from lib.core.enums import PLACE
from lib.core.exception import SqlmapBaseException
from lib.core.exception import SqlmapConnectionException
from lib.core.exception import SqlmapNoneDataException
from lib.core.exception import SqlmapNotVulnerableException
from lib.core.exception import SqlmapSilentQuitException
from lib.core.exception import SqlmapSkipTargetException
from lib.core.exception import SqlmapSystemException
from lib.core.exception import SqlmapUserQuitException
from lib.core.exception import SqlmapValueException
from lib.core.settings import ASP_NET_CONTROL_REGEX
from lib.core.settings import CSRF_TOKEN_PARAMETER_INFIXES
from lib.core.settings import DEFAULT_GET_POST_DELIMITER
from lib.core.settings import EMPTY_FORM_FIELDS_REGEX
from lib.core.settings import GOOGLE_ANALYTICS_COOKIE_PREFIX
from lib.core.settings import HOST_ALIASES
from lib.core.settings import IGNORE_PARAMETERS
from lib.core.settings import LOW_TEXT_PERCENT
from lib.core.settings import REFERER_ALIASES
from lib.core.settings import USER_AGENT_ALIASES
from lib.core.target import initTargetEnv
from lib.core.target import setupTargetEnv
from lib.utils.hash import crackHashFile

def _selectInjection():
    """
    Selection function for injection place, parameters and type.
    """

    points = {}

    # 遍历kb.injections中的每一个injection
    for injection in kb.injections:
        place = injection.place
        parameter = injection.parameter
        ptype = injection.ptype

        # 将place, parameter, ptype组成一个元组point
        point = (place, parameter, ptype)

        # 如果point不在points中，则将injection添加到points中
        if point not in points:
            points[point] = injection
        else:
            # 如果point已经在points中，则将injection中的非data字段添加到points中
            for key in points[point]:
                if key != 'data':
                    points[point][key] = points[point][key] or injection[key]
            # 将injection中的data字段更新到points中
            points[point]['data'].update(injection['data'])

    # 如果points中只有一个injection，则将kb.injection设置为kb.injections[0]
    if len(points) == 1:
        kb.injection = kb.injections[0]

    # 如果points中有多个injection，则提示用户选择一个injection
    elif len(points) > 1:
        message = "there were multiple injection points, please select "
        message += "the one to use for following injections:\n"

        points = []

        # 遍历kb.injections中的每一个injection
        for i in xrange(0, len(kb.injections)):
            place = kb.injections[i].place
            parameter = kb.injections[i].parameter
            ptype = kb.injections[i].ptype
            point = (place, parameter, ptype)

            # 如果point不在points中，则将point添加到points中
            if point not in points:
                points.append(point)
                # 如果ptype是整数，则将其转换为PAYLOAD.PARAMETER[ptype]
                ptype = PAYLOAD.PARAMETER[ptype] if isinstance(ptype, int) else ptype

                # 构造提示信息
                message += "[%d] place: %s, parameter: " % (i, place)
                message += "%s, type: %s" % (parameter, ptype)

                # 如果是第一个injection，则设置为默认
                if i == 0:
                    message += " (default)"

                message += "\n"

        # 提示用户选择一个injection
        message += "[q] Quit"
        choice = readInput(message, default='0').upper()

        # 如果用户选择的是一个有效的injection，则将kb.injection设置为kb.injections[index]
        if isDigit(choice) and int(choice) < len(kb.injections) and int(choice) >= 0:
            index = int(choice)
        # 如果用户选择的是q，则抛出SqlmapUserQuitException异常
        elif choice == 'Q':
            raise SqlmapUserQuitException
        # 如果用户选择的是一个无效的injection，则抛出SqlmapValueException异常
        else:
            errMsg = "invalid choice"
            raise SqlmapValueException(errMsg)

        kb.injection = kb.injections[index]

def _formatInjection(inj):
    # 获取参数类型
    paramType = conf.method if conf.method not in (None, HTTPMETHOD.GET, HTTPMETHOD.POST) else inj.place
    # 格式化数据
    data = "Parameter: %s (%s)\n" % (inj.parameter, paramType)

    # 遍历inj.data中的每个键值对
    for stype, sdata in inj.data.items():
        # 获取title、vector、comment、payload
        title = sdata.title
        vector = sdata.vector
        comment = sdata.comment
        payload = agent.adjustLateValues(sdata.payload)
        # 如果inj.place为CUSTOM_HEADER，则payload取split后的第二个元素
        if inj.place == PLACE.CUSTOM_HEADER:
            payload = payload.split(',', 1)[1]
        # 如果stype为UNION，则count为payload中逗号的数量加1，title中替换为count，vector中forgeUnionQuery
        if stype == PAYLOAD.TECHNIQUE.UNION:
            count = re.sub(r"(?i)(\(.+\))|(\blimit[^a-z]+)", "", sdata.payload).count(',') + 1
            title = re.sub(r"\d+ to \d+", str(count), title)
            vector = agent.forgeUnionQuery("[QUERY]", vector[0], vector[1], vector[2], None, None, vector[5], vector[6])
            # 如果count为1，则title中替换为column
            if count == 1:
                title = title.replace("columns", "column")
        # 如果comment不为空，则vector拼接comment
        elif comment:
            vector = "%s%s" % (vector, comment)
        # 格式化数据
        data += "    Type: %s\n" % PAYLOAD.SQLINJECTION[stype]
        data += "    Title: %s\n" % title
        data += "    Payload: %s\n" % urldecode(payload, unsafe="&", spaceplus=(inj.place != PLACE.GET and kb.postSpaceToPlus))
        data += "    Vector: %s\n\n" % vector if conf.verbose > 1 else "\n"

    return data

def _showInjections():
    # 如果conf.wizard且kb.wizardMode为True，则将kb.wizardMode置为False
    if conf.wizard and kb.wizardMode:
        kb.wizardMode = False

    # 如果kb.testQueryCount大于0，则header为sqlmap识别到的注入点数量
    if kb.testQueryCount > 0:
        header = "sqlmap identified the following injection point(s) with "
        header += "a total of %d HTTP(s) requests" % kb.testQueryCount
    # 否则header为sqlmap从存储的会话中恢复的注入点
    else:
        header = "sqlmap resumed the following injection point(s) from stored session"

    # 如果conf.api，则使用conf.dumper.string输出url、query、data和kb.injections
    if conf.api:
        conf.dumper.string("", {"url": conf.url, "query": conf.parameters.get(PLACE.GET), "data": conf.parameters.get(PLACE.POST)}, content_type=CONTENT_TYPE.TARGET)
        conf.dumper.string("", kb.injections, content_type=CONTENT_TYPE.TECHNIQUES)
    # 否则使用conf.dumper.string输出header和格式化后的kb.injections
    else:
        data = "".join(set(_formatInjection(_) for _ in kb.injections)).rstrip("\n")
        conf.dumper.string(header, data)

    # 如果conf.tamper，则输出警告信息
    if conf.tamper:
        warnMsg = "changes made by tampering scripts are not "
        warnMsg += "included in shown payload content(s)"
        logger.warning(warnMsg)

    # 如果conf.hpp，则输出警告信息
    if conf.hpp:
        warnMsg = "changes made by HTTP parameter pollution are not "
        warnMsg += "included in shown payload content(s)"
        logger.warning(warnMsg)

def _randomFillBlankFields(value):
    # 将retVal赋值为value
    retVal = value

    # 如果value中存在EMPTY_FORM_FIELDS_REGEX，则询问是否填充空白字段
    if extractRegexResult(EMPTY_FORM_FIELDS_REGEX, value):
        message = "do you want to fill blank fields with random values? [Y/n] "

        # 如果输入为Y或空，则将retVal中匹配到的字段替换为随机值
        if readInput(message, default='Y', boolean=True):
            for match in re.finditer(EMPTY_FORM_FIELDS_REGEX, retVal):
                item = match.group("result")
                # 如果item不在IGNORE_PARAMETERS中且不匹配ASP_NET_CONTROL_REGEX，则替换为随机值
                if not any(_ in item for _ in IGNORE_PARAMETERS) and not re.search(ASP_NET_CONTROL_REGEX, item):
                    newValue = randomStr() if not re.search(r"^id|id$", item, re.I) else randomInt()
                    # 如果item以DEFAULT_GET_POST_DELIMITER结尾，则替换为"%s%s%s" % (item[:-1], newValue, DEFAULT_GET_POST_DELIMITER)
                    if item[-1] == DEFAULT_GET_POST_DELIMITER:
                        retVal = retVal.replace(item, "%s%s%s" % (item[:-1], newValue, DEFAULT_GET_POST_DELIMITER))
                    # 否则替换为"%s%s" % (item, newValue)
                    else:
                        retVal = retVal.replace(item, "%s%s" % (item, newValue))

    return retVal

def _saveToHashDB():
    # 从hashDB中获取kb.injections
    injections = hashDBRetrieve(HASHDB_KEYS.KB_INJECTIONS, True)
    # 如果injections不是列表，则将其赋值为空列表
    if not isListLike(injections):
        injections = []
    # 将kb.injections中满足条件的元素添加到injections中
    injections.extend(_ for _ in kb.injections if _ and _.place is not None and _.parameter is not None)

    _ = dict()
    # 遍历injections中的元素
    for injection in injections:
        # 将injection.place、injection.parameter、injection.ptype作为key
        key = (injection.place, injection.parameter, injection.ptype)
        # 如果key不在_中，则将injection添加到_中
        if key not in _:
            _[key] = injection
        # 否则，将injection.data中的元素更新到_中
        else:
            _[key].data.update(injection.data)
    # 将_中的元素写入hashDB
    hashDBWrite(HASHDB_KEYS.KB_INJECTIONS, list(_.values()), True)

    # 从hashDB中获取kb.absFilePaths
    _ = hashDBRetrieve(HASHDB_KEYS.KB_ABS_FILE_PATHS, True)
    # 将kb.absFilePaths中的元素添加到_中
    hashDBWrite(HASHDB_KEYS.KB_ABS_FILE_PATHS, kb.absFilePaths | (_ if isinstance(_, set) else set()), True)

    # 如果hashDB中没有kb.chars，则将kb.chars写入hashDB
    if not hashDBRetrieve(HASHDB_KEYS.KB_CHARS):
        hashDBWrite(HASHDB_KEYS.KB_CHARS, kb.chars, True)

    # 如果hashDB中没有kb.dynamicMarkings，则将kb.dynamicMarkings写入hashDB
    if not hashDBRetrieve(HASHDB_KEYS.KB_DYNAMIC_MARKINGS):
        hashDBWrite(HASHDB_KEYS.KB_DYNAMIC_MARKINGS, kb.dynamicMarkings, True)

def _saveToResultsFile():
    # 如果没有设置conf.resultsFP，则返回
    if not conf.resultsFP:
        return

    # 创建results字典
    results = {}
    # 获取PAYLOAD.TECHNIQUE中的所有公共成员，并将其转换为字典
    techniques = dict((_[1], _[0]) for _ in getPublicTypeMembers(PAYLOAD.TECHNIQUE))

    # 遍历kb.injections和kb.falsePositives中的元素
    for injection in kb.injections + kb.falsePositives:
        # 如果injection.place或injection.parameter为None，则跳过
        if injection.place is None or injection.parameter is None:
            continue

        # 将injection.place、injection.parameter、injection.notes作为key
        key = (injection.place, injection.parameter, ';'.join(injection.notes))
        # 如果key不在results中，则将key添加到results中
        if key not in results:
            results[key] = []

        # 将injection.data中的元素添加到results[key]中
        results[key].extend(list(injection.data.keys()))

    # 尝试将results中的元素写入conf.resultsFP
    try:
        for key, value in results.items():
            # 获取key中的元素
            place, parameter, notes = key
            # 将key中的元素转换为字符串，并添加到line中
            line = "%s,%s,%s,%s,%s%s" % (safeCSValue(kb.originalUrls.get(conf.url) or conf.url), place, parameter, "".join(techniques[_][0].upper() for _ in sorted(value)), notes, os.linesep)
            # 将line写入conf.resultsFP
            conf.resultsFP.write(line)

        # 刷新conf.resultsFP
        conf.resultsFP.flush()
    # 如果发生IOError，则抛出SqlmapSystemException
    except IOError as ex:
        errMsg = "unable to write to the results file '%s' ('%s'). " % (conf.resultsFile, getSafeExString(ex))
        raise SqlmapSystemException(errMsg)

@stackedmethod
def start():
    """
    This function calls a function that performs checks on both URL
    stability and all GET, POST, Cookie and User-Agent parameters to
    check if they are dynamic and SQL injection affected
    """

    # 如果设置了conf.hashFile，则调用crackHashFile函数
    if conf.hashFile:
        crackHashFile(conf.hashFile)

    # 如果设置了conf.direct，则调用initTargetEnv、setupTargetEnv、action函数
    if conf.direct:
        initTargetEnv()
        setupTargetEnv()
        action()
        return True

    # 如果设置了conf.url且没有设置conf.forms和conf.crawlDepth，则将(conf.url, conf.method, conf.data, conf.cookie, None)添加到kb.targets中
    if conf.url and not any((conf.forms, conf.crawlDepth)):
        kb.targets.add((conf.url, conf.method, conf.data, conf.cookie, None))

    # 如果设置了conf.configFile且没有设置kb.targets，则抛出SqlmapSystemException
    if conf.configFile and not kb.targets:
        errMsg = "you did not edit the configuration file properly, set "
        errMsg += "the target URL, list of targets or google dork"
        logger.error(errMsg)
        return False

    # 如果kb.targets不为空且kb.targets是列表，且kb.targets的长度大于1，则输出信息
    if kb.targets and isListLike(kb.targets) and len(kb.targets) > 1:
        infoMsg = "found a total of %d targets" % len(kb.targets)
        logger.info(infoMsg)

    # 初始化targetCount为0
    targetCount = 0
    # 初始化initialHeaders为conf.httpHeaders
    initialHeaders = list(conf.httpHeaders)

    # 遍历kb.targets中的元素
    for targetUrl, targetMethod, targetData, targetCookie, targetHeaders in kb.targets:
        # 将targetCount加1
        targetCount += 1

        try:
            # 如果设置了conf.checkInternet，则调用checkInternet函数
            if conf.checkInternet:
                infoMsg = "checking for Internet connection"
                logger.info(infoMsg)

                # 如果没有检测到Internet连接，则输出警告信息
                if not checkInternet():
                    warnMsg = "[%s] [WARNING] no connection detected" % time.strftime("%X")
                    dataToStdout(warnMsg)

                    # 尝试检测Internet连接
                    valid = False
                    for _ in xrange(conf.retries):
                        if checkInternet():
                            valid = True
                            break
                        else:
                            dataToStdout('.')
                            time.sleep(5)

                    # 如果没有检测到Internet连接，则抛出SqlmapConnectionException
                    if not valid:
                        errMsg = "please check your Internet connection and rerun"
                        raise SqlmapConnectionException(errMsg)
                    else:
                        dataToStdout("\n")

            # 将conf.url赋值为targetUrl
            conf.url = targetUrl
            # 将conf.method赋值为targetMethod的大写形式
            conf.method = targetMethod.upper().strip() if targetMethod else targetMethod
            # 将conf.data赋值为targetData
            conf.data = targetData
            # 将conf.cookie赋值为targetCookie
            conf.cookie = targetCookie
            # 将conf.httpHeaders赋值为initialHeaders和targetHeaders的并集
            conf.httpHeaders = list(initialHeaders)
            conf.httpHeaders.extend(targetHeaders or [])

            # 如果设置了conf.randomAgent或conf.mobile，则将initialHeaders中USER_AGENT的值赋值给conf.httpHeaders
            if conf.randomAgent or conf.mobile:
                for header, value in initialHeaders:
                    if header.upper() == HTTP_HEADER.USER_AGENT.upper():
                        conf.httpHeaders.append((header, value))
                        break

            # 如果设置了conf.data，则将conf.data中的__ ASP(.NET)参数进行URL编码
            if conf.data:
                # Note: explicitly URL encode __ ASP(.NET) parameters (e.g. to avoid problems with Base64 encoded '+' character) - standard procedure in web browsers
                conf.data = re.sub(r"\b(__\w+)=([^&]+)", lambda match: "%s=%s" % (match.group(1), urlencode(match.group(2), safe='%')), conf.data)

            # 将conf.httpHeaders中重复的header删除
            conf.httpHeaders = [conf.httpHeaders[i] for i in xrange(len(conf.httpHeaders)) if conf.httpHeaders[i][0].upper() not in (__[0].upper() for __ in conf.httpHeaders[i + 1:])]

            initTargetEnv()
            parseTargetUrl()

            testSqlInj = False

            # 如果配置中包含PLACE.GET参数，并且没有conf.data和conf.testParameter参数，则遍历conf.parameters[PLACE.GET]中的参数
            if PLACE.GET in conf.parameters and not any((conf.data, conf.testParameter)):
                for parameter in re.findall(r"([^=]+)=([^%s]+%s?|\Z)" % (re.escape(conf.paramDel or "") or DEFAULT_GET_POST_DELIMITER, re.escape(conf.paramDel or "") or DEFAULT_GET_POST_DELIMITER), conf.parameters[PLACE.GET]):
                    paramKey = (conf.hostname, conf.path, PLACE.GET, parameter[0])

                    # 如果paramKey不在kb.testedParams中，则设置testSqlInj为True
                    if paramKey not in kb.testedParams:
                        testSqlInj = True
                        break
            else:
                # 否则，设置paramKey为(conf.hostname, conf.path, None, None)，并检查paramKey是否在kb.testedParams中
                paramKey = (conf.hostname, conf.path, None, None)
                if paramKey not in kb.testedParams:
                    testSqlInj = True

            # 如果testSqlInj为True，并且conf.hostname在kb.vulnHosts中，则检查是否跳过该主机
            if testSqlInj and conf.hostname in kb.vulnHosts:
                # 如果kb.skipVulnHost为None，则提示用户是否跳过该主机的进一步测试
                if kb.skipVulnHost is None:
                    message = "SQL injection vulnerability has already been detected "
                    message += "against '%s'. Do you want to skip " % conf.hostname
                    message += "further tests involving it? [Y/n]"

                    kb.skipVulnHost = readInput(message, default='Y', boolean=True)

                # 如果kb.skipVulnHost为True，则设置testSqlInj为False
                testSqlInj = not kb.skipVulnHost

            # 如果testSqlInj为False，则跳过该URL
            if not testSqlInj:
                infoMsg = "skipping '%s'" % targetUrl
                logger.info(infoMsg)
                continue

            # 如果conf.multipleTargets为True，则提示用户是否测试该表单或URL
            if conf.multipleTargets:
                if conf.forms and conf.method:
                    message = "[%d/%s] Form:\n%s %s" % (targetCount, len(kb.targets) if isListLike(kb.targets) else '?', conf.method, targetUrl)
                else:
                    message = "[%d/%s] URL:\n%s %s" % (targetCount, len(kb.targets) if isListLike(kb.targets) else '?', HTTPMETHOD.GET, targetUrl)

                if conf.cookie:
                    message += "\nCookie: %s" % conf.cookie

                if conf.data is not None:
                    message += "\n%s data: %s" % ((conf.method if conf.method != HTTPMETHOD.GET else None) or HTTPMETHOD.POST, urlencode(conf.data or "") if re.search(r"\A\s*[<{]", conf.data or "") is None else conf.data)

                if conf.forms and conf.method:
                    # 如果conf.method为GET，并且targetUrl中不包含"?"，则跳过
                    if conf.method == HTTPMETHOD.GET and targetUrl.find("?") == -1:
                        continue

                    message += "\ndo you want to test this form? [Y/n/q] "
                    choice = readInput(message, default='Y').upper()

                    # 如果用户选择"N"，则跳过
                    if choice == 'N':
                        continue
                    # 如果用户选择"Q"，则退出
                    elif choice == 'Q':
                        break
                    else:
                        # 如果conf.method不为GET，则编辑POST数据
                        if conf.method != HTTPMETHOD.GET:
                            message = "Edit %s data [default: %s]%s: " % (conf.method, urlencode(conf.data or "") if re.search(r"\A\s*[<{]", conf.data or "None") is None else conf.data, " (Warning: blank fields detected)" if conf.data and extractRegexResult(EMPTY_FORM_FIELDS_REGEX, conf.data) else "")
                            conf.data = readInput(message, default=conf.data)
                            conf.data = _randomFillBlankFields(conf.data)
                            conf.data = urldecode(conf.data) if conf.data and urlencode(DEFAULT_GET_POST_DELIMITER, None) not in conf.data else conf.data

                        # 如果conf.method为GET，则编辑GET数据
                        else:
                            if '?' in targetUrl:
                                firstPart, secondPart = targetUrl.split('?', 1)
                                message = "Edit GET data [default: %s]: " % secondPart
                                test = readInput(message, default=secondPart)
                                test = _randomFillBlankFields(test)
                                conf.url = "%s?%s" % (firstPart, test)

                        # 解析目标URL
                        parseTargetUrl()

                else:
                    # 如果conf.scope为False，则提示用户是否测试该URL
                    if not conf.scope:
                        message += "\ndo you want to test this URL? [Y/n/q]"
                        choice = readInput(message, default='Y').upper()

                        # 如果用户选择"N"，则跳过
                        if choice == 'N':
                            dataToStdout(os.linesep)
                            continue
                        # 如果用户选择"Q"，则退出
                        elif choice == 'Q':
                            break
                    else:
                        pass

                    # 打印测试URL
                    infoMsg = "testing URL '%s'" % targetUrl
                    logger.info(infoMsg)

            # 设置目标环境
            setupTargetEnv()

            # 检查连接
            if not checkConnection(suppressOutput=conf.forms):
                continue

            # 如果conf.rParam为True，并且kb.originalPage不为空，则更新kb.randomPool
            if conf.rParam and kb.originalPage:
                kb.randomPool = dict([_ for _ in kb.randomPool.items() if isinstance(_[1], list)])

                for match in re.finditer(r"(?si)<select[^>]+\bname\s*=\s*[\"']([^\"']+)(.+?)</select>", kb.originalPage):
                    name, _ = match.groups()
                    options = tuple(re.findall(r"<option[^>]+\bvalue\s*=\s*[\"']([^\"']+)", _))
                    if options:
                        kb.randomPool[name] = options

            # 检查WAF
            checkWaf()

            # 如果conf.nullConnection为True，则检查空连接
            if conf.nullConnection:
                checkNullConnection()

            # 如果kb.injections为空，或者kb.injections中只有一个place为None的元素，或者kb.injection.place为None，或者kb.injection.parameter为None，则进行以下操作
            if (len(kb.injections) == 0 or (len(kb.injections) == 1 and kb.injections[0].place is None)) and (kb.injection.place is None or kb.injection.parameter is None):
                # 如果conf.string和conf.notString和conf.regexp都为空，并且PAYLOAD.TECHNIQUE.BOOLEAN在conf.technique中，则检查页面稳定性
                if not any((conf.string, conf.notString, conf.regexp)) and PAYLOAD.TECHNIQUE.BOOLEAN in conf.technique:
                    # NOTE: this is not needed anymore, leaving only to display
                    # a warning message to the user in case the page is not stable
                    checkStability()

                # Do a little prioritization reorder of a testable parameter list
                parameters = list(conf.parameters.keys())

                # Order of testing list (first to last)
                orderList = (PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER, PLACE.URI, PLACE.POST, PLACE.GET)

                for place in orderList[::-1]:
                    if place in parameters:
                        parameters.remove(place)
                        parameters.insert(0, place)

                proceed = True
                for place in parameters:
                    # Test User-Agent and Referer headers only if
                    # --level >= 3
                    skip = (place == PLACE.USER_AGENT and (kb.testOnlyCustom or conf.level < 3))
                    skip |= (place == PLACE.REFERER and (kb.testOnlyCustom or conf.level < 3))

                    # --param-filter
                    skip |= (len(conf.paramFilter) > 0 and place.upper() not in conf.paramFilter)

                    # Test Host header only if
                    # --level >= 5
                    skip |= (place == PLACE.HOST and (kb.testOnlyCustom or conf.level < 5))

                    # Test Cookie header only if --level >= 2
                    skip |= (place == PLACE.COOKIE and (kb.testOnlyCustom or conf.level < 2))

                    skip |= (place == PLACE.USER_AGENT and intersect(USER_AGENT_ALIASES, conf.skip, True) not in ([], None))
                    skip |= (place == PLACE.REFERER and intersect(REFERER_ALIASES, conf.skip, True) not in ([], None))
                    skip |= (place == PLACE.COOKIE and intersect(PLACE.COOKIE, conf.skip, True) not in ([], None))
                    skip |= (place == PLACE.HOST and intersect(PLACE.HOST, conf.skip, True) not in ([], None))

                    skip &= not (place == PLACE.USER_AGENT and intersect(USER_AGENT_ALIASES, conf.testParameter, True))
                    skip &= not (place == PLACE.REFERER and intersect(REFERER_ALIASES, conf.testParameter, True))
                    skip &= not (place == PLACE.HOST and intersect(HOST_ALIASES, conf.testParameter, True))
                    skip &= not (place == PLACE.COOKIE and intersect((PLACE.COOKIE,), conf.testParameter, True))

                    if skip:
                        continue

                    if place not in conf.paramDict or place not in conf.parameters:
                        continue

                    paramDict = conf.paramDict[place]

                    paramType = conf.method if conf.method not in (None, HTTPMETHOD.GET, HTTPMETHOD.POST) else place

                    for parameter, value in paramDict.items():
                        if not proceed:
                            break

                        kb.vainRun = False
                        testSqlInj = True
                        paramKey = (conf.hostname, conf.path, place, parameter)

                        if kb.processUserMarks:
                            if testSqlInj and place not in (PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER, PLACE.URI):
                                if kb.processNonCustom is None:
                                    message = "other non-custom parameters found. "
                                    message += "Do you want to process them too? [Y/n/q] "
                                    choice = readInput(message, default='Y').upper()

                                    if choice == 'Q':
                                        raise SqlmapUserQuitException
                                    else:
                                        kb.processNonCustom = choice == 'Y'

                                if not kb.processNonCustom:
                                    infoMsg = "skipping %sparameter '%s'" % ("%s " % paramType if paramType != parameter else "", parameter)
                                    logger.info(infoMsg)
                                    continue

                        if paramKey in kb.testedParams:
                            testSqlInj = False

                            infoMsg = "skipping previously processed %sparameter '%s'" % ("%s " % paramType if paramType != parameter else "", parameter)
                            logger.info(infoMsg)

                        elif any(_ in conf.testParameter for _ in (parameter, removePostHintPrefix(parameter))):
                            pass

                        elif parameter in conf.rParam:
                            testSqlInj = False

                            infoMsg = "skipping randomizing %sparameter '%s'" % ("%s " % paramType if paramType != parameter else "", parameter)
                            logger.info(infoMsg)

                        elif parameter in conf.skip or kb.postHint and parameter.split(' ')[-1] in conf.skip:
                            testSqlInj = False

                            infoMsg = "skipping %sparameter '%s'" % ("%s " % paramType if paramType != parameter else "", parameter)
                            logger.info(infoMsg)

                        elif conf.paramExclude and (re.search(conf.paramExclude, parameter, re.I) or kb.postHint and re.search(conf.paramExclude, parameter.split(' ')[-1], re.I) or re.search(conf.paramExclude, place, re.I)):
                            testSqlInj = False

                            infoMsg = "skipping %sparameter '%s'" % ("%s " % paramType if paramType != parameter else "", parameter)
                            logger.info(infoMsg)

                        elif conf.csrfToken and re.search(conf.csrfToken, parameter, re.I):
                            testSqlInj = False

                            infoMsg = "skipping anti-CSRF token parameter '%s'" % parameter
                            logger.info(infoMsg)

                        # Ignore session-like parameters for --level < 4
                        elif conf.level < 4 and (parameter.upper() in IGNORE_PARAMETERS or any(_ in parameter.lower() for _ in CSRF_TOKEN_PARAMETER_INFIXES) or parameter.upper().startswith(GOOGLE_ANALYTICS_COOKIE_PREFIX)):
                            testSqlInj = False

                            infoMsg = "ignoring %sparameter '%s'" % ("%s " % paramType if paramType != parameter else "", parameter)
                            logger.info(infoMsg)

                        elif PAYLOAD.TECHNIQUE.BOOLEAN in conf.technique or conf.skipStatic:
                            check = checkDynParam(place, parameter, value)

                            if not check:
                                warnMsg = "%sparameter '%s' does not appear to be dynamic" % ("%s " % paramType if paramType != parameter else "", parameter)
                                logger.warning(warnMsg)

                                if conf.skipStatic:
                                    infoMsg = "skipping static %sparameter '%s'" % ("%s " % paramType if paramType != parameter else "", parameter)
                                    logger.info(infoMsg)

                                    testSqlInj = False
                            else:
                                infoMsg = "%sparameter '%s' appears to be dynamic" % ("%s " % paramType if paramType != parameter else "", parameter)
                                logger.info(infoMsg)

                        kb.testedParams.add(paramKey)

                        if testSqlInj:
                            try:
                                if place == PLACE.COOKIE:
                                    pushValue(kb.mergeCookies)
                                    kb.mergeCookies = False

                                check = heuristicCheckSqlInjection(place, parameter)

                                # 如果启发式检查结果不是正数，则跳过
                                if check != HEURISTIC_TEST.POSITIVE:
                                    if conf.smart or (kb.ignoreCasted and check == HEURISTIC_TEST.CASTED):
                                        infoMsg = "skipping %sparameter '%s'" % ("%s " % paramType if paramType != parameter else "", parameter)
                                        logger.info(infoMsg)
                                        continue

                                infoMsg = "testing for SQL injection on %sparameter '%s'" % ("%s " % paramType if paramType != parameter else "", parameter)
                                logger.info(infoMsg)

                                injection = checkSqlInjection(place, parameter, value)
                                proceed = not kb.endDetection
                                injectable = False

                                # 如果注入点存在
                                if getattr(injection, "place", None) is not None:
                                    # 如果注入点被标记为假阳性或不可利用，则将其添加到假阳性列表中
                                    if NOTE.FALSE_POSITIVE_OR_UNEXPLOITABLE in injection.notes:
                                        kb.falsePositives.append(injection)
                                    else:
                                        injectable = True

                                        kb.injections.append(injection)

                                        # 如果没有发出警报，则发出警报
                                        if not kb.alerted:
                                            if conf.alert:
                                                infoMsg = "executing alerting shell command(s) ('%s')" % conf.alert
                                                logger.info(infoMsg)
                                                try:
                                                    process = subprocess.Popen(conf.alert, shell=True)
                                                    process.wait()
                                                except Exception as ex:
                                                    errMsg = "error occurred while executing '%s' ('%s')" % (conf.alert, getSafeExString(ex))
                                                    logger.error(errMsg)

                                            kb.alerted = True

                                        # In case when user wants to end detection phase (Ctrl+C)
                                        if not proceed:
                                            break

                                        msg = "%sparameter '%s' " % ("%s " % injection.place if injection.place != injection.parameter else "", injection.parameter)
                                        msg += "is vulnerable. Do you want to keep testing the others (if any)? [y/N] "

                                        if not readInput(msg, default='N', boolean=True):
                                            proceed = False
                                            paramKey = (conf.hostname, conf.path, None, None)
                                            kb.testedParams.add(paramKey)

                                if not injectable:
                                    warnMsg = "%sparameter '%s' does not seem to be injectable" % ("%s " % paramType if paramType != parameter else "", parameter)
                                    logger.warning(warnMsg)

                            finally:
                                if place == PLACE.COOKIE:
                                    kb.mergeCookies = popValue()

            # 如果kb.injections的长度为0，或者长度为1且kb.injections[0].place为None，则执行以下代码
            if len(kb.injections) == 0 or (len(kb.injections) == 1 and kb.injections[0].place is None):
                # 如果kb.vainRun为True且conf.multipleTargets为False，则执行以下代码
                if kb.vainRun and not conf.multipleTargets:
                    # 定义错误信息
                    errMsg = "no parameter(s) found for testing in the provided data "
                    errMsg += "(e.g. GET parameter 'id' in 'www.site.com/index.php?id=1')"
                    # 如果kb.originalPage存在，则执行以下代码
                    if kb.originalPage:
                        advice = []
                        # 如果conf.forms为False且kb.originalPage中存在<form，则将--forms添加到advice中
                        if not conf.forms and re.search(r"<form", kb.originalPage) is not None:
                            advice.append("--forms")
                        # 如果conf.crawlDepth为False且kb.originalPage中存在href，则将--crawl=2添加到advice中
                        if not conf.crawlDepth and re.search(r"href=[\"']/?\w", kb.originalPage) is not None:
                            advice.append("--crawl=2")
                        # 如果advice不为空，则将advice添加到错误信息中
                        if advice:
                            errMsg += ". You are advised to rerun with '%s'" % ' '.join(advice)
                    # 抛出SqlmapNoneDataException异常，并传递错误信息
                    raise SqlmapNoneDataException(errMsg)
                # 否则，执行以下代码
                else:
                    # 定义错误信息
                    errMsg = "all tested parameters do not appear to be injectable."

                    # 如果conf.level小于5或conf.risk小于3，则将以下信息添加到错误信息中
                    if conf.level < 5 or conf.risk < 3:
                        errMsg += " Try to increase values for '--level'/'--risk' options "
                        errMsg += "if you wish to perform more tests."

                    # 如果conf.technique为列表且长度小于5，则将以下信息添加到错误信息中
                    if isinstance(conf.technique, list) and len(conf.technique) < 5:
                        errMsg += " Rerun without providing the option '--technique'."

                    # 如果conf.textOnly为False且kb.originalPage存在，则执行以下代码
                    if not conf.textOnly and kb.originalPage:
                        # 计算kb.originalPage中文本内容的百分比
                        percent = (100.0 * len(getFilteredPageContent(kb.originalPage)) / len(kb.originalPage))

                        # 如果kb.dynamicMarkings为True，则将以下信息添加到错误信息中
                        if kb.dynamicMarkings:
                            errMsg += " You can give it a go with the switch '--text-only' "
                            errMsg += "if the target page has a low percentage "
                            errMsg += "of textual content (~%.2f%% of " % percent
                            errMsg += "page content is text)."
                        # 如果文本内容的百分比小于LOW_TEXT_PERCENT且kb.errorIsNone为False，则将以下信息添加到错误信息中
                        elif percent < LOW_TEXT_PERCENT and not kb.errorIsNone:
                            errMsg += " Please retry with the switch '--text-only' "
                            errMsg += "(along with --technique=BU) as this case "
                            errMsg += "looks like a perfect candidate "
                            errMsg += "(low textual content along with inability "
                            errMsg += "of comparison engine to detect at least "
                            errMsg += "one dynamic parameter)."

                    # 如果kb.heuristicTest为HEURISTIC_TEST.POSITIVE，则将以下信息添加到错误信息中
                    if kb.heuristicTest == HEURISTIC_TEST.POSITIVE:
                        errMsg += " As heuristic test turned out positive you are "
                        errMsg += "strongly advised to continue on with the tests."

                    if conf.string:
                        errMsg += " Also, you can try to rerun by providing a "
                        errMsg += "valid value for option '--string' as perhaps the string you "
                        errMsg += "have chosen does not match "
                        errMsg += "exclusively True responses."
                    elif conf.regexp:
                        errMsg += " Also, you can try to rerun by providing a "
                        errMsg += "valid value for option '--regexp' as perhaps the regular "
                        errMsg += "expression that you have chosen "
                        errMsg += "does not match exclusively True responses."

                    if not conf.tamper:
                        errMsg += " If you suspect that there is some kind of protection mechanism "
                        errMsg += "involved (e.g. WAF) maybe you could try to use "
                        errMsg += "option '--tamper' (e.g. '--tamper=space2comment')"

                        if not conf.randomAgent:
                            errMsg += " and/or switch '--random-agent'"

                    raise SqlmapNotVulnerableException(errMsg.rstrip('.'))
            else:
                # Flush the flag
                kb.testMode = False

                # Save the results to a file
                _saveToResultsFile()
                # Save the results to a hash database
                _saveToHashDB()
                # Show the injections
                _showInjections()
                # Select an injection
                _selectInjection()

            # Check if there is an injection
            if kb.injection.place is not None and kb.injection.parameter is not None:
                # Check if there are multiple targets
                if conf.multipleTargets:
                    # Ask the user if they want to exploit the SQL injection
                    message = "do you want to exploit this SQL injection? [Y/n] "
                    condition = readInput(message, default='Y', boolean=True)
                else:
                    # Set the condition to True
                    condition = True

                # If the condition is True, execute the action
                if condition:
                    action()

        except KeyboardInterrupt:
            # 捕获用户中断异常
            if kb.lastCtrlCTime and (time.time() - kb.lastCtrlCTime < 1):
                # 如果用户在1秒内连续按下了Ctrl+C，则设置kb.multipleCtrlC为True，并抛出SqlmapUserQuitException异常
                kb.multipleCtrlC = True
                raise SqlmapUserQuitException("user aborted (Ctrl+C was pressed multiple times)")

            kb.lastCtrlCTime = time.time()

            if conf.multipleTargets:
                # 如果在多个目标模式下，用户中断了扫描
                warnMsg = "user aborted in multiple target mode"
                logger.warning(warnMsg)

                message = "do you want to skip to the next target in list? [Y/n/q]"
                choice = readInput(message, default='Y').upper()

                if choice == 'N':
                    # 如果用户选择不跳过，则返回False
                    return False
                elif choice == 'Q':
                    # 如果用户选择退出，则抛出SqlmapUserQuitException异常
                    raise SqlmapUserQuitException
            else:
                # 如果不在多个目标模式下，则抛出异常
                raise

        except SqlmapSkipTargetException:
            # 捕获SqlmapSkipTargetException异常，不做任何操作
            pass

        except SqlmapUserQuitException:
            # 捕获SqlmapUserQuitException异常，抛出异常
            raise

        except SqlmapSilentQuitException:
            # 捕获SqlmapSilentQuitException异常，抛出异常
            raise

        except SqlmapBaseException as ex:
            # 捕获SqlmapBaseException异常，获取异常信息
            errMsg = getSafeExString(ex)

            if conf.multipleTargets:
                # 如果在多个目标模式下，保存结果到文件，并跳过下一个目标
                _saveToResultsFile()

                errMsg += ", skipping to the next target"
                logger.error(errMsg.lstrip(", "))
            else:
                # 如果不在多个目标模式下，记录错误信息，并返回False
                logger.critical(errMsg)
                return False

        finally:
            # 显示HTTP错误码
            showHttpErrorCodes()

            if kb.maxConnectionsFlag:
                # 如果设置了最大连接数，则记录警告信息
                warnMsg = "it appears that the target "
                warnMsg += "has a maximum connections "
                warnMsg += "constraint"
                logger.warning(warnMsg)

    if kb.dataOutputFlag and not conf.multipleTargets:
        # 如果设置了数据输出，且不在多个目标模式下，则记录信息
        logger.info("fetched data logged to text files under '%s'" % conf.outputPath)

    if conf.multipleTargets:
        if conf.resultsFile:
            # 如果在多个目标模式下，设置了结果文件，则记录信息
            infoMsg = "you can find results of scanning in multiple targets "
            infoMsg += "mode inside the CSV file '%s'" % conf.resultsFile
            logger.info(infoMsg)

    # 返回True
    return True
