#!/usr/bin/env python

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

Shared detection primitives for the non-SQL injection techniques (--nosql, --xpath, --ldap, --hql,
--ssti, --graphql, --xxe). Each of those engines historically carried its own copy of the same
response-comparison, error/blocked-status filtering, blind-bit classification and user-oracle logic;
this module is the single home for that shared machinery so the behavior is uniform and reviewable
in one place rather than drifting across six files.
"""

import difflib
import re

from lib.core.common import urldecode
from lib.core.common import urlencode
from lib.core.convert import getUnicode
from lib.core.data import conf
from lib.core.settings import REFLECTED_VALUE_MARKER
from lib.core.settings import UPPER_RATIO_BOUND
from lib.parse.html import htmlParser

# Minimum similarity margin by which a blind-extraction response must lean toward the confirmed TRUE
# model over the FALSE model before a bit is accepted as true (else ambiguous -> false). Deliberately
# generous: a small (e.g. 5%) margin lets a noisy page fabricate values one character at a time.
EXTRACT_MATCH_MARGIN = 0.2

# HTTP statuses that mean the response is BLOCKED (WAF / rate-limit); together with 5xx these must
# never be fed to a boolean oracle as if they were application content.
BLOCKED_HTTP_CODES = frozenset((403, 429))

# generic SQL/DBMS error marker (mirrors lib/parse/html.py's own generic check), used alongside the
# DBMS-specific errors.xml signatures that htmlParser() recognizes
_SQL_ERROR_REGEX = re.compile(r"(?i)SQL (warning|error|syntax)")


def ratio(first, second):
    """Content-similarity ratio shared by every non-SQL detector (difflib quick_ratio over the two
    response bodies) - one implementation instead of six identical copies."""
    return difflib.SequenceMatcher(None, first or "", second or "").quick_ratio()


def stripReflection(page, payload):
    """
    Remove the payload from the page before any two responses are compared.

    An endpoint that merely ECHOES the parameter returns a different page for every different payload, so
    a true/false differential is satisfied without a single expression, filter or operator ever being
    interpreted. That is not injection, and it is how plain reflective search pages were reported as
    XPath / LDAP / NoSQL injectable.

    Deliberately NOT lib.core.common.removeReflectiveValues: that one is a scan-wide heuristic which
    switches ITSELF OFF after REFLECTIVE_MISS_THRESHOLD misses, after a regex timeout, and during
    heuristic mode - so a detection guard built on it silently stops guarding mid-scan. This is a plain,
    deterministic substring removal with no global state and no failure mode. The two are complementary,
    and the engines apply both.

    The raw, URL-decoded and URL-encoded forms are all removed: a payload travels encoded, and an
    application may echo whichever of the three it happened to hold.
    """

    if not page or not payload:
        return page

    retVal = getUnicode(page)
    forms = set()
    for form in (payload, urldecode(payload, convall=True), urlencode(payload, safe="")):
        try:
            forms.add(getUnicode(form))
        except Exception:
            pass
    # longest first, so a shorter form cannot chop a longer one into unremovable pieces
    for form in sorted(filter(None, forms), key=len, reverse=True):
        if form in retVal:
            retVal = retVal.replace(form, REFLECTED_VALUE_MARKER)
    return retVal


def blockedStatus(code):
    """True when an HTTP status means the response is blocked/errored (a 5xx, or a WAF/rate-limit
    403/429) and so is not a usable oracle sample. `_send()` implementations return None for these
    (and for transport exceptions) so the boolean routines, which reject None, can never decide on
    a non-answer."""
    return bool(code) and (code >= 500 or code in BLOCKED_HTTP_CODES)


def sqlErrorPresent(page):
    """True when the response carries a recognized SQL/DBMS error - either a DBMS-specific signature
    from sqlmap's errors.xml (via htmlParser) or the generic 'SQL warning/error/syntax' marker. The
    non-SQL detectors treat such a page as NOT a valid boolean template, so a payload that merely
    trips a back-end SQL syntax error cannot fake a true/false divergence and get a plainly SQL-
    injectable parameter mis-reported as NoSQL / XPath / LDAP / HQL."""
    page = page or ""
    return bool(htmlParser(page)) or bool(_SQL_ERROR_REGEX.search(page))


# Visible placeholder for a single recovered cell/attribute whose extraction was INCONCLUSIVE (the
# oracle stayed ambiguous after retries). Rendered in dumps in place of the value so a failed cell is
# never silently shown as a genuine empty string - `None` from an extractor means "unknown", `""` means
# "really empty", and they must stay distinguishable in the output.
INCONCLUSIVE_MARK = "<inconclusive>"


class InconclusiveError(Exception):
    """Raised by resolveBit(abort=True) when a bit stays INCONCLUSIVE after retries. Per-value
    extractors catch it to ABORT the current value (return what was recovered so far, marked
    incomplete) instead of substituting a semantic False - which would corrupt a length, pick the
    wrong half of a bisection, or truncate enumeration."""


class Decision(object):
    """Tri(+)-state blind-inference outcome. INCONCLUSIVE is deliberately DISTINCT from FALSE: an
    ambiguous comparison (equally close to both models, close to neither, or a transport/blocked
    anomaly) must be retried/aborted, NOT silently read as a semantic false - which would shorten a
    value, pick the wrong half of a bisection or truncate enumeration."""
    TRUE = "TRUE"
    FALSE = "FALSE"
    INCONCLUSIVE = "INCONCLUSIVE"


def decide(page, trueModel, falseModel, margin=EXTRACT_MATCH_MARGIN):
    """Classify a blind-inference response against the two calibrated models, returning a Decision.
    TRUE when it resembles the confirmed TRUE model (identical, or clearly closer to it than to the
    FALSE model by `margin`); FALSE when it resembles the FALSE model; INCONCLUSIVE when it leans to
    neither (so the caller can retry or abort rather than guess)."""
    if page is None:
        return Decision.INCONCLUSIVE
    simTrue, simFalse = ratio(trueModel, page), ratio(falseModel, page)
    if simTrue >= UPPER_RATIO_BOUND and simTrue >= simFalse:
        return Decision.TRUE
    if simFalse >= UPPER_RATIO_BOUND and simFalse >= simTrue:
        return Decision.FALSE
    if (simTrue - simFalse) >= margin:
        return Decision.TRUE
    if (simFalse - simTrue) >= margin:
        return Decision.FALSE
    return Decision.INCONCLUSIVE


def resolveBit(page, trueModel, falseModel, resend, retries=2, margin=EXTRACT_MATCH_MARGIN, abort=True):
    """Resolve one blind bit to True/False. On an INCONCLUSIVE first read, RE-SEND (fresh, cache-
    bypassing) up to `retries` times to ride out transient jitter before deciding. `resend` is a
    0-arg callable returning a fresh page (or None on error/block). If a bit stays INCONCLUSIVE after
    the retries: raise InconclusiveError when `abort` (the caller aborts the CURRENT VALUE rather than
    corrupt it), else return False."""
    d = decide(page, trueModel, falseModel, margin)
    tries = 0
    while d is Decision.INCONCLUSIVE and tries < retries:
        page = resend()
        if page is None:
            break
        d = decide(page, trueModel, falseModel, margin)
        tries += 1
    if d is Decision.INCONCLUSIVE and abort:
        raise InconclusiveError()
    return d is Decision.TRUE


def leansTrue(page, trueModel, falseModel, margin=EXTRACT_MATCH_MARGIN):
    """Boolean shorthand for `decide(...) is Decision.TRUE` (kept for callers that don't retry).
    A page indistinguishable from the FALSE model, or ambiguous, is NOT true - so a dynamic token, a
    changed error page, a WAF/rate-limit body or a transient exception can never fabricate a bit."""
    return decide(page, trueModel, falseModel, margin) is Decision.TRUE


def userOracleActive():
    """True when the user supplied an explicit true/false response signal (--string / --not-string /
    --regexp) that the non-SQL techniques should honor instead of relying on raw page similarity."""
    return bool(getattr(conf, "string", None) or getattr(conf, "notString", None) or getattr(conf, "regexp", None))


def userDecision(page):
    """Classify a response with the user's explicit oracle (--string / --not-string / --regexp),
    returning True/False, or None when no override is set (caller falls back to content comparison).
    Page-only: HTTP-code overrides (--code) stay per-engine, where the status line is available.

    This routes the non-SQL boolean detectors through sqlmap's documented detection overrides - the
    same knobs the SQL engine honors - rather than discarding them for a fixed similarity ratio."""
    page = page or ""
    if getattr(conf, "string", None):
        return conf.string in page
    if getattr(conf, "notString", None):
        return conf.notString not in page
    if getattr(conf, "regexp", None):
        return re.search(conf.regexp, page) is not None
    return None
