#!/usr/bin/env python3
import os
import re
import sys
import subprocess
from urllib.parse import urlparse, urljoin, urldefrag
from bs4 import BeautifulSoup

USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"

class CurlMirror:
    def __init__(self, base_url, output_dir):
        self.base_url = base_url.rstrip('/')
        self.domain = urlparse(self.base_url).netloc
        self.output_dir = os.path.abspath(output_dir)
        self.visited_urls = set()
        self.downloaded_files = {}

    def run_curl(self, url, output_file=None):
        """Wykonywanie żądania przez natywny proces cURL z dysku."""
        cmd = [
            "curl", "-sSL",
            "-A", USER_AGENT,
            "-H", "Accept-Language: pl-PL,pl;q=0.9,en-US;q=0.8,en;q=0.7",
            "-H", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
            "--compressed",
            url
        ]
        if output_file:
            os.makedirs(os.path.dirname(output_file), exist_ok=True)
            cmd.extend(["-o", output_file])
            res = subprocess.run(cmd)
            return res.returncode == 0
        else:
            res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            return res.stdout.decode('utf-8', errors='ignore') if res.returncode == 0 else None

    def url_to_local_path(self, url):
        """Mapuje adres URL na fizyczną, bezpieczną ścieżkę w systemie Linux."""
        parsed = urlparse(url)
        path = parsed.path
        
        if not path or path.endswith('/'):
            path += "index.html"
            
        # Odcięcie niedozwolonych znaków
        path = path.lstrip('/')
        
        # Jeśli plik nie ma rozszerzenia (np. czysty routing), traktuj jako html
        ext = os.path.splitext(path)[1]
        if not ext:
            path += ".html"
            
        # Zapisz w katalogu odpowiadającym domenie
        local_rel = os.path.join(parsed.netloc, path)
        return os.path.join(self.output_dir, local_rel)

    def extract_css_assets(self, css_content, css_url):
        """Wyciąga zasoby zaszyte w CSS (np. @import, url('font.woff2'))."""
        urls = []
        # Wyrażenie dopasowujące url(...) z cudzysłowami lub bez
        matches = re.findall(r'url\(\s*[\'"]?([^\'")]+)[\'"]?\s*\)', css_content, re.IGNORECASE)
        for rel in matches:
            if rel.startswith('data:') or rel.startswith('#'):
                continue
            full_url = urljoin(css_url, rel)
            urls.append(full_url)
        return urls

    def clone_asset(self, asset_url):
        """Pobiera pojedynczy asset (obrazek, font, JS) przez curl."""
        asset_url, _ = urldefrag(asset_url)
        if asset_url in self.visited_urls:
            return self.downloaded_files.get(asset_url)
        
        self.visited_urls.add(asset_url)
        local_path = self.url_to_local_path(asset_url)
        
        print(f"[*] cURL -> {asset_url}")
        success = self.run_curl(asset_url, local_path)
        
        if success:
            self.downloaded_files[asset_url] = local_path
            
            # Jeśli ściągnięty plik to CSS, parsujemy go rekurencyjnie pod kątem czcionek i tła
            if local_path.endswith('.css'):
                self.process_css(local_path, asset_url)
                
            return local_path
        return None

    def process_css(self, local_css_path, css_url):
        """Parsuje pobrany plik CSS i przepisuje w nim ścieżki."""
        try:
            with open(local_css_path, 'r', encoding='utf-8', errors='ignore') as f:
                content = f.read()
        except Exception:
            return

        assets = self.extract_css_assets(content, css_url)
        for asset_url in assets:
            downloaded_dest = self.clone_asset(asset_url)
            if downloaded_dest:
                # Zamiana ścieżki w pliku CSS na relatywną względem tego CSS
                rel_to_css = os.path.relpath(downloaded_dest, os.path.dirname(local_css_path))
                content = content.replace(asset_url, rel_to_css)
                # Zamiana relatywnych odnośników źródłowych
                parsed = urlparse(asset_url)
                content = content.replace(parsed.path, rel_to_css)

        with open(local_css_path, 'w', encoding='utf-8') as f:
            f.write(content)

    def start(self):
        print(f"[+] Inicjalizacja pobierania: {self.base_url}")
        html = self.run_curl(self.base_url)
        if not html:
            print("[!] Błąd cURL: Nie można pobrać strony głównej.")
            return

        index_path = os.path.join(self.output_dir, "index.html")
        os.makedirs(self.output_dir, exist_ok=True)
        soup = BeautifulSoup(html, 'html.parser')

        # Tagi zawierające zasoby do pobrania
        tags_attributes = [
            ('link', 'href'),
            ('script', 'src'),
            ('img', 'src'),
            ('source', 'srcset'),
            ('video', 'src'),
            ('audio', 'src'),
            ('use', 'href') # SVG sprites
        ]

        for tag, attr in tags_attributes:
            for el in soup.find_all(tag):
                raw_val = el.get(attr)
                if not raw_val or raw_val.startswith('data:') or raw_val.startswith('javascript:'):
                    continue

                # Obsługa atrybutu srcset (wiele URLi rozdzielonych przecinkiem)
                if attr == 'srcset':
                    new_srcset = []
                    for entry in raw_val.split(','):
                        parts = entry.strip().split()
                        if parts:
                            u = urljoin(self.base_url, parts[0])
                            downloaded = self.clone_asset(u)
                            if downloaded:
                                rel_link = os.path.relpath(downloaded, self.output_dir)
                                parts[0] = rel_link
                            new_srcset.append(" ".join(parts))
                    el[attr] = ", ".join(new_srcset)
                    continue

                full_url = urljoin(self.base_url, raw_val)
                downloaded_file = self.clone_asset(full_url)
                
                if downloaded_file:
                    # Przepisz odnośnik na dysku na ścieżkę relatywną do index.html
                    el[attr] = os.path.relpath(downloaded_file, self.output_dir)

        # Zapisz wyczyszczony, przelinkowany plik HTML
        with open(index_path, 'w', encoding='utf-8') as f:
            f.write(str(soup))

        print(f"\n[V] Sukces: Klon 1:1 zapisany w: {self.output_dir}")
        print(f"[V] Uruchom podgląd poleceniem: cd {self.output_dir} && python3 -m http.server 8080")

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print(f"Użycie: {sys.argv[0]} https://strona-do-sklonowania.pl [katalog_docelowy]")
        sys.exit(1)
        
    target = sys.argv[1]
    out = sys.argv[2] if len(sys.argv) > 2 else "./strona_klon"
    mirror = CurlMirror(target, out)
    mirror.start()