From ffb37f9d611f96524c03ea603f8247c6855b87bc Mon Sep 17 00:00:00 2001 From: am0 Date: Sat, 18 Oct 2025 13:08:14 +0200 Subject: [PATCH] =?UTF-8?q?feat/refactor:=20Chrome=20MV3=20Support=20+=20R?= =?UTF-8?q?efaktoryzacja=20architektury=20dla=20kompatybilno=C5=9Bci=20obu?= =?UTF-8?q?=20przegl=C4=85darek?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 🎯 Cel Dodanie pełnego wsparcia dla Chrome Manifest V3 przy zachowaniu kompatybilności z Firefox. Główne wyzwanie: Chrome MV3 używa service workerów zamiast persistent background pages, co wymaga nowej architektury zarządzania pamięcią. --- ## 🏗️ Architektura ### Nowa warstwa abstrakcji: lib/browser-api/ **lib/browser-api/index.ts** - Główny punkt wejścia do zunifikowanego API - Wybiera właściwą implementację na podstawie zmiennej TARGET (build-time) - Eksportuje jeden interfejs dla całej aplikacji **lib/browser-api/types.ts** - Wspólne definicje typów dla obu przeglądarek - Interfejs BrowserAPI definiujący wszystkie potrzebne metody - Typy dla tabs, badge, webRequest, cookies, extension, windows **lib/browser-api/firefox.ts** - Adapter dla Firefox browser.* API - Lazy access do globalnego obiektu browser (bezpieczne dla środowisk bez Firefox API) - Wszystkie metody zwracają Promise lub są no-op jeśli API niedostępne **lib/browser-api/chrome.ts** - Adapter dla Chrome chrome.* API - Mapowanie chrome.action → badge (różnica nazewnictwa) - Ochrona przed błędami gdy karta zostaje zamknięta (try-catch w operacjach badge) --- ## 🔧 Build System ### esbuild.config.js - **Dodano**: Rozpoznawanie zmiennej środowiskowej TARGET (firefox/chrome) - **Dodano**: Różne katalogi wyjściowe (dist-firefox, dist-chrome) - **Dodano**: Kopiowanie odpowiedniego manifestu na podstawie TARGET - **Dodano**: Plugin do konwersji ikon SVG → PNG dla Chrome (wymaga PNG w MV3) - **Zmieniono**: define zawiera teraz process.env.TARGET dostępny w runtime ### package.json - **Dodano**: Skrypty build:firefox, build:chrome, watch:firefox, watch:chrome - **Dodano**: Skrypty build-addon:firefox, build-addon:chrome do tworzenia paczek - **Dodano**: Skrypt convert-icons do generowania PNG z SVG - **Dodano**: Zależność sharp do konwersji obrazów ### scripts/convert-icons.js (NOWY PLIK) - Konwertuje assets/icon-addon.svg → PNG w rozmiarach 16, 32, 48, 128px - Wymagane dla Chrome (MV3 nie akceptuje SVG w manifestach) ### manifest-chrome.json (NOWY PLIK) - Manifest V3 dla Chrome - background.service_worker zamiast background.scripts - action zamiast browser_action - Ikony PNG zamiast SVG - host_permissions zamiast embedowanych w permissions --- ## 🧠 Pamięć i Stan (NAJWIĘKSZA ZMIANA) ### memory.ts - Kompletna refaktoryzacja **Problem Chrome MV3:** - Service worker może być wyładowany w dowolnym momencie - Brak dostępu do window.memory z popup/sidebar - chrome.extension.getBackgroundPage() zwraca null w MV3 **Rozwiązanie:** 1. **Service Worker**: Trzyma pełne dane, synchronizuje do chrome.storage.session 2. **Popup/Sidebar**: Tworzy własną instancję Memory czytając z storage 3. **Throttled sync**: Maksymalnie co 500ms zapisy do storage (wydajność) **Nowa klasa: CachedRequestCluster** - Dziedziczy po RequestCluster ale NIE ma rzeczywistych requestów - Przechowuje tylko metadane: hasCookies, exposesOrigin, hasMarks - Implementuje wszystkie wymagane metody zwracając cached state - Używana TYLKO w popup/report window w Chrome **Zmiany w Memory klasie:** - **Dodano**: isReady flag i readyPromise dla async inicjalizacji (Chrome) - **Dodano**: waitUntilReady() - popup musi poczekać na załadowanie danych - **Dodano**: tabUrls: Map - tracking pełnych URL dla Chrome (service worker nie dostaje documentUrl) - **Dodano**: scheduleSyncToStorage() - throttled sync do storage - **Dodano**: syncToStorage() - serializacja clustrów do JSON - **Dodano**: loadFromStorage() - deserializacja przy starcie popup - **Zmieniono**: register() śledzi main_frame URL i synuje po każdej zmianie - **Zmieniono**: Badge operacje w try-catch (karta może być zamknięta) **Funkcja getMemory():** - **Firefox**: browserAPI.extension.getBackgroundPage().memory (tradycyjnie) - **Chrome Service Worker**: self.memory (jesteśmy W service workerze) - **Chrome Popup**: Tworzy NOWĄ instancję czytając z storage (cachowana jako popupMemoryInstance) --- ## 🔒 Bezpieczeństwo i Obsługa Błędów ### util.ts - getshorthost() - **Dodano**: Walidacja wejścia (null, undefined, pusty string) - **Dodano**: Czyszczenie URL (protokół, ścieżka, port) - **Dodano**: Obsługa edge cases (localhost, single word domains) - **Dodano**: Bezpieczne fallbacki zamiast crashowania - **Dodano**: Console.warn zamiast milczących błędów ### extended-request.ts - **MASYWNE POPRAWKI** parsowania URL w konstruktorze - **Dodano**: isValidHttpUrl() helper - sprawdza czy URL zaczyna się od http(s) - **Dodano**: safeParseUrl() helper - try-catch wokół new URL() - **Dodano**: Próba parsowania wielu URL w kolejności priorytetów - **Dodano**: Obsługa Chrome MV3 initiator property - **Dodano**: Bezpieczne defaulty gdy parsowanie się nie uda (unknown://unknown) - **Zmieniono**: isThirdParty() pomija requesty z unparseable URLs - **Dodano**: uint8ArrayToString() - chunked konwersja dużych arrayów (zapobiega stack overflow) - **Zmieniono**: Request body processing używa chunked konwersji --- ## 🎨 UI Components ### toolbar.tsx (popup) - **Dodano**: getCurrentTab() z retry mechanism (Chrome czasem nie zwraca karty od razu) - **Dodano**: Sprawdzanie memoryReady przed renderowaniem danych - **Dodano**: Wywołanie waitUntilReady() w useEffect - **Dodano**: Opóźnienie 200ms dla Chrome przy inicjalizacji (service worker + storage delay) - **Dodano**: Graceful handling gdy popup otwarty bez active tab ### sidebar.tsx - **Dodano**: Stan memoryReady i loading screen dla Chrome - **Dodano**: Wywołanie waitUntilReady() przed dostępem do danych - **Dodano**: Conditional rendering - pokazuje Ładowanie... gdy pamięć nie gotowa ### report-window.tsx - **Dodano**: Stan memoryReady i loading message - **Dodano**: Wywołanie waitUntilReady() przed generowaniem raportu - **Dodano**: Fallback konstruowania URL z origin gdy brak visited_url - **Zmieniono**: Filtr clustrów używa hasMarks() zamiast getMarkedRequests().length ### stolen-data-cluster.tsx - **Bez znaczących zmian** - działa z abstrakcją RequestCluster --- ## 🐛 Poprawki Bugów ### background.ts - **Dodano**: Diagnostic logging do debugowania inicjalizacji - **Dodano**: Try-catch wokół init() z error logging - **Dodano**: Różne logi dla Firefox vs Chrome ### memory.ts - badge operations - **Dodano**: Try-catch wokół wszystkich operacji badge - Zapobiega crashowaniu gdy użytkownik zamknie kartę podczas operacji ### chrome.ts - badge adapter - **Dodano**: Try-catch w setBadgeText, setTitle, setBadgeBackgroundColor - Chrome rzuca błędy gdy operujemy na zamkniętych kartach --- ## 📝 Workflow Użytkownika (Chrome) 1. **Użytkownik odwiedza stronę** → Service worker rejestruje requesty → Auto-mark podejrzanych → Sync do storage 2. **Użytkownik otwiera popup** → Tworzy Memory → Czyta ze storage → Pokazuje dane z flagami 3. **Użytkownik (od)zaznacza domeny** → Zmienia flagi → Sync do storage 4. **Użytkownik generuje raport** → Otwiera report-window → Czyta ze storage → Filtruje według hasMarks() --- ## ✅ Rezultat - ✅ **Firefox**: Działa jak wcześniej (background page + window.memory) - ✅ **Chrome**: Pełne wsparcie MV3 (service worker + storage.session) - ✅ **Wspólny kod**: 95% kodu jest shared, tylko warstwa dostępu do API różni się - ✅ **Bezpieczeństwo**: Obsługa wszystkich edge cases w parsowaniu URL - ✅ **Wydajność**: Throttled sync do storage (max co 500ms) - ✅ **UX**: Loading states w popup/sidebar dla Chrome - ✅ **Build**: npm run build-addon:firefox lub npm build-addon dla firefox / npm run build:chrome dla chrome --- background.ts | 19 +- components/report-window/report-window.tsx | 79 ++- components/sidebar/sidebar.tsx | 47 +- components/sidebar/stolen-data-cluster.tsx | 2 + components/toolbar/toolbar.html | 4 + components/toolbar/toolbar.tsx | 127 +++- esbuild.config.js | 247 +++++-- extended-request.ts | 161 ++++- lib/browser-api/chrome.ts | 47 +- lib/browser-api/firefox.ts | 93 ++- lib/browser-api/index.ts | 19 +- lib/browser-api/types.ts | 57 +- manifest-chrome.json | 35 + memory.ts | 354 +++++++++- package-lock.json | 775 +++++++++++++++++++++ package.json | 8 +- scripts/convert-icons.js | 32 + util.ts | 45 +- 18 files changed, 1887 insertions(+), 264 deletions(-) create mode 100644 manifest-chrome.json create mode 100644 scripts/convert-icons.js diff --git a/background.ts b/background.ts index 4e2e800..7529424 100644 --- a/background.ts +++ b/background.ts @@ -1,3 +1,18 @@ -import { init } from "./memory"; +import { init } from './memory'; -init(); +console.log('🔴 [DIAGNOSTYKA] Wczytywanie background.ts, TARGET =', process.env.TARGET); + +// Inicjalizacja pamięci storage +try { + init(); + console.log('✅ init() zakończone pomyślnie'); +} catch (error) { + console.error('❌ init() nie powiodło się:', error); +} + +// Log zakończenia inicjalizacji +if (process.env.TARGET === 'chrome') { + console.log('🔵 Service worker Chrome Rentgen zainicjalizowany (używa chrome.storage.session)'); +} else { + console.log('🦊 Strona tła Firefox Rentgen zainicjalizowana'); +} \ No newline at end of file diff --git a/components/report-window/report-window.tsx b/components/report-window/report-window.tsx index 5dc58e8..d870c76 100644 --- a/components/report-window/report-window.tsx +++ b/components/report-window/report-window.tsx @@ -24,6 +24,24 @@ function Report() { if (!origin) { return
Błąd: brak parametru "origin"
; } + + // Oczekiwanie na gotowość pamięci Chrome + const [memoryReady, setMemoryReady] = React.useState(process.env.TARGET !== 'chrome'); + + React.useEffect(() => { + if (process.env.TARGET === 'chrome') { + const memory = getMemory(); + if (typeof (memory as any).waitUntilReady === 'function') { + (memory as any).waitUntilReady().then(() => { + setMemoryReady(true); + console.log('✅ Memory gotowa dla okna raportu'); + }); + } else { + setMemoryReady(true); + } + } + }, []); + const [counter] = useEmitter(getMemory()); const rawAnswers = url.searchParams.get('answers'); const [answers, setAnswers] = React.useState( @@ -32,22 +50,32 @@ function Report() { const [mode, setMode] = React.useState(url.searchParams.get('mode') || 'survey'); const [scrRequestPath, setScrRequestPath] = React.useState(''); - const clusters = getMemory().getClustersForOrigin(origin || ''); + // Pobieranie klastrów tylko gdy pamięć jest gotowa + const clusters = memoryReady ? getMemory().getClustersForOrigin(origin || '') : {}; React.useEffect(() => { - if (!origin) return; + if (!origin || !memoryReady) return; const url = new URL(document.location.toString()); url.searchParams.set('origin', origin); url.searchParams.set('answers', JSON.stringify(answers)); url.searchParams.set('mode', mode); history.pushState({}, 'Rentgen', url.toString()); - }, [mode, answers, origin]); + }, [mode, answers, origin, memoryReady]); + + // Wyświetlanie wczytywania w trakcie oczekiwania na pamięć + if (!memoryReady) { + return
Wczytywanie danych z rozszerzenia...
; + } + const visited_url = Object.values(clusters) .sort((a, b) => (a.lastModified > b.lastModified ? -1 : 1)) .find((cluster) => !!cluster.lastFullUrl)?.lastFullUrl; - if (!visited_url) { - return
Wczytywanie...
; + // Jeśli nie znaleziono visited_url, próba skonstruowania z origin + const finalVisitedUrl = visited_url || origin; + + if (!finalVisitedUrl) { + return
Błąd: nie można znaleźć adresu strony
; } const result = ( @@ -55,7 +83,7 @@ function Report() { {mode === 'survey' ? ( cluster.getMarkedRequests().length > 0 + (cluster) => cluster.hasMarks() )} onComplete={(answers) => { setAnswers(parseAnswers(answers)); @@ -68,11 +96,11 @@ function Report() { {mode === 'screenshots' ? ( @@ -83,10 +111,10 @@ function Report() { @@ -95,28 +123,19 @@ function Report() { )} ); - return ( - -
- -
- {origin ? ( - <> - Generowanie raportu - {origin} - - ) : ( - Przejdź do wybranej strony internetowej - )} -
-
-
{result}
-
- ); + return result; } catch (e) { console.error(e); - return
ERROR! {JSON.stringify(e)}
; + return ( +
+

+ Wystąpił błąd +

+

Najprawdopodobniej Rentgen napotkał stronę, której nie jest w stanie obsłużyć.

+

{(e as Error).toString()}

+
+ ); } } -ReactDOM.render(, document.getElementById('app')); +ReactDOM.render(, document.getElementById('app')); \ No newline at end of file diff --git a/components/sidebar/sidebar.tsx b/components/sidebar/sidebar.tsx index 6fdd0de..d4709ff 100644 --- a/components/sidebar/sidebar.tsx +++ b/components/sidebar/sidebar.tsx @@ -12,6 +12,8 @@ const Sidebar = () => { const url = new URL(document.location.toString()); const origin = url.searchParams.get('origin'); + // Chrome: Oczekiwanie na gotowość pamięci + const [memoryReady, setMemoryReady] = React.useState(process.env.TARGET !== 'chrome'); const [minValueLength, setMinValueLength] = React.useState( localStorage.getItem('minValueLength') === null ? 7 @@ -44,8 +46,24 @@ const Sidebar = () => { : false ); + // Oczekiwanie na gotowość pamięci Chrome React.useEffect(() => { - if (!origin) return; + if (process.env.TARGET === 'chrome') { + const memory = getMemory(); + if (typeof (memory as any).waitUntilReady === 'function') { + (memory as any).waitUntilReady().then(() => { + setMemoryReady(true); + console.log('✅ Memory gotowa dla sidebara'); + }); + } else { + setMemoryReady(true); + } + } + }, []); + + React.useEffect(() => { + if (!origin || !memoryReady) return; + for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) { if (cluster.hasMarks()) { return setMarksOccurrence(true); @@ -53,9 +71,24 @@ const Sidebar = () => { } return setMarksOccurrence(false); - }, [eventCounts['*']]); + }, [eventCounts['*'], memoryReady]); if (!origin) return
Błąd: Brak parametru "origin"
; + + // Wyświetlanie stanu wczytywania dla Chrome + if (!memoryReady) { + return ( +
+
+ +
+ Ładowanie danych... +
+
+
+ ); + } + return (
@@ -121,12 +154,12 @@ const Sidebar = () => { {localStorage.getItem('blottingBrowser') === - 'nikttakniesplamitwojejprzeglądarkijakspidersweb' ? ( + 'nikttakniesplamitwojejprzeglądarki jakspidersweb' ? (