#!/usr/bin/env python

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

Unit coverage for PURE functions in lib/request/basic.py.

These exercise getHeuristicCharEncoding (with its kb.cache.encoding memoization)
and decodePage's charset + HTML-entity decoding branches, in isolation - WITHOUT
touching the network, the DBMS or any interactive prompt.

stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
"""

import os
import sys
import unittest

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()

from lib.core.data import conf, kb


class TestBasicHeuristicCharEncoding(unittest.TestCase):
    def test_ascii(self):
        from lib.request.basic import getHeuristicCharEncoding
        self.assertEqual(getHeuristicCharEncoding(b"<html></html>"), "ascii")

    def test_cache_hit_returns_same(self):
        from lib.request.basic import getHeuristicCharEncoding
        page = b"<html>hello world</html>"
        first = getHeuristicCharEncoding(page)
        # second call for identical page must come back identical (and from cache)
        self.assertEqual(getHeuristicCharEncoding(page), first)
        key = (len(page), hash(page))
        self.assertEqual(kb.cache.encoding.get(key), first)


class TestBasicDecodePage(unittest.TestCase):
    """decodePage charset + HTML-entity decoding branches."""

    def setUp(self):
        self._old_encoding = conf.encoding
        self._old_null = conf.nullConnection
        conf.nullConnection = False

    def tearDown(self):
        conf.encoding = self._old_encoding
        conf.nullConnection = self._old_null

    def test_html_entity_amp(self):
        from lib.request.basic import decodePage
        from lib.core.common import getText
        conf.encoding = None
        self.assertEqual(
            getText(decodePage(b"<html>foo&amp;bar</html>", None, "text/html; charset=utf-8")),
            "<html>foo&bar</html>",
        )

    def test_numeric_hex_entity_tab(self):
        from lib.request.basic import decodePage
        from lib.core.common import getText
        conf.encoding = None
        self.assertEqual(getText(decodePage(b"&#x9;", None, "text/html; charset=utf-8")), "\t")

    def test_numeric_hex_entity_letter(self):
        from lib.request.basic import decodePage
        from lib.core.common import getText
        conf.encoding = None
        self.assertEqual(getText(decodePage(b"&#x4A;", None, "text/html; charset=utf-8")), "J")

    def test_unicode_entity(self):
        from lib.request.basic import decodePage
        conf.encoding = None
        self.assertEqual(decodePage(b"&#x2122;", None, "text/html; charset=utf-8"), u"\u2122")

    def test_empty_page(self):
        from lib.request.basic import decodePage
        from lib.core.common import getText
        # empty page short-circuits to getUnicode(page)
        self.assertEqual(getText(decodePage(b"", None, "text/html")), "")


class TestRedirectSetCookieMerge(unittest.TestCase):
    """A 302 that sets more than one cookie sends SEPARATE Set-Cookie headers (RFC-6265 forbids
    comma-folding them). The handler must merge ALL of them into the follow-up request's Cookie
    header; a __getitem__ read returns only the first, silently dropping the 2nd+ (e.g. a CSRF token)."""

    _CONF = ("cookieDel", "scope")

    def setUp(self):
        self._c = dict((k, conf.get(k)) for k in self._CONF)
        self._redirect = kb.choices.get("redirect") if kb.get("choices") else None

    def tearDown(self):
        for k, v in self._c.items():
            conf[k] = v
        if kb.get("choices"):
            kb.choices.redirect = self._redirect

    def test_all_set_cookies_merged_across_redirect(self):
        from lib.core.enums import HTTP_HEADER, REDIRECTION
        from thirdparty import six
        from thirdparty.six.moves import urllib as _urllib
        import lib.request.redirecthandler as rh

        conf.cookieDel = None
        conf.scope = None
        kb.choices.redirect = REDIRECTION.YES

        raw = ("Location: http://example.com/home\r\n"
               "Set-Cookie: sid=NEW; Path=/; HttpOnly\r\n"
               "Set-Cookie: csrf=XYZ; Path=/\r\n\r\n")
        # build the same headers object type the redirect handler receives on this interpreter:
        # py3 http.client.HTTPMessage (email.message.Message, get_all) / py2 mimetools.Message (getheaders)
        if six.PY2:
            import mimetools
            headers = mimetools.Message(six.StringIO(raw))
        else:
            from email import message_from_string
            from thirdparty.six.moves.http_client import HTTPMessage
            headers = message_from_string(raw, _class=HTTPMessage)

        # stub the network-following parent so the test touches no socket
        saved = _urllib.request.HTTPRedirectHandler.http_error_302
        _urllib.request.HTTPRedirectHandler.http_error_302 = lambda self, req, fp, code, msg, headers: fp
        try:
            req = _urllib.request.Request("http://example.com/login", headers={"Cookie": "sid=OLD"})
            fp = _urllib.response.addinfourl(six.BytesIO(b""), headers, req.get_full_url())
            rh.SmartRedirectHandler().http_error_302(req, fp, 302, "Found", headers)
        finally:
            _urllib.request.HTTPRedirectHandler.http_error_302 = saved

        merged = req.headers.get(HTTP_HEADER.COOKIE) or req.headers.get("Cookie") or ""
        self.assertIn("sid=NEW", merged)
        self.assertIn("csrf=XYZ", merged)   # the 2nd Set-Cookie must survive the redirect (order-independent)


class TestForgeHeadersCookieMerge(unittest.TestCase):
    """A domain-scoped jar cookie (Domain=example.com -> '.example.com') must merge into the
    request for the apex host, not be dropped by a naive endswith() domain check."""

    _CONF = ("cj", "hostname", "httpHeaders", "loadCookies", "cookieDel", "parameters", "csrfToken", "safeUrl")
    _KB = ("mergeCookies", "testMode", "injection")

    def setUp(self):
        self._c = dict((k, conf.get(k)) for k in self._CONF)
        self._k = dict((k, kb.get(k)) for k in self._KB)

    def tearDown(self):
        for k, v in self._c.items():
            conf[k] = v
        for k, v in self._k.items():
            kb[k] = v

    def _jar_with_domain_cookie(self):
        try:
            from http.cookiejar import CookieJar, Cookie
        except ImportError:
            from cookielib import CookieJar, Cookie
        # a domain-scoped cookie the jar stores as '.example.com' (domain_specified=True),
        # exactly as it would after Set-Cookie: sid=NEW; Domain=example.com
        cookie = Cookie(version=0, name="sid", value="NEW", port=None, port_specified=False,
                        domain=".example.com", domain_specified=True, domain_initial_dot=True,
                        path="/", path_specified=True, secure=False, expires=None, discard=True,
                        comment=None, comment_url=None, rest={})
        cj = CookieJar()
        cj.set_cookie(cookie)
        return cj

    def test_domain_cookie_merged_on_apex_host(self):
        from lib.request.basic import forgeHeaders
        from lib.core.enums import PLACE, HTTP_HEADER
        from lib.core.datatype import AttribDict

        conf.cj = self._jar_with_domain_cookie()
        conf.hostname = "example.com"                       # apex host == cookie domain
        conf.httpHeaders = [(HTTP_HEADER.COOKIE, "sid=OLD")]
        conf.loadCookies = False
        conf.cookieDel = None
        conf.parameters = {}
        conf.csrfToken = conf.safeUrl = None
        kb.mergeCookies = True
        kb.testMode = False
        kb.injection = AttribDict()
        kb.injection.place = PLACE.GET

        headers = forgeHeaders()
        # before the fix the domain cookie was skipped for the apex host, leaving 'sid=OLD'
        self.assertEqual(headers.get(HTTP_HEADER.COOKIE), "sid=NEW")


if __name__ == "__main__":
    unittest.main(verbosity=2)
