1
0
forked from icd/rentgen

Compare commits

...

2 Commits

Author SHA1 Message Date
am0
ffb37f9d61 feat/refactor: Chrome MV3 Support + Refaktoryzacja architektury dla kompatybilności obu przeglądarek
## 🎯 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
2025-10-18 13:08:14 +02:00
am0
3512386b2b refactor: migracja wywołań API przeglądarki do abstrakcji browserAPI
Zmigrowano wszystkie bezpośrednie wywołania browser.* API do zunifikowanej abstrakcji browserAPI, umożliwiając budowanie rozszerzenia zarówno dla Firefox (browser.browserAction) jak i Chrome (chrome.action) z jednego kodu źródłowego.

## Zmigrowane pliki aplikacji (4):

### 1. memory.ts
Dodano import:
- import browserAPI from ./lib/browser-api

Zastąpiono wywołania API:
- browser.browserAction.setBadgeText → browserAPI.badge.setBadgeText
- browser.browserAction.setTitle → browserAPI.badge.setTitle
- browser.browserAction.setBadgeBackgroundColor → browserAPI.badge.setBadgeBackgroundColor
- browser.webRequest.onBeforeRequest.addListener → browserAPI.webRequest.onBeforeRequest.addListener
- browser.webRequest.onBeforeSendHeaders.addListener → browserAPI.webRequest.onBeforeSendHeaders.addListener
- browser.cookies.getAll → browserAPI.cookies.getAll
- browser.cookies.remove → browserAPI.cookies.remove
- browser.extension.getBackgroundPage() → browserAPI.extension.getBackgroundPage()

Dodano obsługę null:
- Funkcja getMemory() sprawdza teraz czy getBackgroundPage() nie zwraca null

### 2. components/toolbar/toolbar.tsx
Dodano import:
- import browserAPI from ../../lib/browser-api

Zastąpiono wywołania API:
- browser.tabs.query → browserAPI.tabs.query
- browser.windows.WINDOW_ID_CURRENT → browserAPI.windows.WINDOW_ID_CURRENT
- browser.tabs.onUpdated.addListener → browserAPI.tabs.onUpdated.addListener
- browser.tabs.onUpdated.removeListener → browserAPI.tabs.onUpdated.removeListener

Zachowano całą funkcjonalność:
- Wszystkie sekcje UI (header, summary z licznikami, details, about, actions)
- Wszystkie hooki React i logika biznesowa
- Funkcje pomocnicze (getCurrentTab, isDomainHighlySuspicious, autoMark)

### 3. components/tab-dropdown.tsx
Zmieniono importy:
- Usunięto: import { Tab } from ../../util
- Dodano: import browserAPI, { Tab } from ../../lib/browser-api

Zastąpiono wywołania API:
- browser.tabs.query({ currentWindow: true }) → browserAPI.tabs.query({ currentWindow: true })

Poprawka typów:
- Typ Tab teraz pochodzi z browserAPI, zapewniając zgodność typów

### 4. util.ts
Dodano import:
- import browserAPI from ./lib/browser-api

Zastąpiono wywołania API:
- Typ Tab pochodzi teraz z browserAPI.tabs.query zamiast browser.tabs.query
- browser.tabs.query({ currentWindow: true }) → browserAPI.tabs.query({ currentWindow: true }) w funkcji getTabByID

Zachowano wszystkie funkcje:
- getshorthost, useEmitter, parseCookie, getTabByID
- parseToObject, isJSONObject, isURL, hyphenate, unique
- allSubhosts, reduceConcat, getDate, toBase64, makeThrottle
- isSameURL, isBase64, isBase64JSON
- flattenObject, flattenObjectEntries
- maskString, safeDecodeURIComponent, normalizeForClassname
- wordlist, dataLocationToText, downloadText

## Rozszerzenie abstrakcji browserAPI:

### lib/browser-api/types.ts
Dlaczego rozszerzono:
Początkowy minimalny interfejs RequestDetails był niewystarczający, ponieważ brakowało kluczowych właściwości wymaganych przez konstruktor ExtendedRequest. Gdy listenery webRequest są wywoływane, przekazują kompletny obiekt Request do ExtendedRequest, a nie tylko podstawowe szczegóły.

Co zostało dodane:
Pełny typ Request z util.ts, zawierający:
- Właściwości główne: requestId, tabId, url, method, type
- Nawigacja ramek: frameId, parentFrameId, documentUrl, originUrl
- Opcjonalne metadane: cookieStoreId, incognito, thirdParty, timeStamp
- Szczegóły żądania: requestHeaders, urlClassification, proxyInfo

Przyczyna źródłowa błędów TypeScript:
Konstruktor ExtendedRequest oczekiwał właściwości takich jak frameId, method,
originUrl, parentFrameId, documentUrl, urlClassification, etc. Minimalny
interfejs powodował błędy:
- Argument of type RequestDetails is not assignable to parameter of type Request
- Type RequestDetails is missing properties: frameId, method, originUrl, parentFrameId, and 4 more

Rozwiązanie:
Używając pełnej definicji typu Request, abstrakcja browserAPI poprawnie typuje callbacki webRequest, zapewniając bezpieczeństwo typów zarówno dla buildu Firefox jak i Chrome, przy zachowaniu kompatybilności z istniejącą implementacją ExtendedRequest.

Zmiana w RequestListener:
- Było: (details: RequestDetails) => void
- Jest: (details: Request) => void

## Wpływ zmian:
- memory.ts, toolbar.tsx, tab-dropdown.tsx, util.ts działają z TARGET=firefox i TARGET=chrome
- Zachowano bezpieczeństwo typów w całym kodzie
- Brak zmian funkcjonalnych - tylko warstwa abstrakcji
- Gotowość do kompatybilności z Chrome Manifest V3 (chrome.action vs browser.browserAction)

## Następne kroki:
- Aktualizacja esbuild.config.js dla budowania z TARGET=chrome do dist-chrome/
- Aktualizacja manifestu Chrome zgodnie z regułami Manifest v3
- Skrypt konwertujący SVG na PNG dla Chrome
- Testowanie rozszerzenia w przeglądarce Chrome
2025-09-30 12:17:18 +02:00
19 changed files with 1971 additions and 341 deletions

View File

@ -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');
}

View File

@ -24,6 +24,24 @@ function Report() {
if (!origin) { if (!origin) {
return <div>Błąd: brak parametru "origin"</div>; return <div>Błąd: brak parametru "origin"</div>;
} }
// 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 [counter] = useEmitter(getMemory());
const rawAnswers = url.searchParams.get('answers'); const rawAnswers = url.searchParams.get('answers');
const [answers, setAnswers] = React.useState<ParsedAnswers>( const [answers, setAnswers] = React.useState<ParsedAnswers>(
@ -32,22 +50,32 @@ function Report() {
const [mode, setMode] = React.useState(url.searchParams.get('mode') || 'survey'); const [mode, setMode] = React.useState(url.searchParams.get('mode') || 'survey');
const [scrRequestPath, setScrRequestPath] = React.useState(''); 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(() => { React.useEffect(() => {
if (!origin) return; if (!origin || !memoryReady) return;
const url = new URL(document.location.toString()); const url = new URL(document.location.toString());
url.searchParams.set('origin', origin); url.searchParams.set('origin', origin);
url.searchParams.set('answers', JSON.stringify(answers)); url.searchParams.set('answers', JSON.stringify(answers));
url.searchParams.set('mode', mode); url.searchParams.set('mode', mode);
history.pushState({}, 'Rentgen', url.toString()); history.pushState({}, 'Rentgen', url.toString());
}, [mode, answers, origin]); }, [mode, answers, origin, memoryReady]);
// Wyświetlanie wczytywania w trakcie oczekiwania na pamięć
if (!memoryReady) {
return <div>Wczytywanie danych z rozszerzenia...</div>;
}
const visited_url = Object.values(clusters) const visited_url = Object.values(clusters)
.sort((a, b) => (a.lastModified > b.lastModified ? -1 : 1)) .sort((a, b) => (a.lastModified > b.lastModified ? -1 : 1))
.find((cluster) => !!cluster.lastFullUrl)?.lastFullUrl; .find((cluster) => !!cluster.lastFullUrl)?.lastFullUrl;
if (!visited_url) { // Jeśli nie znaleziono visited_url, próba skonstruowania z origin
return <div>Wczytywanie...</div>; const finalVisitedUrl = visited_url || origin;
if (!finalVisitedUrl) {
return <div>Błąd: nie można znaleźć adresu strony</div>;
} }
const result = ( const result = (
@ -55,7 +83,7 @@ function Report() {
{mode === 'survey' ? ( {mode === 'survey' ? (
<Questions <Questions
clusters={Object.values(clusters).filter( clusters={Object.values(clusters).filter(
(cluster) => cluster.getMarkedRequests().length > 0 (cluster) => cluster.hasMarks()
)} )}
onComplete={(answers) => { onComplete={(answers) => {
setAnswers(parseAnswers(answers)); setAnswers(parseAnswers(answers));
@ -68,11 +96,11 @@ function Report() {
{mode === 'screenshots' ? ( {mode === 'screenshots' ? (
<ScreenshotGenerator <ScreenshotGenerator
{...{ {...{
visited_url, visited_url: finalVisitedUrl,
clusters, clusters,
setReportWindowMode: setMode, setReportWindowMode: setMode,
setRequestPath: setScrRequestPath, setRequestPath: setScrRequestPath,
downloadFiles: downloadFiles, downloadFiles,
user_role: answers.user_role, user_role: answers.user_role,
}} }}
/> />
@ -83,10 +111,10 @@ function Report() {
<EmailContent <EmailContent
{...{ {...{
answers, answers,
visited_url, visited_url: finalVisitedUrl,
clusters, clusters,
scrRequestPath, scrRequestPath,
downloadFiles: downloadFiles, downloadFiles,
user_role: answers.user_role, user_role: answers.user_role,
}} }}
/> />
@ -95,28 +123,19 @@ function Report() {
)} )}
</div> </div>
); );
return ( return result;
<Fragment>
<header className="header">
<img src="../../assets/icon-addon.svg" height={32}></img>
<div className="webpage-metadata">
{origin ? (
<>
<span>Generowanie raportu </span>
<span className="webpage-metadata--hyperlink">{origin}</span>
</>
) : (
<span>Przejdź do wybranej strony internetowej</span>
)}
</div>
</header>
<section id="main-section">{result}</section>
</Fragment>
);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return <div>ERROR! {JSON.stringify(e)}</div>; return (
<div style={{ padding: 30 }}>
<p>
<strong>Wystąpił błąd</strong>
</p>
<p>Najprawdopodobniej Rentgen napotkał stronę, której nie jest w stanie obsłużyć.</p>
<p>{(e as Error).toString()}</p>
</div>
);
} }
} }
ReactDOM.render(<Report />, document.getElementById('app')); ReactDOM.render(<Report />, document.getElementById('app'));

View File

@ -12,6 +12,8 @@ const Sidebar = () => {
const url = new URL(document.location.toString()); const url = new URL(document.location.toString());
const origin = url.searchParams.get('origin'); 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<number | null>( const [minValueLength, setMinValueLength] = React.useState<number | null>(
localStorage.getItem('minValueLength') === null localStorage.getItem('minValueLength') === null
? 7 ? 7
@ -44,8 +46,24 @@ const Sidebar = () => {
: false : false
); );
// Oczekiwanie na gotowość pamięci Chrome
React.useEffect(() => { 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))) { for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) {
if (cluster.hasMarks()) { if (cluster.hasMarks()) {
return setMarksOccurrence(true); return setMarksOccurrence(true);
@ -53,9 +71,24 @@ const Sidebar = () => {
} }
return setMarksOccurrence(false); return setMarksOccurrence(false);
}, [eventCounts['*']]); }, [eventCounts['*'], memoryReady]);
if (!origin) return <div>Błąd: Brak parametru "origin"</div>; if (!origin) return <div>Błąd: Brak parametru "origin"</div>;
// Wyświetlanie stanu wczytywania dla Chrome
if (!memoryReady) {
return (
<div className="sidebar">
<header className="header">
<img src="../../assets/icon-addon.svg" height={32}></img>
<div className="webpage-metadata">
<span>Ładowanie danych...</span>
</div>
</header>
</div>
);
}
return ( return (
<div className="sidebar"> <div className="sidebar">
<header className="header"> <header className="header">
@ -121,12 +154,12 @@ const Sidebar = () => {
</button> </button>
{localStorage.getItem('blottingBrowser') === {localStorage.getItem('blottingBrowser') ===
'nikttakniesplamitwojejprzeglądarkijakspidersweb' ? ( 'nikttakniesplamitwojejprzeglądarki jakspidersweb' ? (
<button <button
onClick={() => { onClick={() => {
if ( if (
window.confirm( window.confirm(
'Czy chcesz wczytać wszystkie domeny w celu „splamienia” twojej przeglądarki? Uwaga przeglądarka może zablokować otwieranie nowych kart. (Ten krok jest opcjonalny)' 'Czy chcesz wczytać wszystkie domeny w celu „splamienia" twojej przeglądarki? Uwaga przeglądarka może zablokować otwieranie nowych kart. (Ten krok jest opcjonalny)'
) )
) { ) {
let deep_copy = JSON.parse( let deep_copy = JSON.parse(
@ -178,8 +211,8 @@ const Sidebar = () => {
<section className="dialog-container dialog-container--warning"> <section className="dialog-container dialog-container--warning">
<span> <span>
<strong>Uwaga!</strong> Niekoniecznie każda przesłana poniżej <strong>Uwaga!</strong> Niekoniecznie każda przesłana poniżej
informacja jest daną osobową. Niektóre z podanych domen mogą informacja jest daną osobową. Niektóre z podanych domen mogą
należeć do właściciela strony i nie reprezentować podmiotów należeć do właściciela strony i nie reprezentować podmiotów
trzecich. trzecich.
</span> </span>
<button <button
@ -234,4 +267,4 @@ const Sidebar = () => {
); );
}; };
ReactDOM.render(<Sidebar />, document.getElementById('app')); ReactDOM.render(<Sidebar />, document.getElementById('app'));

View File

@ -102,6 +102,7 @@ function StolenDataRow({ entry }: { entry: StolenDataEntry }) {
export default function StolenDataCluster({ export default function StolenDataCluster({
origin, origin,
shorthost, shorthost,
refreshToken,
minValueLength, minValueLength,
cookiesOnly, cookiesOnly,
cookiesOrOriginOnly, cookiesOrOriginOnly,
@ -109,6 +110,7 @@ export default function StolenDataCluster({
}: { }: {
origin: string; origin: string;
shorthost: string; shorthost: string;
refreshToken: number;
minValueLength: number; minValueLength: number;
cookiesOnly: boolean; cookiesOnly: boolean;
cookiesOrOriginOnly: boolean; cookiesOrOriginOnly: boolean;

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Tab } from '../../util'; import browserAPI, { Tab } from '../../lib/browser-api';
export default function TabDropdown({ export default function TabDropdown({
setPickedTab, setPickedTab,
@ -10,7 +10,7 @@ export default function TabDropdown({
}) { }) {
const [tabs, setTabs] = React.useState<Tab[]>([]); const [tabs, setTabs] = React.useState<Tab[]>([]);
React.useEffect(() => { React.useEffect(() => {
browser.tabs.query({ currentWindow: true }).then(setTabs); browserAPI.tabs.query({ currentWindow: true }).then(setTabs);
}, []); }, []);
return ( return (
<select <select
@ -27,4 +27,4 @@ export default function TabDropdown({
))} ))}
</select> </select>
); );
} }

View File

@ -7,6 +7,10 @@
rel="stylesheet" rel="stylesheet"
href="/lib/styles/global.css" href="/lib/styles/global.css"
> >
<link
rel="stylesheet"
href="/lib/styles/fonts.css"
>
<link <link
rel="stylesheet" rel="stylesheet"
href="/lib/components/toolbar/toolbar.css" href="/lib/components/toolbar/toolbar.css"

View File

@ -1,20 +1,46 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { useEmitter } from '../../util';
import { getMemory } from '../../memory'; import { getMemory } from '../../memory';
import { useEmitter, getshorthost } from '../../util';
import browserAPI from '../../lib/browser-api';
async function getCurrentTab() { // Niezawodne pobieranie zakładki z ponawianiem prób
const [tab] = await browser.tabs.query({ async function getCurrentTab(retries = 3, delay = 100): Promise<any> {
active: true, for (let i = 0; i < retries; i++) {
windowId: browser.windows.WINDOW_ID_CURRENT, try {
}); // Metoda 1: Zapytanie o aktywną zakładkę
return tab; const tabs = await browserAPI.tabs.query({
active: true,
currentWindow: true,
});
if (tabs && tabs[0] && tabs[0].url) {
return tabs[0];
}
// Metoda 2: Użycie lastFocusedWindow
const tabsLastFocused = await browserAPI.tabs.query({
active: true,
lastFocusedWindow: true,
});
if (tabsLastFocused && tabsLastFocused[0] && tabsLastFocused[0].url) {
return tabsLastFocused[0];
}
} catch (error) {
console.warn(`Próba ${i + 1} zapytania o zakładkę nie powiodła się:`, error);
}
// Czekanie przed ponowieniem próby
if (i < retries - 1) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
return undefined;
} }
import './../../styles/global.scss'; function isDomainHighlySuspicious(domain: string) {
import './toolbar.scss';
function isDomainHighlySuspicious(domain: string): boolean {
return ( return (
domain.includes('facebook') || domain.includes('facebook') ||
domain.includes('twitter') || domain.includes('twitter') ||
@ -25,51 +51,99 @@ function isDomainHighlySuspicious(domain: string): boolean {
const Toolbar = () => { const Toolbar = () => {
const [origin, setOrigin] = React.useState<string | null>(null); const [origin, setOrigin] = React.useState<string | null>(null);
const [memoryReady, setMemoryReady] = React.useState(process.env.TARGET !== 'chrome');
const [eventCounts] = useEmitter(getMemory()); const [eventCounts] = useEmitter(getMemory());
const [cookieDomainCopy, setCookieDomainCopy] = React.useState<string | null>(null); const [cookieDomainCopy, setCookieDomainCopy] = React.useState<string | null>(null);
const [_, setMarksOccurrence] = React.useState<boolean>(false); const [_, setMarksOccurrence] = React.useState(false);
const [exposedOriginDomainCopy, setExposedOriginDomainCopy] = React.useState<string | null>( const [exposedOriginDomainCopy, setExposedOriginDomainCopy] = React.useState<string | null>(
null null
); );
const first_sentence_cookie = 'Strona dokonała zapisu i odczytu plików Cookie dla domen '; const first_sentence_cookie =
'Strona dokonała zapisu i odczytu plików Cookie dla domen ';
const first_sentence_history = const first_sentence_history =
'Część informacji o Twojej historii przeglądania została wysłana do '; 'Część informacji o Twojej historii przeglądania została wysłana do ';
// Oczekiwanie na gotowość pamięci Chrome
React.useEffect(() => { React.useEffect(() => {
const listener = async () => { if (process.env.TARGET === 'chrome') {
const tab = await getCurrentTab(); const memory = getMemory();
if (typeof (memory as any).waitUntilReady === 'function') {
if (tab !== undefined && tab.url) { (memory as any).waitUntilReady().then(() => {
const url = new URL(tab.url); setMemoryReady(true);
if (url.origin.startsWith('moz-extension')) { console.log('✅ Memory gotowa, popup może wyświetlać dane');
return; });
}
setOrigin(url.origin);
} else { } else {
console.warn('Out of the tab scope'); setMemoryReady(true);
}
}
}, []);
React.useEffect(() => {
let isMounted = true;
const listener = async () => {
if (!isMounted) return;
const tab = await getCurrentTab();
if (!isMounted) return;
if (tab && tab.url) {
try {
const url = new URL(tab.url);
// Pomijanie stron rozszerzenia
if (url.origin.startsWith('moz-extension') ||
url.origin.startsWith('chrome-extension') ||
url.protocol === 'chrome:' ||
url.protocol === 'about:') {
return;
}
setOrigin(url.origin);
} catch (error) {
console.warn('Nie udało się sparsować URL zakładki:', tab.url, error);
}
} else {
// Tylko ostrzeżenie w trybie debug, nie błąd
if (process.env.NODE_ENV === 'development') {
console.debug('Popup otwarty bez kontekstu aktywnej zakładki');
}
} }
}; };
browser.tabs.onUpdated.addListener(listener); browserAPI.tabs.onUpdated.addListener(listener);
listener();
// Początkowe wczytywanie z odpowiednim opóźnieniem
if (process.env.TARGET === 'chrome') {
// Chrome potrzebuje więcej czasu dla service worker + storage
setTimeout(listener, 200);
} else {
// Firefox jest gotowy natychmiast
listener();
}
return () => { return () => {
browser.tabs.onUpdated.removeListener(listener); isMounted = false;
browserAPI.tabs.onUpdated.removeListener(listener);
}; };
}); }, []);
React.useEffect(() => { React.useEffect(() => {
if (!origin) return; if (!origin || !memoryReady) return;
const exposedOriginDomains = Object.values(getMemory().getClustersForOrigin(origin)) const exposedOriginDomains = Object.values(getMemory().getClustersForOrigin(origin))
.filter((cluster) => cluster.exposesOrigin()) .filter((cluster) => cluster.exposesOrigin())
.sort((cluster1, cluster2) => .sort((cluster1, cluster2) =>
isDomainHighlySuspicious(cluster1.id) isDomainHighlySuspicious(cluster1.id)
? -1 ? -1
: isDomainHighlySuspicious(cluster2.id) : isDomainHighlySuspicious(cluster2.id)
? 1 ? 1
: 0 : 0
) )
.map((cluster) => cluster.id); .map((cluster) => cluster.id);
setExposedOriginDomainCopy(''); setExposedOriginDomainCopy('');
switch (exposedOriginDomains.length) { switch (exposedOriginDomains.length) {
@ -96,20 +170,22 @@ const Toolbar = () => {
); );
break; break;
} }
}, [eventCounts['*'], origin]); }, [eventCounts['*'], origin, memoryReady]);
React.useEffect(() => { React.useEffect(() => {
if (!origin) return; if (!origin || !memoryReady) return;
const cookieDomains = Object.values(getMemory().getClustersForOrigin(origin)) const cookieDomains = Object.values(getMemory().getClustersForOrigin(origin))
.filter((cluster) => cluster.hasCookies()) .filter((cluster) => cluster.hasCookies())
.sort((cluster1, cluster2) => .sort((cluster1, cluster2) =>
isDomainHighlySuspicious(cluster1.id) isDomainHighlySuspicious(cluster1.id)
? -1 ? -1
: isDomainHighlySuspicious(cluster2.id) : isDomainHighlySuspicious(cluster2.id)
? 1 ? 1
: 0 : 0
) )
.map((cluster) => cluster.id); .map((cluster) => cluster.id);
setCookieDomainCopy(''); setCookieDomainCopy('');
switch (cookieDomains.length) { switch (cookieDomains.length) {
@ -128,52 +204,45 @@ const Toolbar = () => {
break; break;
default: default:
setCookieDomainCopy( setCookieDomainCopy(
`${cookieDomains[0]}, ${cookieDomains[1]} (i ${ `${cookieDomains[0]}, ${cookieDomains[1]} (i ${
cookieDomains.length - 2 < 2 ? 2 : cookieDomains.length - 2 cookieDomains.length - 2 < 2 ? 2 : cookieDomains.length - 2
} innych).` } innych).`
); );
break; break;
} }
}, [eventCounts['*'], origin]); }, [eventCounts['*'], origin, memoryReady]);
React.useEffect(() => { const autoMark = () => {
if (!origin) return; Object.values(getMemory().getClustersForOrigin(origin || '')).forEach((cluster) =>
for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) { cluster.autoMark()
if (cluster.hasMarks()) { );
return setMarksOccurrence(true); setMarksOccurrence(true);
} };
}
return setMarksOccurrence(false);
}, [eventCounts['*']]);
function autoMark() {
if (!origin) return;
for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) {
cluster.autoMark();
}
return setMarksOccurrence(true);
}
return ( return (
<div className="toolbar"> <div className="toolbar">
<header className={origin ? 'header' : 'header header--no-page'}> <header className={origin ? 'header' : 'header header--no-page'}>
<img src="../../assets/icon-addon.svg" height={32}></img> <img src="../../assets/icon-addon.svg" height="24" />
<div className="webpage-metadata"> <div className="webpage-metadata">
{origin ? ( {origin ? (
<> <div className="webpage-metadata--hyperlink">{origin}</div>
<span>Analiza strony</span>
<span className="webpage-metadata--hyperlink">{origin}</span>
</>
) : ( ) : (
<span>Przejdź do wybranej strony internetowej</span> <div>Rentgen - wtyczka do przeglądania</div>
)} )}
</div> </div>
{origin ? ( {origin ? (
<button
onClick={() => {
window.close();
}}
>
<img src="../../assets/icons/x_thick.svg" width="12" height="12" />
</button>
) : (
<a href="https://internet-czas-dzialac.pl"> <a href="https://internet-czas-dzialac.pl">
<img src="/assets/icons/info_circle_outline.svg" width="20" height="20" /> <img src="/assets/icons/info_circle_outline.svg" width="20" height="20" />
</a> </a>
) : null} )}
</header> </header>
{origin ? ( {origin ? (
@ -183,30 +252,22 @@ const Toolbar = () => {
<div className="counters-wrapper"> <div className="counters-wrapper">
<div className="counters"> <div className="counters">
<div className="counter counter--cookies"> <div className="counter counter--cookies">
<img <img src="/assets/icons/cookie.svg#color" width="24" height="24" />
src="/assets/icons/cookie.svg#color"
width="24"
height="24"
/>
<span data-event={`${eventCounts['*']}`}> <span data-event={`${eventCounts['*']}`}>
{ {
Object.values( Object.values(getMemory().getClustersForOrigin(origin)).filter(
getMemory().getClustersForOrigin(origin) (cluster) => cluster.hasCookies()
).filter((cluster) => cluster.hasCookies()).length ).length
} }
</span> </span>
</div> </div>
<div className="counter counter--browser-history"> <div className="counter counter--browser-history">
<img <img src="/assets/icons/warning.svg#color" width="24" height="24" />
src="/assets/icons/warning.svg#color"
width="24"
height="24"
/>
<span data-event={`${eventCounts['*']}`}> <span data-event={`${eventCounts['*']}`}>
{ {
Object.values( Object.values(getMemory().getClustersForOrigin(origin)).filter(
getMemory().getClustersForOrigin(origin) (cluster) => cluster.exposesOrigin()
).filter((cluster) => cluster.exposesOrigin()).length ).length
} }
</span> </span>
</div> </div>
@ -247,9 +308,9 @@ const Toolbar = () => {
<Fragment> <Fragment>
<section className="about"> <section className="about">
<p> <p>
Takie przetwarzanie danych może być niezgodne z prawem. Przejdź Takie przetwarzanie danych może być niezgodne z&nbsp;prawem.
do analizy aby pomóc ustalić, czy ta strona nie narusza RODO lub Przejdź do analizy aby pomóc ustalić, czy ta strona nie narusza
ustawy Prawo Komunikacji Elektronicznej. RODO lub ustawy Prawo Komunikacji Elektronicznej.
</p> </p>
</section> </section>
<section className="actions"> <section className="actions">
@ -261,7 +322,7 @@ const Toolbar = () => {
`/components/sidebar/sidebar.html?origin=${origin}`, `/components/sidebar/sidebar.html?origin=${origin}`,
'new_tab' 'new_tab'
); );
window.close(); // close toolbar popup window.close();
}} }}
> >
Przejdź do analizy Przejdź do analizy
@ -281,4 +342,4 @@ const Toolbar = () => {
); );
}; };
ReactDOM.render(<Toolbar />, document.getElementById('toolbar')); ReactDOM.render(<Toolbar />, document.getElementById('toolbar'));

View File

@ -1,68 +1,179 @@
import esbuild from 'esbuild'; import esbuild from 'esbuild';
import scss from 'esbuild-plugin-sass'; import scss from 'esbuild-plugin-sass';
import { copyFileSync, mkdirSync, readdirSync, existsSync } from 'fs';
const watch = process.argv.includes('--watch') && { import { join, dirname } from 'path';
onRebuild(error) {
if (error) console.error('[watch] build failed', error); // Określenie platformy docelowej: firefox (domyślnie) lub chrome
else console.log('[watch] build finished'); const TARGET = process.env.TARGET || 'firefox';
}, const IS_FIREFOX = TARGET === 'firefox';
}; const IS_CHROME = TARGET === 'chrome';
// see https://github.com/evanw/esbuild/issues/806#issuecomment-779138268 // Katalogi wyjściowe
let skipReactImports = { const DIST_DIR = IS_FIREFOX ? './dist-firefox' : './dist-chrome';
name: 'skipReactImports', const LIB_DIR = join(DIST_DIR, 'lib');
setup(build) {
build.onResolve({ filter: /^(react(-dom)?|survey-react)$/ }, (args) => { console.log(`🎯 Budowanie dla: ${TARGET.toUpperCase()}`);
return { console.log(`📁 Katalog wyjściowy: ${DIST_DIR}`);
path: args.path,
namespace: `globalExternal_${args.path}`, const watch = process.argv.includes('--watch') && {
}; onRebuild(error) {
}); if (error) console.error('[watch] budowanie nie powiodło się', error);
else console.log('[watch] budowanie zakończone');
build.onLoad({ filter: /.*/, namespace: 'globalExternal_react' }, () => { },
return { };
contents: `module.exports = globalThis.React`,
loader: 'js', // Funkcja pomocnicza: rekurencyjne kopiowanie katalogów
}; function copyDir(src, dest) {
}); if (!existsSync(dest)) {
mkdirSync(dest, { recursive: true });
build.onLoad({ filter: /.*/, namespace: 'globalExternal_react-dom' }, () => { }
return {
contents: `module.exports = globalThis.ReactDOM`, const entries = readdirSync(src, { withFileTypes: true });
loader: 'js',
}; for (const entry of entries) {
}); const srcPath = join(src, entry.name);
build.onLoad({ filter: /.*/, namespace: 'globalExternal_survey-react' }, () => { const destPath = join(dest, entry.name);
return {
contents: `module.exports = globalThis.Survey`, if (entry.isDirectory()) {
loader: 'js', copyDir(srcPath, destPath);
}; } else {
}); copyFileSync(srcPath, destPath);
}, }
}; }
}
esbuild
.build({ // Plugin: kopiowanie plików statycznych po zakończeniu budowania
entryPoints: [ const copyStaticFiles = {
'components/toolbar/toolbar.tsx', name: 'copy-static-files',
'components/sidebar/sidebar.tsx', setup(build) {
'components/report-window/report-window.tsx', build.onEnd(() => {
'background.ts', console.log('📋 Kopiowanie plików statycznych...');
'diag.tsx',
'styles/global.scss', // Kopiowanie manifestu (wybór na podstawie platformy docelowej)
'styles/fonts.scss', const manifestSrc = IS_FIREFOX ? './manifest.json' : './manifest-chrome.json';
], const manifestDest = join(DIST_DIR, 'manifest.json');
bundle: true, mkdirSync(dirname(manifestDest), { recursive: true });
// minify: true, copyFileSync(manifestSrc, manifestDest);
outdir: './lib', console.log(` ✓ Skopiowano ${manifestSrc}${manifestDest}`);
loader: { '.woff': 'file', '.woff2': 'file' },
plugins: [scss(), skipReactImports], // Kopiowanie katalogu components
define: { if (existsSync('./components')) {
PLUGIN_NAME: '"Rentgen"', copyDir('./components', join(DIST_DIR, 'components'));
PLUGIN_URL: '"https://addons.mozilla.org/pl/firefox/addon/rentgen/"', console.log(' ✓ Skopiowano components/');
}, }
external: ['react', 'react-dom', 'survey-react'],
watch, // Kopiowanie katalogu assets
}) if (existsSync('./assets')) {
.then(() => console.log('Add-on was built')) copyDir('./assets', join(DIST_DIR, 'assets'));
.catch(() => process.exit(1)); console.log(' ✓ Skopiowano assets/');
}
// Kopiowanie wymaganych bibliotek z node_modules (potrzebne dla plików HTML z UMD React)
const nodeModulesDest = join(DIST_DIR, 'node_modules');
// React
const reactUmdSrc = './node_modules/react/umd';
const reactUmdDest = join(nodeModulesDest, 'react/umd');
if (existsSync(reactUmdSrc)) {
copyDir(reactUmdSrc, reactUmdDest);
console.log(' ✓ Skopiowano node_modules/react/umd/');
}
// React-DOM
const reactDomUmdSrc = './node_modules/react-dom/umd';
const reactDomUmdDest = join(nodeModulesDest, 'react-dom/umd');
if (existsSync(reactDomUmdSrc)) {
copyDir(reactDomUmdSrc, reactDomUmdDest);
console.log(' ✓ Skopiowano node_modules/react-dom/umd/');
}
// Survey-React
const surveyReactSrc = './node_modules/survey-react';
const surveyReactDest = join(nodeModulesDest, 'survey-react');
if (existsSync(surveyReactSrc)) {
// Kopiowanie tylko niezbędnych plików
mkdirSync(surveyReactDest, { recursive: true });
const surveyFiles = ['survey.react.min.js', 'survey.react.min.css'];
surveyFiles.forEach(file => {
const src = join(surveyReactSrc, file);
if (existsSync(src)) {
copyFileSync(src, join(surveyReactDest, file));
}
});
console.log(' ✓ Skopiowano node_modules/survey-react/');
}
console.log(`✅ Budowanie dla ${TARGET.toUpperCase()} zakończone!`);
});
},
};
// Zobacz: https://github.com/evanw/esbuild/issues/806#issuecomment-779138268
// Plugin pomijający importy React (używamy globalnych obiektów z UMD)
let skipReactImports = {
name: 'skipReactImports',
setup(build) {
build.onResolve({ filter: /^(react(-dom)?|survey-react)$/ }, (args) => {
return {
path: args.path,
namespace: `globalExternal_${args.path}`,
};
});
build.onLoad({ filter: /.*/, namespace: 'globalExternal_react' }, () => {
return {
contents: `module.exports = globalThis.React`,
loader: 'js',
};
});
build.onLoad({ filter: /.*/, namespace: 'globalExternal_react-dom' }, () => {
return {
contents: `module.exports = globalThis.ReactDOM`,
loader: 'js',
};
});
build.onLoad({ filter: /.*/, namespace: 'globalExternal_survey-react' }, () => {
return {
contents: `module.exports = globalThis.Survey`,
loader: 'js',
};
});
},
};
esbuild
.build({
entryPoints: [
// JavaScript/TypeScript
'components/toolbar/toolbar.tsx',
'components/sidebar/sidebar.tsx',
'components/report-window/report-window.tsx',
'background.ts',
'diag.tsx',
// Globalne style
'styles/global.scss',
'styles/fonts.scss',
// Style komponentów (kompilowane osobno)
'components/toolbar/toolbar.scss',
// 'components/sidebar/sidebar.scss',
// 'components/report-window/report-window.scss',
],
bundle: true,
// minify: true,
outdir: LIB_DIR,
loader: { '.woff': 'file', '.woff2': 'file' },
plugins: [scss(), skipReactImports, copyStaticFiles],
define: {
PLUGIN_NAME: '"Rentgen"',
PLUGIN_URL: '"https://addons.mozilla.org/pl/firefox/addon/rentgen/"',
'process.env.TARGET': JSON.stringify(TARGET),
},
external: ['react', 'react-dom', 'survey-react'],
watch,
})
.then(() => console.log(`\n🎉 Dodatek dla ${TARGET.toUpperCase()} zbudowany pomyślnie!\n`))
.catch(() => process.exit(1));

View File

@ -8,6 +8,27 @@ import {
safeDecodeURIComponent, safeDecodeURIComponent,
} from './util'; } from './util';
/**
* Bezpieczna konwersja Uint8Array na string, obsługująca duże tablice
* które mogłyby spowodować stack overflow przy użyciu String.fromCharCode.apply()
*/
function uint8ArrayToString(bytes: Uint8Array): string {
const CHUNK_SIZE = 8192; // Przetwarzanie w kawałkach 8KB aby uniknąć stack overflow
if (bytes.length <= CHUNK_SIZE) {
// Mała tablica - używamy szybkiej metody
return String.fromCharCode.apply(null, Array.from(bytes) as any);
}
// Duża tablica - przetwarzamy w kawałkach
let result = '';
for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
const chunk = bytes.subarray(i, i + CHUNK_SIZE);
result += String.fromCharCode.apply(null, Array.from(chunk) as any);
}
return result;
}
type NameValue = { name: string; value: string }; type NameValue = { name: string; value: string };
export type HAREntry = { export type HAREntry = {
@ -62,7 +83,7 @@ const whitelisted_cookies = [
/^Connection$/, /^Connection$/,
/^Sec-Fetch-.*$/, /^Sec-Fetch-.*$/,
/^Content-Type$/, /^Content-Type$/,
/^Cookie$/, // we're extracting it in getCookie separately anyway /^Cookie$/, // wyodrębniamy to w getCookie() osobno
/^User-Agent$/, /^User-Agent$/,
]; ];
@ -80,8 +101,8 @@ export default class ExtendedRequest {
public origin: string; public origin: string;
public initialized = false; public initialized = false;
public stolenData: StolenDataEntry[] = []; public stolenData: StolenDataEntry[] = [];
public originalURL: string | null = null; // sometimes we can only establish that the given request applied to a certain origin, not a full URL from the address bar - in case of service workers, for example. Hence the null public originalURL: string | null = null; // czasami możemy ustalić tylko origin, a nie pełny URL z paska adresu - np. w przypadku service workerów
public originalPathname: string | null = null; // same as above public originalPathname: string | null = null; // tak samo jak powyżej
public originalHost: string; public originalHost: string;
public requestBody: RequestBody; public requestBody: RequestBody;
@ -91,20 +112,45 @@ export default class ExtendedRequest {
constructor(data: Request) { constructor(data: Request) {
this.tabId = data.tabId; this.tabId = data.tabId;
this.url = data.url; this.url = data.url;
this.shorthost = getshorthost(data.url);
this.requestBody = ((data as any).requestBody as undefined | RequestBody) || {}; this.requestBody = ((data as any).requestBody as undefined | RequestBody) || {};
ExtendedRequest.by_id[data.requestId] = this;
this.data = Object.assign({}, data); this.data = Object.assign({}, data);
(this.data as any).frameAncestors = [ (this.data as any).frameAncestors = [
...((data as any)?.frameAncestors?.map((e: any) => ({ url: e.url })) || []), ...((data as any)?.frameAncestors?.map((e: any) => ({ url: e.url })) || []),
]; // making a copy? ];
// console.log('→→→',(this.data as any).frameAncestors, (data as any).frameAncestors); ExtendedRequest.by_id[data.requestId] = this;
// ========================================
// Parsowanie URL - obsługa różnych przeglądarek
// ========================================
// Funkcja pomocnicza: sprawdzenie czy URL jest poprawnym HTTP(S)
const isValidHttpUrl = (urlString: string): boolean => {
if (!urlString || urlString.length < 8) return false;
const lower = urlString.toLowerCase();
return lower.startsWith('http://') || lower.startsWith('https://');
};
// Funkcja pomocnicza: bezpieczne parsowanie URL
const safeParseUrl = (urlString: string) => {
try {
if (!isValidHttpUrl(urlString)) return null;
const parsed = new URL(urlString);
return {
origin: parsed.origin,
host: parsed.host,
pathname: parsed.pathname
};
} catch (error) {
return null;
}
};
// Określenie URL kandydata z kontekstu żądania
let url: string; let url: string;
let is_full_url = true; let is_full_url = true;
let url_comes_from: string; let url_comes_from: string;
if (this.data.type === 'main_frame') { if (this.data.type === 'main_frame') {
url = this.data.url; url = this.data.url;
url_comes_from = 'main_frame'; url_comes_from = 'main_frame';
@ -112,7 +158,6 @@ export default class ExtendedRequest {
url = this.data.documentUrl; url = this.data.documentUrl;
url_comes_from = 'documentUrl'; url_comes_from = 'documentUrl';
if (this.data.tabId == -1) { if (this.data.tabId == -1) {
//a service worker?
url_comes_from = 'documentUrl (webworker)'; url_comes_from = 'documentUrl (webworker)';
is_full_url = false; is_full_url = false;
} }
@ -122,16 +167,58 @@ export default class ExtendedRequest {
) { ) {
url = (this.data as any).frameAncestors.at(-1).url || ''; url = (this.data as any).frameAncestors.at(-1).url || '';
url_comes_from = 'frameAncestors'; url_comes_from = 'frameAncestors';
} else if (process.env.TARGET === 'chrome' && (this.data as any).initiator) {
// Chrome MV3: Używamy właściwości initiator
url = (this.data as any).initiator;
url_comes_from = 'initiator (Chrome MV3)';
is_full_url = false;
} else { } else {
url = this.data.documentUrl || this.data.originUrl; url = this.data.documentUrl || this.data.originUrl;
url_comes_from = 'last resort'; url_comes_from = 'ostatnia deska ratunku';
} }
this.originalURL = is_full_url ? url : null; // Próba parsowania URLi w kolejności preferencji
this.origin = new URL(url).origin; const urlsToTry = [
url,
this.data.documentUrl,
this.data.originUrl,
this.data.url
].filter(Boolean);
this.originalHost = new URL(url).host; let parsedUrl: { origin: string; host: string; pathname: string } | null = null;
this.originalPathname = is_full_url ? new URL(url).pathname : null;
for (const urlToTry of urlsToTry) {
parsedUrl = safeParseUrl(urlToTry as string);
if (parsedUrl) {
url = urlToTry as string;
break;
}
}
// Ustawienie właściwości z bezpiecznymi wartościami domyślnymi
if (parsedUrl) {
// Pomyślnie sparsowano
this.originalURL = is_full_url ? url : null;
this.origin = parsedUrl.origin;
this.originalHost = parsedUrl.host;
this.originalPathname = is_full_url ? parsedUrl.pathname : null;
// Bezpieczne ustawienie shorthost
try {
this.shorthost = getshorthost(parsedUrl.host);
} catch (error) {
console.warn('Nie udało się uzyskać shorthost:', parsedUrl.host, error);
this.shorthost = parsedUrl.host;
}
} else {
// Nie udało się sparsować - używamy bezpiecznych wartości domyślnych
// Te żądania zostaną odfiltrowane przez isThirdParty() później
this.originalURL = null;
this.origin = 'unknown://unknown';
this.originalHost = 'unknown';
this.originalPathname = '/';
this.shorthost = 'unknown';
}
} }
addHeaders(headers: Request['requestHeaders']) { addHeaders(headers: Request['requestHeaders']) {
@ -145,17 +232,30 @@ export default class ExtendedRequest {
} }
isThirdParty() { isThirdParty() {
const request_url = new URL(this.data.url); // Pomijanie żądań z nieznanym origin (nieparsowalny URL)
if (request_url.host.includes(this.originalHost)) { if (this.origin === 'unknown://unknown' || this.originalHost === 'unknown') {
return false; // Nie śledzimy tych
}
try {
const request_url = new URL(this.data.url);
if (request_url.host.includes(this.originalHost)) {
return false;
}
if (getshorthost(request_url.host) == getshorthost(this.originalHost)) {
return false;
}
return (
request_url.origin != this.origin ||
(this.data as any).urlClassification.thirdParty.length > 0
);
} catch (error) {
// Jeśli nie możemy sparsować URL żądania, nie jest śledzalne
return false; return false;
} }
if (getshorthost(request_url.host) == getshorthost(this.originalHost)) {
return false;
}
return (
request_url.origin != this.origin ||
(this.data as any).urlClassification.thirdParty.length > 0
);
} }
getReferer() { getReferer() {
@ -218,14 +318,19 @@ export default class ExtendedRequest {
]) ])
), ),
}).map(([key, value]) => { }).map(([key, value]) => {
// to handle how ocdn.eu encrypts POST body on https://businessinsider.com.pl/ // Obsługa szyfrowanego POST body (jak na ocdn.eu na businessinsider.com.pl)
if ((Array.isArray(value) && value.length === 1 && !value[0]) || !value) { if ((Array.isArray(value) && value.length === 1 && !value[0]) || !value) {
return ['requestBody', key]; return ['requestBody', key];
} else if (!Array.isArray(value)) { } else if (!Array.isArray(value)) {
return [ // POPRAWKA: Używamy bezpiecznej konwersji w kawałkach zamiast apply()
'raw', try {
String.fromCharCode.apply(null, Array.from(new Uint8Array(value.bytes))), const uint8Array = new Uint8Array(value.bytes);
]; const stringValue = uint8ArrayToString(uint8Array);
return ['raw', stringValue];
} catch (e) {
console.warn('Nie udało się przetworzyć bajtów body żądania:', e);
return ['raw', '[Dane binarne - nie udało się przetworzyć]'];
}
} else { } else {
return [key, value || '']; return [key, value || ''];
} }

View File

@ -14,40 +14,63 @@ export const chromeAPI: BrowserAPI = {
tabs: { tabs: {
query: chrome.tabs.query, query: chrome.tabs.query,
onUpdated: { onUpdated: {
addListener: chrome.tabs.onUpdated.addListener, addListener: chrome.tabs.onUpdated.addListener.bind(chrome.tabs.onUpdated),
removeListener: chrome.tabs.onUpdated.removeListener, removeListener: chrome.tabs.onUpdated.removeListener.bind(chrome.tabs.onUpdated),
}, },
onRemoved: chrome.tabs.onRemoved ? {
addListener: chrome.tabs.onRemoved.addListener.bind(chrome.tabs.onRemoved),
removeListener: chrome.tabs.onRemoved.removeListener.bind(chrome.tabs.onRemoved),
} : undefined,
}, },
// Badge API - Chrome używa action (nie browserAction) // Badge API - Chrome używa action (nie browserAction jak Firefox)
// Owinięte w try-catch aby obsłużyć zamknięte zakładki
badge: { badge: {
setBadgeText: chrome.action.setBadgeText, setBadgeText: (details: any) => {
setTitle: chrome.action.setTitle, try {
setBadgeBackgroundColor: chrome.action.setBadgeBackgroundColor, chrome.action.setBadgeText(details);
} catch (e) {
// Zakładka zamknięta - ignorujemy
}
},
setTitle: (details: any) => {
try {
chrome.action.setTitle(details);
} catch (e) {
// Zakładka zamknięta - ignorujemy
}
},
setBadgeBackgroundColor: (details: any) => {
try {
chrome.action.setBadgeBackgroundColor(details);
} catch (e) {
// Zakładka zamknięta - ignorujemy
}
},
}, },
// WebRequest API - chrome.webRequest.* → webRequest.* // WebRequest API - chrome.webRequest.* → webRequest.*
webRequest: { webRequest: {
onBeforeRequest: { onBeforeRequest: {
addListener: chrome.webRequest.onBeforeRequest.addListener, addListener: chrome.webRequest.onBeforeRequest.addListener.bind(chrome.webRequest.onBeforeRequest),
}, },
onBeforeSendHeaders: { onBeforeSendHeaders: {
addListener: chrome.webRequest.onBeforeSendHeaders.addListener, addListener: chrome.webRequest.onBeforeSendHeaders.addListener.bind(chrome.webRequest.onBeforeSendHeaders),
}, },
}, },
// Cookies API - chrome.cookies.* → cookies.* // Cookies API - chrome.cookies.* → cookies.*
cookies: { cookies: {
getAll: chrome.cookies.getAll, getAll: chrome.cookies.getAll.bind(chrome.cookies),
remove: chrome.cookies.remove, remove: chrome.cookies.remove.bind(chrome.cookies),
}, },
// Extension API - chrome.extension.* → extension.* // Extension API - chrome.extension.* → extension.*
extension: { extension: {
getBackgroundPage: chrome.extension.getBackgroundPage, getBackgroundPage: chrome.extension?.getBackgroundPage?.bind(chrome.extension) || (() => null),
}, },
// Windows API - chrome.windows.* → windows.* // Windows API - chrome.windows.* → windows.*
windows: { windows: {
WINDOW_ID_CURRENT: chrome.windows.WINDOW_ID_CURRENT, WINDOW_ID_CURRENT: chrome.windows.WINDOW_ID_CURRENT,
}, },

View File

@ -6,49 +6,102 @@
import type { BrowserAPI } from './types'; import type { BrowserAPI } from './types';
// Firefox używa globalnego obiektu `browser` // Bezpieczny dostęp do globalnego obiektu browser - sprawdzamy czy istnieje w runtime
declare const browser: any; function getBrowser() {
// @ts-ignore - dostęp do potencjalnie niezdefiniowanego globalnego obiektu
if (typeof globalThis.browser !== 'undefined') {
// @ts-ignore
return globalThis.browser;
}
return null;
}
export const firefoxAPI: BrowserAPI = { export const firefoxAPI: BrowserAPI = {
// Tabs API - direct mapping // Tabs API - leniwy dostęp z odpowiednimi typami zwracanymi
tabs: { tabs: {
query: browser.tabs.query, query: (queryInfo: any) => {
const b = getBrowser();
if (b) {
return b.tabs.query(queryInfo);
}
return Promise.resolve([]);
},
onUpdated: { onUpdated: {
addListener: browser.tabs.onUpdated.addListener, addListener: (listener: any) => {
removeListener: browser.tabs.onUpdated.removeListener, getBrowser()?.tabs.onUpdated.addListener(listener);
},
removeListener: (listener: any) => {
getBrowser()?.tabs.onUpdated.removeListener(listener);
},
},
onRemoved: {
addListener: (listener: any) => {
getBrowser()?.tabs.onRemoved?.addListener(listener);
},
removeListener: (listener: any) => {
getBrowser()?.tabs.onRemoved?.removeListener(listener);
},
}, },
}, },
// Badge API - Firefox używa browserAction // Badge API - Firefox używa browserAction (nie action jak Chrome)
badge: { badge: {
setBadgeText: browser.browserAction.setBadgeText, setBadgeText: (details: any) => {
setTitle: browser.browserAction.setTitle, getBrowser()?.browserAction.setBadgeText(details);
setBadgeBackgroundColor: browser.browserAction.setBadgeBackgroundColor, },
setTitle: (details: any) => {
getBrowser()?.browserAction.setTitle(details);
},
setBadgeBackgroundColor: (details: any) => {
getBrowser()?.browserAction.setBadgeBackgroundColor(details);
},
}, },
// WebRequest API - direct mapping // WebRequest API - leniwy dostęp
webRequest: { webRequest: {
onBeforeRequest: { onBeforeRequest: {
addListener: browser.webRequest.onBeforeRequest.addListener, addListener: (listener: any, filter: any, extraInfoSpec?: any) => {
getBrowser()?.webRequest.onBeforeRequest.addListener(listener, filter, extraInfoSpec);
},
}, },
onBeforeSendHeaders: { onBeforeSendHeaders: {
addListener: browser.webRequest.onBeforeSendHeaders.addListener, addListener: (listener: any, filter: any, extraInfoSpec?: any) => {
getBrowser()?.webRequest.onBeforeSendHeaders.addListener(listener, filter, extraInfoSpec);
},
}, },
}, },
// Cookies API - direct mapping // Cookies API - leniwy dostęp z odpowiednimi typami zwracanymi
cookies: { cookies: {
getAll: browser.cookies.getAll, getAll: (details: any) => {
remove: browser.cookies.remove, const b = getBrowser();
if (b) {
return b.cookies.getAll(details);
}
return Promise.resolve([]);
},
remove: (details: any) => {
const b = getBrowser();
if (b) {
return b.cookies.remove(details);
}
return Promise.resolve(null);
},
}, },
// Extension API - direct mapping // Extension API - leniwy dostęp
extension: { extension: {
getBackgroundPage: browser.extension.getBackgroundPage, getBackgroundPage: () => {
const b = getBrowser();
return b ? b.extension.getBackgroundPage() : null;
},
}, },
// Windows API - direct mapping // Windows API - leniwy dostęp
windows: { windows: {
WINDOW_ID_CURRENT: browser.windows.WINDOW_ID_CURRENT, get WINDOW_ID_CURRENT() {
const b = getBrowser();
return b ? b.windows.WINDOW_ID_CURRENT : -2;
},
}, },
}; };

View File

@ -1,27 +1,28 @@
/** /**
* Browser API Abstraction - Main Export * Browser API Abstraction - Główny eksport
* *
* Eksportuje właściwą implementację na podstawie TARGET build variable * Eksportuje właściwą implementację na podstawie zmiennej TARGET z procesu budowania
* Używa statycznych importów dla kompatybilności z Chrome service worker
*/ */
import type { BrowserAPI } from './types'; import type { BrowserAPI } from './types';
import { chromeAPI } from './chrome';
import { firefoxAPI } from './firefox';
// Build-time selection of browser API implementation // Wybór implementacji API przeglądarki w czasie budowania
let browserApi: BrowserAPI; let browserApi: BrowserAPI;
// TARGET jest ustawiane przez esbuild.config.js na podstawie npm script // TARGET jest ustawiane przez esbuild.config.js na podstawie npm script
if (process.env.TARGET === 'chrome') { if (process.env.TARGET === 'chrome') {
// Chrome build - używamy chrome adapter // Build dla Chrome - używamy adaptera Chrome
const { chromeAPI } = require('./chrome');
browserApi = chromeAPI; browserApi = chromeAPI;
} else { } else {
// Firefox build (default) - używamy firefox adapter // Build dla Firefox (domyślny) - używamy adaptera Firefox
const { firefoxAPI } = require('./firefox');
browserApi = firefoxAPI; browserApi = firefoxAPI;
} }
// Eksportuj jako default export // Eksport jako default export
export default browserApi; export default browserApi;
// Re-export typów dla wygody // Re-eksport typów dla wygody
export * from './types'; export * from './types';

View File

@ -1,76 +1,86 @@
/** /**
* Browser API Abstraction - Typy na podstawie faktycznego użycia w kodzie * Browser API Abstraction - Typy na podstawie faktycznego użycia w kodzie
*
* Przeanalizowane pliki:
* - util.ts: tabs.query, Tab.id
* - tab-dropdown.tsx: tabs.query, Tab.id, Tab.title
* - toolbar.tsx: tabs.query, tabs.onUpdated, Tab.url, windows.WINDOW_ID_CURRENT
* - memory.ts: browserAction.*, webRequest.*, cookies.*, extension.*
*/ */
// === Tab API (util.ts, tab-dropdown.tsx, toolbar.tsx) === // Import pełnego typu Request z util.ts
export type Request = {
cookieStoreId?: string;
documentUrl?: string;
frameId: number;
incognito?: boolean;
method: string;
originUrl: string;
parentFrameId: number;
proxyInfo?: {
host: string;
port: number;
type: string;
username: string;
proxyDNS: boolean;
failoverTimeout: number;
};
requestHeaders?: { name: string; value?: string; binaryValue?: number[] }[];
requestId: string;
tabId: number;
thirdParty?: boolean;
timeStamp: number;
type: string;
url: string;
urlClassification?: { firstParty: string[]; thirdParty: string[] };
};
// === Tab API ===
export interface Tab { export interface Tab {
id?: number; // util.ts: tab.id, tab-dropdown.tsx: tab.id id?: number;
title?: string; // tab-dropdown.tsx: tab.title title?: string;
url?: string; // toolbar.tsx: tab.url url?: string;
} }
export interface TabQuery { export interface TabQuery {
currentWindow?: boolean; // util.ts, tab-dropdown.tsx currentWindow?: boolean;
active?: boolean; // toolbar.tsx active?: boolean;
windowId?: number; // toolbar.tsx windowId?: number;
lastFocusedWindow?: boolean; // Chrome używa tego zamiast currentWindow czasami
} }
// === Badge/BrowserAction API (memory.ts) === // === Badge/BrowserAction API ===
export interface BadgeTextDetails { export interface BadgeTextDetails {
text: string; // memory.ts: setBadgeText text: string;
tabId?: number; // memory.ts: setBadgeText (optional) tabId?: number;
} }
export interface BadgeTitleDetails { export interface BadgeTitleDetails {
title: string; // memory.ts: setTitle title: string;
tabId?: number; // memory.ts: setTitle (optional) tabId?: number;
} }
export interface BadgeColorDetails { export interface BadgeColorDetails {
color: string; // memory.ts: setBadgeBackgroundColor color: string;
}
// === WebRequest API (memory.ts) ===
export interface RequestDetails {
requestId: string; // memory.ts: request.requestId
requestHeaders?: RequestHeader[]; // memory.ts: request.requestHeaders
// Note: ExtendedRequest konstruktor używa więcej pól,
// ale tu skupiamy się na tym co bezpośrednio używa browser API
}
export interface RequestHeader {
name: string;
value?: string;
} }
// === WebRequest API ===
export interface RequestFilter { export interface RequestFilter {
urls: string[]; // memory.ts: { urls: ['<all_urls>'] } urls: string[];
} }
export type RequestListener = (details: RequestDetails) => void; export type RequestListener = (details: Request) => void;
// === Cookies API (memory.ts) === // === Cookies API ===
export interface Cookie { export interface Cookie {
name: string; // memory.ts: cookie.name name: string;
domain: string; // memory.ts: cookie.domain domain: string;
} }
export interface CookieQuery { export interface CookieQuery {
domain?: string; // memory.ts: { domain: shorthost } domain?: string;
} }
export interface CookieRemove { export interface CookieRemove {
name: string; // memory.ts: { name: cookie.name, url: ... } name: string;
url: string; // memory.ts: { url: `https://${cookie.domain}` } url: string;
} }
// === Main Browser API Interface === // === Główny interfejs Browser API ===
export interface BrowserAPI { export interface BrowserAPI {
// Tabs API // Tabs API
tabs: { tabs: {
@ -79,6 +89,10 @@ export interface BrowserAPI {
addListener(listener: (tabId: number, changeInfo: any, tab: Tab) => void): void; addListener(listener: (tabId: number, changeInfo: any, tab: Tab) => void): void;
removeListener(listener: (tabId: number, changeInfo: any, tab: Tab) => void): void; removeListener(listener: (tabId: number, changeInfo: any, tab: Tab) => void): void;
}; };
onRemoved?: {
addListener(listener: (tabId: number, removeInfo: any) => void): void;
removeListener(listener: (tabId: number, removeInfo: any) => void): void;
};
}; };
// Badge API (Firefox: browserAction, Chrome: action) // Badge API (Firefox: browserAction, Chrome: action)

35
manifest-chrome.json Normal file
View File

@ -0,0 +1,35 @@
{
"description": "Rentgen is an add-on that automatically visualizes all the data that a given website sends to third parties.",
"manifest_version": 3,
"name": "Rentgen",
"short_name": "Rentgen",
"version": "0.1.10",
"author": "Kuba Orlik, Arkadiusz Wieczorek (Internet. Czas działać!)",
"homepage_url": "https://git.internet-czas-dzialac.pl/icd/rentgen",
"background": {
"service_worker": "lib/background.js"
},
"action": {
"default_icon": {
"16": "assets/icon-16.png",
"32": "assets/icon-32.png",
"48": "assets/icon-48.png"
},
"default_title": "Rentgen",
"default_popup": "components/toolbar/toolbar.html"
},
"icons": {
"16": "assets/icon-16.png",
"32": "assets/icon-32.png",
"48": "assets/icon-48.png",
"128": "assets/icon-128.png"
},
"permissions": [
"storage",
"webRequest",
"cookies"
],
"host_permissions": [
"<all_urls>"
]
}

363
memory.ts
View File

@ -2,19 +2,134 @@ import ExtendedRequest from './extended-request';
import { getshorthost } from './util'; import { getshorthost } from './util';
import { RequestCluster } from './request-cluster'; import { RequestCluster } from './request-cluster';
import { SaferEmitter } from './safer-emitter'; import { SaferEmitter } from './safer-emitter';
import browserAPI from './lib/browser-api';
// Deklaracja Chrome API dla TypeScript
declare const chrome: any;
function setDomainsCount(counter: number, tabId: number) { function setDomainsCount(counter: number, tabId: number) {
browser.browserAction.setBadgeText({ text: counter < 0 ? '0' : counter.toString(), tabId }); // Ochrona przed próbą ustawienia badge dla zamkniętej zakładki
browser.browserAction.setTitle({ try {
title: 'Rentgen', browserAPI.badge.setBadgeText({ text: counter < 0 ? '0' : counter.toString(), tabId });
tabId, browserAPI.badge.setTitle({
}); title: 'Rentgen',
tabId,
});
} catch (e) {
// Zakładka została zamknięta - ignorujemy błąd
console.debug(`Zakładka ${tabId} już nie istnieje, pomijanie aktualizacji badge`);
}
}
// Cachowany RequestCluster dla popupu Chrome (ma metody, ale używa danych z cache)
class CachedRequestCluster extends RequestCluster {
private _hasCookies: boolean = false;
private _exposesOrigin: boolean = false;
private _hasMarks: boolean = false;
public lastFullUrl: string | null = null;
public lastModified: number = 0;
constructor(id: string, cached: any) {
super(id);
this._hasCookies = cached.hasCookies || false;
this._exposesOrigin = cached.exposesOrigin || false;
this._hasMarks = cached.hasMarks || false;
this.lastFullUrl = cached.lastFullUrl || null;
this.lastModified = cached.lastModified || 0;
this.requests = []; // Pusta tablica zapobiegająca błędom
}
hasCookies(): boolean {
return this._hasCookies;
}
exposesOrigin(): boolean {
return this._exposesOrigin;
}
hasMarks(): boolean {
return this._hasMarks;
}
// Automatyczne zaznaczenie dla cachowanego clustra
autoMark(): void {
this._hasMarks = true;
// ✅ Trigger storage sync
if (process.env.TARGET === 'chrome') {
const memory = getMemory();
if (memory instanceof Memory) {
memory.scheduleSyncToStorage?.();
}
}
}
// Cofnięcie zaznaczenia dla cachowanego clustra
undoMark(): void {
this._hasMarks = false;
// ✅ Trigger storage sync
if (process.env.TARGET === 'chrome') {
const memory = getMemory();
if (memory instanceof Memory) {
memory.scheduleSyncToStorage?.();
}
}
}
// Nadpisanie metody - zwraca pustą tablicę
calculateRepresentativeStolenData(): any[] {
return [];
}
// Nadpisanie metody - zwraca tylko główną domenę
getFullHosts(): string[] {
return [this.id];
}
// Metody wymagane przez report-window
hasMarkedCookies(): boolean {
return this._hasCookies && this._hasMarks;
}
getMarkedRequests(): any[] {
return [];
}
getMarkedEntries(): any[] {
return [];
}
exposesOriginWhere(): any[] {
return this._exposesOrigin ? [{ path: '', source: 'cached', key: '' }] : [];
}
getDataTypeDescription(noun = 'Twojej'): string {
let types_of_data: string[] = [];
if (this.exposesOrigin()) {
types_of_data.push(`część ${noun} historii przeglądania`);
}
if (this.hasMarkedCookies()) {
types_of_data.push('unikalne ID z cookies');
}
if (types_of_data.length > 1) {
types_of_data[types_of_data.length - 1] = 'oraz ' + types_of_data[types_of_data.length - 1];
}
return types_of_data.join(', ');
}
} }
export default class Memory extends SaferEmitter { export default class Memory extends SaferEmitter {
origin_to_history = {} as Record<string, Record<string, RequestCluster>>; origin_to_history = {} as Record<string, Record<string, RequestCluster>>;
isReady: boolean = true; // Firefox jest zawsze gotowy
private readyPromise: Promise<void> | null = null;
private syncScheduled: boolean = false;
private lastSyncTime: number = 0;
// Chrome: Śledzenie pełnych URLi dla zakładek (do generowania screenshotów)
private tabUrls: Map<number, string> = new Map();
async register(request: ExtendedRequest) { async register(request: ExtendedRequest) {
await request.init(); await request.init();
if (!request.isThirdParty()) { if (!request.isThirdParty()) {
return; return;
} }
@ -22,38 +137,75 @@ export default class Memory extends SaferEmitter {
this.origin_to_history[request.origin] = {}; this.origin_to_history[request.origin] = {};
} }
const shorthost = getshorthost(new URL(request.url).host); const shorthost = getshorthost(new URL(request.url).host);
let isNewCluster = false;
if (!this.origin_to_history[request.origin][shorthost]) { if (!this.origin_to_history[request.origin][shorthost]) {
const cluster = new RequestCluster(shorthost); const cluster = new RequestCluster(shorthost);
this.origin_to_history[request.origin][shorthost] = cluster; this.origin_to_history[request.origin][shorthost] = cluster;
isNewCluster = true;
} }
this.origin_to_history[request.origin][shorthost].add(request); this.origin_to_history[request.origin][shorthost].add(request);
// Chrome: Automatyczne zaznaczanie podejrzanych domen w service workerze
if (process.env.TARGET === 'chrome' && isNewCluster) {
// Zaznacz od razu po utworzeniu clustra
this.origin_to_history[request.origin][shorthost].autoMark();
}
this.emit('change', shorthost); this.emit('change', shorthost);
Object.values(this.getClustersForOrigin(request.origin)).some((cluster) => // Owinięcie operacji badge w try-catch
cluster.hasCookies() try {
) Object.values(this.getClustersForOrigin(request.origin)).some((cluster) =>
? browser.browserAction.setBadgeBackgroundColor({ color: '#ff726b' }) cluster.hasCookies()
: browser.browserAction.setBadgeBackgroundColor({ color: '#ffb900' }); )
? browserAPI.badge.setBadgeBackgroundColor({ color: '#ff726b' })
: browserAPI.badge.setBadgeBackgroundColor({ color: '#ffb900' });
if (request.tabId >= 0) { if (request.tabId >= 0) {
setDomainsCount( setDomainsCount(
Object.values(this.getClustersForOrigin(request.origin)).length, Object.values(this.getClustersForOrigin(request.origin)).length,
request.tabId request.tabId
); );
}
} catch (e) {
// Zakładka zamknięta - ignorujemy błędy badge
console.debug('Aktualizacja badge nie powiodła się - zakładka prawdopodobnie zamknięta');
}
// Chrome: Throttlowana synchronizacja do storage (max co 500ms)
if (process.env.TARGET === 'chrome') {
this.scheduleSyncToStorage();
} }
} }
constructor() { constructor() {
super(); super();
browser.webRequest.onBeforeRequest.addListener( browserAPI.webRequest.onBeforeRequest.addListener(
async (request) => { async (request) => {
new ExtendedRequest(request); // Chrome: Śledzenie nawigacji main_frame dla pełnego URL
if (process.env.TARGET === 'chrome' && request.type === 'main_frame' && request.tabId >= 0) {
this.tabUrls.set(request.tabId, request.url);
console.log(`📍 Zapamiętano URL zakładki ${request.tabId}:`, request.url);
}
const extReq = new ExtendedRequest(request);
// Chrome: Wstrzyknięcie pełnego URL ze śledzonych zakładek
if (process.env.TARGET === 'chrome' && request.tabId >= 0) {
const fullUrl = this.tabUrls.get(request.tabId);
if (fullUrl && !extReq.originalURL) {
extReq.originalURL = fullUrl;
}
}
}, },
{ urls: ['<all_urls>'] }, { urls: ['<all_urls>'] },
['requestBody'] ['requestBody']
); );
browser.webRequest.onBeforeSendHeaders.addListener(
browserAPI.webRequest.onBeforeSendHeaders.addListener(
async (request) => { async (request) => {
const extendedRequest = ExtendedRequest.by_id[request.requestId].addHeaders( const extendedRequest = ExtendedRequest.by_id[request.requestId].addHeaders(
request.requestHeaders || [] request.requestHeaders || []
@ -63,10 +215,34 @@ export default class Memory extends SaferEmitter {
{ urls: ['<all_urls>'] }, { urls: ['<all_urls>'] },
['requestHeaders'] ['requestHeaders']
); );
// Chrome: Czyszczenie URLi zakładek po zamknięciu
if (process.env.TARGET === 'chrome') {
browserAPI.tabs.onRemoved?.addListener?.((tabId: number) => {
this.tabUrls.delete(tabId);
});
}
// Chrome: Wczytywanie z storage przy starcie (dla popupu)
if (process.env.TARGET === 'chrome') {
this.isReady = false;
this.readyPromise = this.loadFromStorage().then(() => {
this.isReady = true;
this.emit('ready');
console.log('🔵 Memory gotowa do użycia przez popup');
});
}
} }
emit(eventName: string, data = 'any'): boolean { emit(eventName: string, data = 'any'): boolean {
setTimeout(() => super.emit(eventName, data), 0); setTimeout(() => {
super.emit(eventName, data);
// ✅ Sync to storage when marks change (Chrome only)
if (process.env.TARGET === 'chrome' && eventName === 'change') {
this.scheduleSyncToStorage();
}
}, 0);
return true; return true;
} }
@ -74,11 +250,19 @@ export default class Memory extends SaferEmitter {
return this.origin_to_history[origin] || {}; return this.origin_to_history[origin] || {};
} }
// Chrome: Oczekiwanie na zakończenie wczytywania ze storage
async waitUntilReady(): Promise<void> {
if (this.isReady) return;
if (this.readyPromise) {
await this.readyPromise;
}
}
async removeCookiesFor(origin: string, shorthost?: string): Promise<void> { async removeCookiesFor(origin: string, shorthost?: string): Promise<void> {
if (shorthost) { if (shorthost) {
const cookies = await browser.cookies.getAll({ domain: shorthost }); const cookies = await browserAPI.cookies.getAll({ domain: shorthost });
for (const cookie of cookies) { for (const cookie of cookies) {
await browser.cookies.remove({ await browserAPI.cookies.remove({
name: cookie.name, name: cookie.name,
url: `https://${cookie.domain}`, url: `https://${cookie.domain}`,
}); });
@ -92,19 +276,150 @@ export default class Memory extends SaferEmitter {
.map((cluster) => this.removeCookiesFor(origin, cluster.id)) .map((cluster) => this.removeCookiesFor(origin, cluster.id))
); );
} }
// Chrome: Throttlowana synchronizacja do storage
if (process.env.TARGET === 'chrome') {
this.scheduleSyncToStorage();
}
} }
async removeRequestsFor(origin: string) { async removeRequestsFor(origin: string) {
this.origin_to_history[origin] = {}; this.origin_to_history[origin] = {};
// Chrome: Throttlowana synchronizacja do storage
if (process.env.TARGET === 'chrome') {
this.scheduleSyncToStorage();
}
}
// ========================================
// CHROME: Metody synchronizacji ze storage (THROTTLED)
// ========================================
public scheduleSyncToStorage(): void {
if (this.syncScheduled) return;
this.syncScheduled = true;
setTimeout(() => {
this.syncToStorage();
this.syncScheduled = false;
}, 500); // Synchronizacja max co 500ms
}
private syncToStorage(): void {
if (typeof chrome !== 'undefined' && chrome.storage?.session) {
const now = Date.now();
// Pomijamy jeśli synchronizowano niedawno (w ciągu 300ms)
if (now - this.lastSyncTime < 300) {
return;
}
this.lastSyncTime = now;
const serializable: Record<string, any> = {};
for (const [origin, clusters] of Object.entries(this.origin_to_history)) {
serializable[origin] = {};
for (const [shorthost, cluster] of Object.entries(clusters)) {
// Zapisujemy tylko niezbędne dane dla UI
serializable[origin][shorthost] = {
id: cluster.id,
hasCookies: cluster.hasCookies(),
exposesOrigin: cluster.exposesOrigin(),
hasMarks: cluster.hasMarks(),
requestCount: cluster.requests?.length || 0,
lastFullUrl: cluster.lastFullUrl || null,
lastModified: cluster.lastModified || 0,
};
}
}
chrome.storage.session.set({ rentgen_memory: serializable }).catch((err: any) => {
console.error('Nie udało się zsynchronizować pamięci do storage:', err);
});
}
}
private async loadFromStorage(): Promise<void> {
if (typeof chrome !== 'undefined' && chrome.storage?.session) {
try {
const result = await chrome.storage.session.get('rentgen_memory');
if (result.rentgen_memory) {
const serialized = result.rentgen_memory;
for (const [origin, clusters] of Object.entries(serialized)) {
this.origin_to_history[origin] = {};
for (const [shorthost, cached] of Object.entries(clusters as Record<string, any>)) {
// Tworzenie CachedRequestCluster ze wszystkimi potrzebnymi danymi
const clusterData = cached as {
id: string;
hasCookies: boolean;
exposesOrigin: boolean;
hasMarks: boolean;
requestCount: number;
lastFullUrl: string | null;
lastModified: number;
};
this.origin_to_history[origin][shorthost] = new CachedRequestCluster(
clusterData.id,
clusterData
);
}
}
console.log('🔵 Wczytano pamięć z chrome.storage.session:', Object.keys(this.origin_to_history).length, 'origins');
}
} catch (err) {
console.error('Nie udało się wczytać pamięci ze storage:', err);
}
}
} }
} }
// ========================================
// INICJALIZACJA: Firefox vs Chrome
// ========================================
export function init() { export function init() {
const memory = new Memory(); const memory = new Memory();
(window as any).memory = memory; if (process.env.TARGET === 'chrome') {
// Chrome MV3: Service worker używa 'self' zamiast 'window'
(self as any).memory = memory;
console.log('🔵 Memory zainicjalizowana w service workerze Chrome (self.memory + chrome.storage.session)');
} else {
// Firefox: Standardowa strona tła z 'window'
(window as any).memory = memory;
console.log('🦊 Memory zainicjalizowana w stronie tła Firefox (window.memory)');
}
} }
// ========================================
// DOSTĘP DO MEMORY: Firefox vs Chrome
// ========================================
// Cachowana instancja memory dla popupu (tylko Chrome)
let popupMemoryInstance: Memory | null = null;
export function getMemory(): Memory { export function getMemory(): Memory {
return (browser.extension.getBackgroundPage().window as any).memory as Memory; if (process.env.TARGET === 'chrome') {
} // Chrome: Najpierw próba pobrania z service workera
if (typeof self !== 'undefined' && (self as any).memory) {
// Jesteśmy W service workerze - bezpośredni dostęp
return (self as any).memory as Memory;
} else {
// Jesteśmy w popupie/contencie - tworzymy RAZ i cachujemy
if (!popupMemoryInstance) {
console.log('🔵 Tworzenie instancji Chrome memory dla popupu (czytanie z chrome.storage.session)');
popupMemoryInstance = new Memory();
}
return popupMemoryInstance;
}
} else {
// Firefox: Używamy tradycyjnego getBackgroundPage()
const backgroundPage = browserAPI.extension.getBackgroundPage();
if (!backgroundPage) {
throw new Error('Strona tła nie jest dostępna');
}
return (backgroundPage.window as any).memory as Memory;
}
}

775
package-lock.json generated
View File

@ -25,6 +25,7 @@
"addons-linter": "^4.7.0", "addons-linter": "^4.7.0",
"esbuild": "^0.14.14", "esbuild": "^0.14.14",
"esbuild-plugin-sass": "^1.0.1", "esbuild-plugin-sass": "^1.0.1",
"sharp": "^0.34.4",
"typescript": "^4.6.4", "typescript": "^4.6.4",
"web-ext": "^6.7.0", "web-ext": "^6.7.0",
"web-ext-types": "^3.2.1" "web-ext-types": "^3.2.1"
@ -187,6 +188,17 @@
"node": ">= 0.10.4" "node": ">= 0.10.4"
} }
}, },
"node_modules/@emnapi/runtime": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
"integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@eslint/eslintrc": { "node_modules/@eslint/eslintrc": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
@ -292,6 +304,456 @@
"resolved": "https://registry.npmjs.org/@iabtcf/core/-/core-1.5.3.tgz", "resolved": "https://registry.npmjs.org/@iabtcf/core/-/core-1.5.3.tgz",
"integrity": "sha512-DZsenDL6uz/jULc/PlWs9HA2eDHkAcL+JiZ5TtB1O5ZSwZ2BMyJc4I5qC+j/7BZFPXqaHRbGQ7QpbxmcHYLw1Q==" "integrity": "sha512-DZsenDL6uz/jULc/PlWs9HA2eDHkAcL+JiZ5TtB1O5ZSwZ2BMyJc4I5qC+j/7BZFPXqaHRbGQ7QpbxmcHYLw1Q=="
}, },
"node_modules/@img/colour": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz",
"integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.2.3"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz",
"integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.3"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz",
"integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz",
"integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz",
"integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==",
"cpu": [
"arm"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz",
"integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz",
"integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz",
"integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==",
"cpu": [
"s390x"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz",
"integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz",
"integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz",
"integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz",
"integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.3"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz",
"integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.3"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz",
"integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.3"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz",
"integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.2.3"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz",
"integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.3"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz",
"integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.3"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz",
"integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.3"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz",
"integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==",
"cpu": [
"wasm32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.5.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz",
"integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz",
"integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz",
"integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@mdn/browser-compat-data": { "node_modules/@mdn/browser-compat-data": {
"version": "4.1.13", "version": "4.1.13",
"resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-4.1.13.tgz", "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-4.1.13.tgz",
@ -1832,6 +2294,16 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/doctrine": { "node_modules/doctrine": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@ -5699,6 +6171,62 @@
"sha.js": "bin.js" "sha.js": "bin.js"
} }
}, },
"node_modules/sharp": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz",
"integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.0",
"semver": "^7.7.2"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.4",
"@img/sharp-darwin-x64": "0.34.4",
"@img/sharp-libvips-darwin-arm64": "1.2.3",
"@img/sharp-libvips-darwin-x64": "1.2.3",
"@img/sharp-libvips-linux-arm": "1.2.3",
"@img/sharp-libvips-linux-arm64": "1.2.3",
"@img/sharp-libvips-linux-ppc64": "1.2.3",
"@img/sharp-libvips-linux-s390x": "1.2.3",
"@img/sharp-libvips-linux-x64": "1.2.3",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.3",
"@img/sharp-libvips-linuxmusl-x64": "1.2.3",
"@img/sharp-linux-arm": "0.34.4",
"@img/sharp-linux-arm64": "0.34.4",
"@img/sharp-linux-ppc64": "0.34.4",
"@img/sharp-linux-s390x": "0.34.4",
"@img/sharp-linux-x64": "0.34.4",
"@img/sharp-linuxmusl-arm64": "0.34.4",
"@img/sharp-linuxmusl-x64": "0.34.4",
"@img/sharp-wasm32": "0.34.4",
"@img/sharp-win32-arm64": "0.34.4",
"@img/sharp-win32-ia32": "0.34.4",
"@img/sharp-win32-x64": "0.34.4"
}
},
"node_modules/sharp/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -7316,6 +7844,16 @@
"async": "~0.2.9" "async": "~0.2.9"
} }
}, },
"@emnapi/runtime": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
"integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
"dev": true,
"optional": true,
"requires": {
"tslib": "^2.4.0"
}
},
"@eslint/eslintrc": { "@eslint/eslintrc": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
@ -7400,6 +7938,196 @@
"resolved": "https://registry.npmjs.org/@iabtcf/core/-/core-1.5.3.tgz", "resolved": "https://registry.npmjs.org/@iabtcf/core/-/core-1.5.3.tgz",
"integrity": "sha512-DZsenDL6uz/jULc/PlWs9HA2eDHkAcL+JiZ5TtB1O5ZSwZ2BMyJc4I5qC+j/7BZFPXqaHRbGQ7QpbxmcHYLw1Q==" "integrity": "sha512-DZsenDL6uz/jULc/PlWs9HA2eDHkAcL+JiZ5TtB1O5ZSwZ2BMyJc4I5qC+j/7BZFPXqaHRbGQ7QpbxmcHYLw1Q=="
}, },
"@img/colour": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
"dev": true
},
"@img/sharp-darwin-arm64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz",
"integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==",
"dev": true,
"optional": true,
"requires": {
"@img/sharp-libvips-darwin-arm64": "1.2.3"
}
},
"@img/sharp-darwin-x64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz",
"integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==",
"dev": true,
"optional": true,
"requires": {
"@img/sharp-libvips-darwin-x64": "1.2.3"
}
},
"@img/sharp-libvips-darwin-arm64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz",
"integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==",
"dev": true,
"optional": true
},
"@img/sharp-libvips-darwin-x64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz",
"integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==",
"dev": true,
"optional": true
},
"@img/sharp-libvips-linux-arm": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz",
"integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==",
"dev": true,
"optional": true
},
"@img/sharp-libvips-linux-arm64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz",
"integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==",
"dev": true,
"optional": true
},
"@img/sharp-libvips-linux-ppc64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz",
"integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==",
"dev": true,
"optional": true
},
"@img/sharp-libvips-linux-s390x": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz",
"integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==",
"dev": true,
"optional": true
},
"@img/sharp-libvips-linux-x64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz",
"integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==",
"dev": true,
"optional": true
},
"@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz",
"integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==",
"dev": true,
"optional": true
},
"@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz",
"integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==",
"dev": true,
"optional": true
},
"@img/sharp-linux-arm": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz",
"integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==",
"dev": true,
"optional": true,
"requires": {
"@img/sharp-libvips-linux-arm": "1.2.3"
}
},
"@img/sharp-linux-arm64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz",
"integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==",
"dev": true,
"optional": true,
"requires": {
"@img/sharp-libvips-linux-arm64": "1.2.3"
}
},
"@img/sharp-linux-ppc64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz",
"integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==",
"dev": true,
"optional": true,
"requires": {
"@img/sharp-libvips-linux-ppc64": "1.2.3"
}
},
"@img/sharp-linux-s390x": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz",
"integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==",
"dev": true,
"optional": true,
"requires": {
"@img/sharp-libvips-linux-s390x": "1.2.3"
}
},
"@img/sharp-linux-x64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz",
"integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==",
"dev": true,
"optional": true,
"requires": {
"@img/sharp-libvips-linux-x64": "1.2.3"
}
},
"@img/sharp-linuxmusl-arm64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz",
"integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==",
"dev": true,
"optional": true,
"requires": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.3"
}
},
"@img/sharp-linuxmusl-x64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz",
"integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==",
"dev": true,
"optional": true,
"requires": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.3"
}
},
"@img/sharp-wasm32": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz",
"integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==",
"dev": true,
"optional": true,
"requires": {
"@emnapi/runtime": "^1.5.0"
}
},
"@img/sharp-win32-arm64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz",
"integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==",
"dev": true,
"optional": true
},
"@img/sharp-win32-ia32": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz",
"integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==",
"dev": true,
"optional": true
},
"@img/sharp-win32-x64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz",
"integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==",
"dev": true,
"optional": true
},
"@mdn/browser-compat-data": { "@mdn/browser-compat-data": {
"version": "4.1.13", "version": "4.1.13",
"resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-4.1.13.tgz", "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-4.1.13.tgz",
@ -8626,6 +9354,12 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true
},
"doctrine": { "doctrine": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@ -11633,6 +12367,47 @@
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
"sharp": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz",
"integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==",
"dev": true,
"requires": {
"@img/colour": "^1.0.0",
"@img/sharp-darwin-arm64": "0.34.4",
"@img/sharp-darwin-x64": "0.34.4",
"@img/sharp-libvips-darwin-arm64": "1.2.3",
"@img/sharp-libvips-darwin-x64": "1.2.3",
"@img/sharp-libvips-linux-arm": "1.2.3",
"@img/sharp-libvips-linux-arm64": "1.2.3",
"@img/sharp-libvips-linux-ppc64": "1.2.3",
"@img/sharp-libvips-linux-s390x": "1.2.3",
"@img/sharp-libvips-linux-x64": "1.2.3",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.3",
"@img/sharp-libvips-linuxmusl-x64": "1.2.3",
"@img/sharp-linux-arm": "0.34.4",
"@img/sharp-linux-arm64": "0.34.4",
"@img/sharp-linux-ppc64": "0.34.4",
"@img/sharp-linux-s390x": "0.34.4",
"@img/sharp-linux-x64": "0.34.4",
"@img/sharp-linuxmusl-arm64": "0.34.4",
"@img/sharp-linuxmusl-x64": "0.34.4",
"@img/sharp-wasm32": "0.34.4",
"@img/sharp-win32-arm64": "0.34.4",
"@img/sharp-win32-ia32": "0.34.4",
"@img/sharp-win32-x64": "0.34.4",
"detect-libc": "^2.1.0",
"semver": "^7.7.2"
},
"dependencies": {
"semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true
}
}
},
"shebang-command": { "shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@ -5,18 +5,19 @@
"main": "esbuild.config.js", "main": "esbuild.config.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"convert-icons": "node scripts/convert-icons.js",
"build": "node esbuild.config.js", "build": "node esbuild.config.js",
"build:firefox": "TARGET=firefox node esbuild.config.js", "build:firefox": "TARGET=firefox node esbuild.config.js",
"build:chrome": "TARGET=chrome node esbuild.config.js", "build:chrome": "npm run convert-icons && TARGET=chrome node esbuild.config.js",
"watch": "node esbuild.config.js --watch", "watch": "node esbuild.config.js --watch",
"watch:firefox": "TARGET=firefox node esbuild.config.js --watch", "watch:firefox": "TARGET=firefox node esbuild.config.js --watch",
"watch:chrome": "TARGET=chrome node esbuild.config.js --watch", "watch:chrome": "TARGET=chrome node esbuild.config.js --watch",
"ext-test": "web-ext run", "ext-test": "web-ext run",
"build-addon": "npm i && npm run build && npm run create-package", "build-addon": "npm i && npm run build && npm run create-package",
"build-addon:firefox": "npm i && npm run build:firefox && npm run create-package:firefox", "build-addon:firefox": "npm i && npm run build && npm run create-package:firefox",
"build-addon:chrome": "npm i && npm run build:chrome && npm run create-package:chrome", "build-addon:chrome": "npm i && npm run build:chrome && npm run create-package:chrome",
"create-package": "web-ext build --ignore-files '!**/node_modules' '!**/node_modules/**/react-dom' '!**/node_modules/**/react-dom/umd' '!**/node_modules/**/*/react-dom.production.min.js' '!**/node_modules/**/react' '!**/node_modules/**/react/umd' '!**/node_modules/**/*/react.production.min.js' '!**/node_modules/**/survey-react' '!**/node_modules/**/survey-react/*.min.js' '!**/node_modules/**/survey-react/*.min.css' --overwrite-dest", "create-package": "web-ext build --ignore-files '!**/node_modules' '!**/node_modules/**/react-dom' '!**/node_modules/**/react-dom/umd' '!**/node_modules/**/*/react-dom.production.min.js' '!**/node_modules/**/react' '!**/node_modules/**/react/umd' '!**/node_modules/**/*/react.production.min.js' '!**/node_modules/**/survey-react' '!**/node_modules/**/survey-react/*.min.js' '!**/node_modules/**/survey-react/*.min.css' --overwrite-dest",
"create-package:firefox": "cd dist-firefox && web-ext build --overwrite-dest --artifacts-dir ../web-ext-artifacts", "create-package:firefox": "cd dist-firefox && web-ext build --overwrite-dest --artifacts-dir ../web-ext-artifacts --ignore-files '!**/node_modules' '!**/node_modules/**/react-dom' '!**/node_modules/**/react-dom/umd' '!**/node_modules/**/*/react-dom.production.min.js' '!**/node_modules/**/react' '!**/node_modules/**/react/umd' '!**/node_modules/**/*/react.production.min.js' '!**/node_modules/**/survey-react' '!**/node_modules/**/survey-react/*.min.js' '!**/node_modules/**/survey-react/*.min.css'",
"create-package:chrome": "cd dist-chrome && 7z a -tzip ../web-ext-artifacts/rentgen-chrome-0.1.10.zip * && cd ..", "create-package:chrome": "cd dist-chrome && 7z a -tzip ../web-ext-artifacts/rentgen-chrome-0.1.10.zip * && cd ..",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "web-ext lint" "lint": "web-ext lint"
@ -57,6 +58,7 @@
"addons-linter": "^4.7.0", "addons-linter": "^4.7.0",
"esbuild": "^0.14.14", "esbuild": "^0.14.14",
"esbuild-plugin-sass": "^1.0.1", "esbuild-plugin-sass": "^1.0.1",
"sharp": "^0.34.4",
"typescript": "^4.6.4", "typescript": "^4.6.4",
"web-ext": "^6.7.0", "web-ext": "^6.7.0",
"web-ext-types": "^3.2.1" "web-ext-types": "^3.2.1"

32
scripts/convert-icons.js Normal file
View File

@ -0,0 +1,32 @@
import sharp from 'sharp';
import { mkdirSync } from 'fs';
const sizes = [16, 32, 48, 128];
const svgPath = 'assets/icon-addon.svg';
const outputDir = 'dist-chrome/assets';
async function convertIcons() {
try {
// Upewnienie się, że katalog wyjściowy istnieje
mkdirSync(outputDir, { recursive: true });
console.log('🎨 Konwersja ikon SVG do PNG dla Chrome...');
// Konwersja do każdego rozmiaru
for (const size of sizes) {
await sharp(svgPath)
.resize(size, size)
.png()
.toFile(`${outputDir}/icon-${size}.png`);
console.log(`✓ Utworzono icon-${size}.png`);
}
console.log('✅ Wszystkie ikony Chrome wygenerowane pomyślnie');
} catch (error) {
console.error('Błąd konwersji ikon:', error);
process.exit(1);
}
}
convertIcons();

52
util.ts
View File

@ -1,11 +1,12 @@
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import React from 'react'; import React from 'react';
import { DataLocation, Sources } from './stolen-data-entry'; import { DataLocation, Sources } from './stolen-data-entry';
import browserAPI from './lib/browser-api';
export type Unpromisify<T> = T extends Promise<infer R> ? R : T; export type Unpromisify<T> = T extends Promise<infer R> ? R : T;
export type Unarray<T> = T extends Array<infer R> ? R : T; export type Unarray<T> = T extends Array<infer R> ? R : T;
export type Tab = Unarray<Unpromisify<ReturnType<typeof browser.tabs.query>>>; export type Tab = Unarray<Unpromisify<ReturnType<typeof browserAPI.tabs.query>>>;
export type Request = { export type Request = {
cookieStoreId?: string; cookieStoreId?: string;
documentUrl?: string; // RL of the document in which the resource will be loaded. For example, if the web page at "https://example.com" contains an image or an iframe, then the documentUrl for the image or iframe will be "https://example.com". For a top-level document, documentUrl is undefined. documentUrl?: string; // RL of the document in which the resource will be loaded. For example, if the web page at "https://example.com" contains an image or an iframe, then the documentUrl for the image or iframe will be "https://example.com". For a top-level document, documentUrl is undefined.
@ -33,21 +34,50 @@ export type Request = {
}; };
export function getshorthost(host: string) { export function getshorthost(host: string) {
const parts = host // Obsługa przypadków brzegowych
.replace(/^.*:\/\//, '') if (!host || typeof host !== 'string') {
.replace(/\/.*$/, '') console.warn('getshorthost: nieprawidłowy host:', host);
.split('.'); return 'unknown';
}
// Czyszczenie stringa hosta
const cleanHost = host
.replace(/^.*:\/\//, '') // Usunięcie protokołu
.replace(/\/.*$/, '') // Usunięcie ścieżki
.replace(/:\d+$/, ''); // Usunięcie portu
const parts = cleanHost.split('.');
// Obsługa przypadków specjalnych
if (parts.length === 0 || !cleanHost) {
console.warn('getshorthost: pusty host po czyszczeniu');
return 'unknown';
}
if (parts.length === 1) {
// Pojedyncze słowo jak "localhost" lub "unknown"
return parts[0];
}
const second_last = parts.at(-2); const second_last = parts.at(-2);
// Bezpieczny fallback jeśli wciąż nieprawidłowy
if (!second_last) { if (!second_last) {
throw new Error('url too short?'); console.warn('getshorthost: nie można określić domeny dla:', host);
return cleanHost; // Zwracamy jak jest zamiast crashować
} }
let lookback = !['co', 'com'].includes(second_last) ? -2 : -3; let lookback = !['co', 'com'].includes(second_last) ? -2 : -3;
if (parts.at(-2) == 'doubleclick' || parts.at(-2) == 'google') { if (parts.at(-2) == 'doubleclick' || parts.at(-2) == 'google') {
lookback = -4; // to distinguish between google ads and stats lookback = -4; // aby rozróżnić google ads i stats
} else if (parts.at(-2) == 'google') { } else if (parts.at(-2) == 'google') {
lookback = -3; // to distinguish various google services lookback = -3; // aby rozróżnić różne usługi google
} }
return parts.slice(lookback).join('.');
// Upewnienie się, że nie wycinamy poza granice tablicy
const sliceStart = Math.max(0, parts.length + lookback);
return parts.slice(sliceStart).join('.');
} }
export function useEmitter( export function useEmitter(
@ -89,7 +119,7 @@ export function parseCookie(cookie: string): Record<string, string> {
} }
export async function getTabByID(id: number) { export async function getTabByID(id: number) {
const tabs = await browser.tabs.query({ currentWindow: true }); const tabs = await browserAPI.tabs.query({ currentWindow: true });
return tabs.find((tab) => tab.id == id); return tabs.find((tab) => tab.id == id);
} }
@ -317,4 +347,4 @@ export function downloadText(filename: string, text: string) {
element.click(); element.click();
document.body.removeChild(element); document.body.removeChild(element);
} }