#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# This file is part of the Wapiti project (https://wapiti-scanner.github.io)
# Copyright (C) 2023-2024 Cyberwatch
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
import hashlib
import json
import re
from typing import Optional, Tuple

from httpx import RequestError
from asyncio import TimeoutError
from playwright.async_api import async_playwright, Page, Browser, Error as PlaywrightError

from wapitiCore.parsers.html_parser import Html
from wapitiCore.net import Request
from wapitiCore.net.web import urlparse
from wapitiCore.attack.cms.cms_common import CommonCMS, MSG_TECHNO_VERSIONED, calculate_git_hash
from wapitiCore.net.classes import CrawlerConfiguration
from wapitiCore.net.response import Response
from wapitiCore.definitions.fingerprint_webapp import SoftwareVersionDisclosureFinding
from wapitiCore.definitions.fingerprint import SoftwareNameDisclosureFinding
from wapitiCore.main.log import logging, log_blue

MSG_NO_MAGENTO = "No Magento Detected"


async def playwright_get_content(url: str, page: Page, browser: Browser, crawler_configuration: CrawlerConfiguration) -> str:
    html = ""
    try:
        await page.goto(
            url,
            timeout = crawler_configuration.timeout * 1000,
            wait_until="load"
        )

        html = await page.content()
        return html
    except PlaywrightError as exception:
        logging.exception(exception)
        return ""
    finally:
        await browser.close()
        

async def fetch_source_files(url: str, crawler_configuration: CrawlerConfiguration) -> set:
    my_files_list = set()
    proxy_settings = None
    if crawler_configuration.proxy:
        proxy_settings = {"server": crawler_configuration.proxy}

    try:
        # Initialize a headless browser session to extract source filess
        async with async_playwright() as pw:
            browser = await pw.firefox.launch(
                headless=True,
                proxy=proxy_settings,
                firefox_user_prefs={
                    "network.proxy.allow_hijacking_localhost": True,
                    "devtools.jsonview.enabled": False,
                }
            )
            context = await browser.new_context(
                ignore_https_errors=True, 
                user_agent=crawler_configuration.user_agent
            )
            page = await context.new_page()

            html = await playwright_get_content(url, page, browser, crawler_configuration)
            parsed = Html(html, url)

            # Collect JS and CSS file links
            js_files = {script['src'] for script in parsed.soup.find_all('script', src=True)}
            css_files = {link['href'] for link in parsed.soup.find_all('link', rel="stylesheet", href=True)}

            my_files_list.update(js_files)
            my_files_list.update(css_files)

    except (PlaywrightError, FileNotFoundError, TimeoutError) as exception:
            # Playwright browser may be missing, etc
            logging.exception(exception)
            logging.warning(
                "Could not run headless browser. "
                "Make sure playwright is installed (`pip install playwright`) "
                "and browsers are installed (`playwright install`)."
            )
    except Exception as e:
        logging.error("An error occurred while fetching JS files and CSS files: %s", e)

    return my_files_list

def get_root_url(url):
    # Parse the URL into components
    parsed_url = urlparse(url)
    # Reconstruct the root URL from the scheme and netloc
    root_url = f"{parsed_url.scheme}://{parsed_url.netloc}/"
    return root_url


class ModuleMagentoEnum(CommonCMS):
    """Detect Magento version."""
    PAYLOADS_HASH = "magento2_hash_files.json"
    versions = []
    url_list = []

    def init(self, crawler, persister, options, event, crawler_configuration):
        CommonCMS.__init__(self,
                           crawler=crawler,
                           persister=persister,
                           attack_options=options,
                           crawler_configuration=crawler_configuration)
        self.url_list = []

    async def fetch_source_list(self, url):
        self.url_list = await fetch_source_files(url, self.crawler_configuration)
        return True

    async def check_magento(self, url):
        """Return the list of urls or None if nothing was founded."""
        try:
            request = Request(f'{url}', 'GET')
            response: Response = await self.crawler.async_send(request, follow_redirects=True)
        except RequestError:
            self.network_errors += 1
            return None
        page = Html(response.content, url)
        pattern = re.compile(r'skin/frontend/.*/default/.*')

        keywords = [
            "magento_opensource", "x-magento-init", "Magento_Ui",
            "Magento_PageBuilder", "VarienForm", "Magento_Enterprise"
        ]

        if (response.is_success and
            (
                any(keyword in response.content for keyword in keywords) or
                "Magento" in response.headers.get('X-Powered-By', '')
            )
        ):
            return await self.fetch_source_list(url)

        for script in page.soup.find_all('script', src=True):
            if "Magento_Theme" in script['src']:
                return await self.fetch_source_list(url)

        for link in page.soup.find_all('link', href=True):
            if pattern.search(link['href']):
                return await self.fetch_source_list(url)

        for cookie in response.cookies:
            if cookie.startswith(('frontend', 'X-Magento', 'mage-')):
                return await self.fetch_source_list(url)

        return None

    async def detect_magento_version(self, payloads_hash, hashes):
        versions = {}
        detection_db = self.get_hashes(payloads_hash)
        for content_hash, path in hashes:

            if content_hash and content_hash in detection_db:
                versions[content_hash] = detection_db[content_hash]

        if versions:
            self.versions = set.intersection(*[set(version_list) for version_list in versions.values()])

    async def get_url_hashes(self, url: str) -> Optional[Tuple[str, str]]:
        if get_root_url(url) == await self.persister.get_root_url():
            request = Request(f"{url}", "GET")
            try:
                response: Response = await self.crawler.async_send(request, follow_redirects=True)
            except RequestError:
                self.network_errors += 1
                return "",""

            file_content = response.bytes
            git_hash = calculate_git_hash(file_content)

            return git_hash, url

    async def attack(self, request: Request, response: Optional[Response] = None):
        self.finished = True
        request_to_root = Request(request.url)
        hashes = []
        if await self.check_magento(request_to_root.url):
            for url in self.url_list:
                hash_val = await self.get_url_hashes(url)
                if hash_val:
                    hashes.append(hash_val)
            await self.detect_magento_version(self.PAYLOADS_HASH, hashes)
            self.versions = sorted(self.versions, key=lambda x: x.split('.')) if self.versions else []

            magento_detected = {
                "name": "Magento",
                "versions": list(self.versions),
                "categories": ["CMS Magento"],
                "groups": ["Content"]
            }

            log_blue(
                MSG_TECHNO_VERSIONED,
                "Magento!",
                self.versions
            )

            if self.versions:
                await self.add_info(
                    finding_class=SoftwareVersionDisclosureFinding,
                    request=request_to_root,
                    info=json.dumps(magento_detected),
                )
            await self.add_info(
                finding_class=SoftwareNameDisclosureFinding,
                request=request_to_root,
                info=json.dumps(magento_detected),
            )
        else:
            log_blue(MSG_NO_MAGENTO)
