## 🎯 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<number, string> - 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
201 lines
7.1 KiB
TypeScript
201 lines
7.1 KiB
TypeScript
import React from 'react';
|
|
import { getMemory } from '../../memory';
|
|
import { StolenDataEntry } from '../../stolen-data-entry';
|
|
|
|
import { useEmitter } from '../../util';
|
|
|
|
import './stolen-data-cluster.scss';
|
|
|
|
function StolenDataValue({ entry }: { entry: StolenDataEntry; prefixKey?: string }) {
|
|
const [version] = useEmitter(entry);
|
|
let body = null;
|
|
if (!entry.value) {
|
|
body = <></>;
|
|
} else {
|
|
body = <div data-version={version}>{entry.value}</div>;
|
|
}
|
|
return (
|
|
<td
|
|
className="value"
|
|
onClick={(e) => {
|
|
entry.toggleMark();
|
|
getMemory().emit('change', entry.request.shorthost);
|
|
e.stopPropagation();
|
|
}}
|
|
title={entry.value}
|
|
>
|
|
{body}
|
|
</td>
|
|
);
|
|
}
|
|
|
|
function StolenDataRow({ entry }: { entry: StolenDataEntry }) {
|
|
const [version] = useEmitter(entry);
|
|
return (
|
|
<tr
|
|
data-key={entry.id}
|
|
data-version={version}
|
|
className={`${entry.isMarked ? 'toggled' : 'untoggled'}`}
|
|
>
|
|
<td className="checkbox">
|
|
<input
|
|
type="checkbox"
|
|
checked={entry.isMarked}
|
|
id={entry.id.toString()}
|
|
onChange={() => {
|
|
entry.toggleMark();
|
|
getMemory().emit('change', entry.request.shorthost);
|
|
}}
|
|
/>
|
|
</td>
|
|
<th title={`Nazwa: ${entry.name}\nŹródło: ${entry.source}`}>
|
|
<label htmlFor={entry.id.toString()}>{entry.name}</label>
|
|
</th>
|
|
<td className="icons">
|
|
{entry.source === 'cookie' ? (
|
|
<span title="Dane przechowywane w Cookies">
|
|
<img
|
|
src="/assets/icons/cookie.svg"
|
|
height={16}
|
|
width={16}
|
|
className="cookie-data"
|
|
/>
|
|
</span>
|
|
) : entry.request.hasCookie() ? (
|
|
<span title="Wysłane w zapytaniu opatrzonym Cookies" style={{ opacity: 0.25 }}>
|
|
<img
|
|
src="/assets/icons/cookie.svg"
|
|
height={16}
|
|
width={16}
|
|
className="request-with-cookie"
|
|
/>
|
|
</span>
|
|
) : null}
|
|
{entry.exposesOrigin() ? (
|
|
<span title="Pokazuje część historii przeglądania">
|
|
<img
|
|
src="/assets/icons/warning.svg"
|
|
height={16}
|
|
width={16}
|
|
className="show-history-part"
|
|
/>
|
|
</span>
|
|
) : entry.request.exposesOrigin() ? (
|
|
<span
|
|
title="Jest częścią zapytania, które ujawnia historię przeglądania"
|
|
style={{ opacity: 0.25 }}
|
|
>
|
|
<img
|
|
src="/assets/icons/warning.svg"
|
|
height={16}
|
|
width={16}
|
|
className="request-with-history-part"
|
|
/>
|
|
</span>
|
|
) : null}
|
|
</td>
|
|
<StolenDataValue entry={entry} />
|
|
</tr>
|
|
);
|
|
}
|
|
|
|
export default function StolenDataCluster({
|
|
origin,
|
|
shorthost,
|
|
refreshToken,
|
|
minValueLength,
|
|
cookiesOnly,
|
|
cookiesOrOriginOnly,
|
|
detailsVisibility,
|
|
}: {
|
|
origin: string;
|
|
shorthost: string;
|
|
refreshToken: number;
|
|
minValueLength: number;
|
|
cookiesOnly: boolean;
|
|
cookiesOrOriginOnly: boolean;
|
|
detailsVisibility: boolean;
|
|
}) {
|
|
const cluster = getMemory().getClustersForOrigin(origin)[shorthost];
|
|
const fullHosts = cluster.getFullHosts();
|
|
const [version] = useEmitter(cluster);
|
|
|
|
return (
|
|
<div className="stolen-data-cluster-container">
|
|
<header className="domains-container">
|
|
<div className="domains-container__header">
|
|
<input
|
|
type="checkbox"
|
|
className="domain-checkbox"
|
|
data-version={version}
|
|
checked={cluster.hasMarks()}
|
|
onChange={() => {
|
|
console.log('Clicked checkbox!', {
|
|
cluster_id: cluster.id,
|
|
has_marks: cluster.hasMarks(),
|
|
});
|
|
cluster.hasMarks() ? cluster.undoMark() : cluster.autoMark();
|
|
getMemory().emit('change', cluster.id);
|
|
}}
|
|
/>
|
|
<a className="domain" href={'https://' + cluster.id} target="_blank">
|
|
{cluster.id}
|
|
</a>{' '}
|
|
{cluster.hasCookies() ? (
|
|
<img
|
|
src="/assets/icons/cookie.svg"
|
|
height={16}
|
|
width={16}
|
|
className="icon cookie-data"
|
|
/>
|
|
) : (
|
|
''
|
|
)}
|
|
</div>
|
|
<div className="subdomains-container">
|
|
{fullHosts.map((host, index) => (
|
|
<a
|
|
className="subdomain"
|
|
key={host}
|
|
href={`https://${host}`}
|
|
target="_blank"
|
|
>
|
|
{host} {`${fullHosts.length - 1 !== index ? '· ' : ''}`}
|
|
</a>
|
|
))}
|
|
</div>
|
|
</header>
|
|
|
|
{detailsVisibility ? (
|
|
<section>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th className="table-header" colSpan={4}>
|
|
Wysłane dane:
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{cluster
|
|
.calculateRepresentativeStolenData({
|
|
minValueLength,
|
|
cookiesOnly,
|
|
cookiesOrOriginOnly,
|
|
})
|
|
.map((entry) => (
|
|
<StolenDataRow
|
|
{...{
|
|
entry,
|
|
key: entry.id,
|
|
}}
|
|
/>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|