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
31 changed files with 1979 additions and 1421 deletions

View File

@ -1,16 +0,0 @@
.log
node_modules
sidebar.js
web-ext-artifacts/
lib/*
yarn-error.log
rentgen.zip
# Generated PNG icons (build artifacts)
assets/icons/*.png
assets/icon-addon-*.png
# Exception: do not ignore the `browser-api` directory inside `lib`
!/lib/browser-api/
Dockerfile

5
.gitignore vendored
View File

@ -2,7 +2,6 @@
node_modules node_modules
sidebar.js sidebar.js
/web-ext-artifacts/ /web-ext-artifacts/
/artifacts/
lib/* lib/*
/yarn-error.log /yarn-error.log
/rentgen.zip /rentgen.zip
@ -12,6 +11,4 @@ lib/*
/assets/icon-addon-*.png /assets/icon-addon-*.png
# Exception: do not ignore the `browser-api` directory inside `lib` # Exception: do not ignore the `browser-api` directory inside `lib`
!/lib/browser-api/ !/lib/browser-api/
.claude

View File

@ -1,111 +0,0 @@
# Rentgen Browser Extension - Docker Build
# See README.md for detailed usage instructions
# Build stage
FROM node:lts AS builder
WORKDIR /app
# Copy package files for dependency installation (better layer caching)
COPY package.json package-lock.json ./
# Install dependencies
RUN npm install
# FIXME: COPY . . invalidates cache, so we need to optionally install Firefox
# and jump back to the correct stage. It might be too complex though, so we
# either need to use build args (if cache properly) or heavily document the
# stage transitions, maybe even with a graph.
# Copy source code (respecting .dockerignore)
COPY . .
# Build the extension for Firefox (default) - without tests
RUN npm run build
# Create the package
RUN npm run create-package
# Test builder stage - builds with ENABLE_TESTS=true
FROM node:lts AS test_builder
WORKDIR /app
# Copy package files for dependency installation
COPY package.json package-lock.json ./
# Install dependencies
RUN npm install
# Copy source code
COPY . .
# Build with tests enabled
RUN ENABLE_TESTS=true npm run build
# Create the package
RUN npm run create-package
# Code quality stage - for running quality checks
FROM builder AS code_quality
RUN npm run typecheck && npm run lint
# Artifacts stage - only contains the built artifacts (for --output)
FROM scratch AS artifacts
# Copy only the built extension zip file to root
COPY --from=builder /app/web-ext-artifacts/*.zip /
# Default stage - full development environment
FROM builder
# Default command shows the built artifact
CMD ["ls", "-lh", "/app/web-ext-artifacts/"]
# Runtime stage - for running extension in Firefox
FROM node:lts AS runtime
WORKDIR /app
# Copy built extension from test_builder (includes test code)
COPY --from=test_builder /app /app
# Install Firefox and Xvfb for headless execution (cached layer)
RUN apt-get update && apt-get install -y \
firefox-esr \
xvfb \
procps \
&& rm -rf /var/lib/apt/lists/*
# Install Python and pip in a separate layer for better caching
RUN apt-get update && apt-get install -y \
python3-pip \
wget \
&& rm -rf /var/lib/apt/lists/*
# Install geckodriver for WebDriver protocol
RUN wget -q https://github.com/mozilla/geckodriver/releases/download/v0.34.0/geckodriver-v0.34.0-linux64.tar.gz \
&& tar -xzf geckodriver-v0.34.0-linux64.tar.gz \
&& mv geckodriver /usr/local/bin/ \
&& rm geckodriver-v0.34.0-linux64.tar.gz \
&& chmod +x /usr/local/bin/geckodriver
# Install Python dependencies for testing
RUN pip3 install --break-system-packages marionette_driver
# Set display for Xvfb
ENV DISPLAY=:99
# Start script (not used in verify stage)
CMD ["echo", "Use verify stage for testing"]
# Integration test stage - automated testing with exit code
FROM runtime AS integration_test
# Copy verification scripts
COPY tests/test_verify.py /app/tests/test_verify.py
COPY tests/test-lib.js /app/tests/test-lib.js
RUN chmod +x /app/tests/test_verify.py
# Run verification and exit with proper exit code
CMD ["python3", "/app/tests/test_verify.py"]

View File

@ -1,33 +1,18 @@
import { init } from "./memory"; import { init } from './memory';
// Use global browser object directly (available in extension context) console.log('🔴 [DIAGNOSTYKA] Wczytywanie background.ts, TARGET =', process.env.TARGET);
declare const browser: any;
declare const ENABLE_TESTS: boolean;
init(); // Inicjalizacja pamięci storage
try {
// Test verification handler for Marionette tests init();
// This proves the background script is executing and can communicate with content scripts console.log('✅ init() zakończone pomyślnie');
if (ENABLE_TESTS) { } catch (error) {
browser.runtime.onMessage.addListener((message: any, sender: any, sendResponse: any) => { console.error('❌ init() nie powiodło się:', error);
if (message.type === 'RENTGEN_TEST_VERIFICATION') {
// Perform a computation to prove the background script is running
// This is not just an echo - we're doing actual processing
const inputValue = message.inputValue || 0;
const computed = (inputValue * 2) + 3;
// Send back a response with computed value and metadata
const response = {
success: true,
computed: computed,
formula: `(${inputValue} * 2) + 3 = ${computed}`,
backgroundTimestamp: Date.now(),
receivedFrom: message.url || 'unknown',
originalInput: inputValue
};
sendResponse(response);
return true; // Keep channel open for async response
}
});
} }
// 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

@ -8,7 +8,7 @@ import verbs, { v } from './verbs';
export default function useSurvey( export default function useSurvey(
clusters: RequestCluster[], clusters: RequestCluster[],
{ onComplete }: { onComplete: (sender: { data: RawAnswers }) => void } { onComplete }: { onComplete: (sender: { data: RawAnswers }) => void }
): Survey.Model | null { ): Survey.ReactSurveyModel | null {
const [survey, setSurvey] = React.useState<Survey.Model | null>(null); const [survey, setSurvey] = React.useState<Survey.Model | null>(null);
React.useEffect(() => { React.useEffect(() => {
const model = generateSurveyQuestions(clusters); const model = generateSurveyQuestions(clusters);

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

@ -43,6 +43,7 @@ export function StolenData({
origin={origin} origin={origin}
shorthost={cluster.id} shorthost={cluster.id}
key={cluster.id + origin} key={cluster.id + origin}
refreshToken={eventCounts[cluster.id] || 0}
minValueLength={minValueLength} minValueLength={minValueLength}
cookiesOnly={cookiesOnly} cookiesOnly={cookiesOnly}
cookiesOrOriginOnly={cookiesOrOriginOnly} cookiesOrOriginOnly={cookiesOrOriginOnly}

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,21 +0,0 @@
services:
rentgen_build:
build: .
rentgen_check:
build:
context: .
target: code_quality
rentgen_run:
build:
context: .
target: runtime
stdin_open: true
tty: true
rentgen_verify:
build:
context: .
target: integration_test
restart: "no"

View File

@ -1,77 +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';
const ENABLE_TESTS = process.env.ENABLE_TESTS === 'true'; // Katalogi wyjściowe
const DIST_DIR = IS_FIREFOX ? './dist-firefox' : './dist-chrome';
// see https://github.com/evanw/esbuild/issues/806#issuecomment-779138268 const LIB_DIR = join(DIST_DIR, 'lib');
let skipReactImports = {
name: 'skipReactImports', console.log(`🎯 Budowanie dla: ${TARGET.toUpperCase()}`);
setup(build) { console.log(`📁 Katalog wyjściowy: ${DIST_DIR}`);
build.onResolve({ filter: /^(react(-dom)?|survey-react)$/ }, (args) => {
return { const watch = process.argv.includes('--watch') && {
path: args.path, onRebuild(error) {
namespace: `globalExternal_${args.path}`, 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 { // Funkcja pomocnicza: rekurencyjne kopiowanie katalogów
contents: `module.exports = globalThis.React`, function copyDir(src, dest) {
loader: 'js', if (!existsSync(dest)) {
}; mkdirSync(dest, { recursive: true });
}); }
build.onLoad({ filter: /.*/, namespace: 'globalExternal_react-dom' }, () => { const entries = readdirSync(src, { withFileTypes: true });
return {
contents: `module.exports = globalThis.ReactDOM`, for (const entry of entries) {
loader: 'js', const srcPath = join(src, entry.name);
}; const destPath = join(dest, entry.name);
});
build.onLoad({ filter: /.*/, namespace: 'globalExternal_survey-react' }, () => { if (entry.isDirectory()) {
return { copyDir(srcPath, destPath);
contents: `module.exports = globalThis.Survey`, } else {
loader: 'js', copyFileSync(srcPath, destPath);
}; }
}); }
}, }
};
// Plugin: kopiowanie plików statycznych po zakończeniu budowania
const 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');
mkdirSync(dirname(manifestDest), { recursive: true });
if (ENABLE_TESTS) { copyFileSync(manifestSrc, manifestDest);
entryPoints.push('tests/test-content-script.js'); console.log(` ✓ Skopiowano ${manifestSrc}${manifestDest}`);
}
// Kopiowanie katalogu components
esbuild if (existsSync('./components')) {
.build({ copyDir('./components', join(DIST_DIR, 'components'));
entryPoints, console.log(' ✓ Skopiowano components/');
bundle: true, }
// minify: true,
outdir: './lib', // Kopiowanie katalogu assets
loader: { '.woff': 'file', '.woff2': 'file' }, if (existsSync('./assets')) {
plugins: [scss(), skipReactImports], copyDir('./assets', join(DIST_DIR, 'assets'));
define: { console.log(' ✓ Skopiowano assets/');
PLUGIN_NAME: '"Rentgen"', }
PLUGIN_URL: '"https://addons.mozilla.org/pl/firefox/addon/rentgen/"',
ENABLE_TESTS: String(ENABLE_TESTS), // Kopiowanie wymaganych bibliotek z node_modules (potrzebne dla plików HTML z UMD React)
}, const nodeModulesDest = join(DIST_DIR, 'node_modules');
external: ['react', 'react-dom', 'survey-react'],
watch, // React
}) const reactUmdSrc = './node_modules/react/umd';
.then(() => console.log('Add-on was built')) const reactUmdDest = join(nodeModulesDest, 'react/umd');
.catch(() => process.exit(1)); 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>"
]
}

View File

@ -3,8 +3,8 @@
"manifest_version": 2, "manifest_version": 2,
"name": "Rentgen", "name": "Rentgen",
"short_name": "Rentgen", "short_name": "Rentgen",
"version": "0.2.1", "version": "0.1.10",
"author": "Kuba Orlik, Arkadiusz Wieczorek (Internet. Time to act! Foundation)", "author": "Kuba Orlik, Arkadiusz Wieczorek (Internet. Czas działać!)",
"homepage_url": "https://git.internet-czas-dzialac.pl/icd/rentgen", "homepage_url": "https://git.internet-czas-dzialac.pl/icd/rentgen",
"background": { "background": {
"scripts": ["lib/background.js"] "scripts": ["lib/background.js"]

365
memory.ts
View File

@ -2,20 +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;
} }
@ -23,37 +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;
}
}

1257
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,26 @@
{ {
"name": "rentgen", "name": "rentgen",
"version": "0.2.1", "version": "0.1.10",
"description": "Rentgen is an add-on prepared for both Firefox-based and Chromium-based browsers. This extension will automatically visualize all the data that a given website sends to third parties.", "description": "Rentgen is an add-on prepared for both Firefox-based and Chromium-based browsers. This extension will automatically visualize all the data that a given website sends to third parties.",
"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": "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"
"docker:verify": "docker compose up --force-recreate --build --abort-on-container-exit --exit-code-from rentgen_verify",
"docker:clean": "docker compose down --rmi local --volumes --remove-orphans"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -59,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();

View File

@ -1,21 +0,0 @@
#!/bin/bash
# Pre-commit hook for Rentgen extension
# Builds and runs verification tests before allowing commit
set -e
echo "Running pre-commit checks..."
# Build all stages
echo "Building Docker images..."
docker compose build
# Run code quality checks (typecheck + lint)
echo "Running code quality checks..."
docker compose up --abort-on-container-exit --exit-code-from rentgen_check rentgen_check
# Run integration tests
echo "Running integration tests..."
docker compose up --abort-on-container-exit --exit-code-from rentgen_verify rentgen_verify
echo "✓ All pre-commit checks passed!"

View File

@ -1,69 +0,0 @@
// Test content script - only for automated testing
// This script proves bidirectional communication between content script and background
// Set initial DOM marker to prove content script is injected
(function() {
function setMarker() {
if (document.body) {
document.body.setAttribute('data-rentgen-injected', 'true');
} else {
// Wait for DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
document.body.setAttribute('data-rentgen-injected', 'true');
});
}
}
}
setMarker();
})();
// Listen for test request from Marionette test script
document.addEventListener('rentgen_test_request', async (event) => {
try {
// Mark that we received the event
document.body.setAttribute('data-rentgen-event-received', 'true');
// Extract test data from event
const testData = event.detail || {};
const inputValue = testData.value || 42;
const timestamp = testData.timestamp || Date.now();
// Send message to background script and wait for response
// This proves background script is running and responsive
const response = await browser.runtime.sendMessage({
type: 'RENTGEN_TEST_VERIFICATION',
inputValue: inputValue,
timestamp: timestamp,
url: window.location.href,
title: document.title
});
// Store the response from background in DOM
// This provides undeniable proof of bidirectional communication
if (response && response.success) {
document.body.setAttribute('data-rentgen-verified', 'true');
document.body.setAttribute('data-rentgen-computed', String(response.computed));
document.body.setAttribute('data-rentgen-formula', response.formula);
document.body.setAttribute('data-rentgen-background-timestamp', String(response.backgroundTimestamp));
// Also dispatch a custom event with the results
document.dispatchEvent(new CustomEvent('rentgen_test_complete', {
detail: {
success: true,
computed: response.computed,
formula: response.formula,
backgroundTimestamp: response.backgroundTimestamp
}
}));
} else {
document.body.setAttribute('data-rentgen-verified', 'false');
document.body.setAttribute('data-rentgen-error', 'No response from background');
}
} catch (error) {
// Store error in DOM for debugging
document.body.setAttribute('data-rentgen-verified', 'false');
document.body.setAttribute('data-rentgen-error', String(error));
}
});

View File

@ -1,58 +0,0 @@
// Test library for Marionette-based extension verification
// This JavaScript code runs in the browser context via Marionette
/**
* Inject test content script into the page
* @returns {Promise<boolean>} - True if injection successful
*/
async function injectTestContentScript() {
// Read the content script file
const response = await fetch(browser.runtime.getURL('lib/tests/test-content-script.js'));
const scriptCode = await response.text();
// Inject it into the page
const script = document.createElement('script');
script.textContent = scriptCode;
document.documentElement.appendChild(script);
script.remove();
// Wait a bit for script to initialize
await new Promise(resolve => setTimeout(resolve, 100));
return document.body.getAttribute('data-rentgen-injected') === 'true';
}
/**
* Test that background script performs computation correctly
* @param {number} testValue - Input value for computation
* @returns {Promise<number|null>} - Computed result or null on failure
*/
async function testBackgroundComputation(testValue) {
// Inject content script first
const injected = await injectTestContentScript();
if (!injected) {
return -1; // Content script not loaded
}
// Dispatch test request to content script
document.dispatchEvent(new CustomEvent('rentgen_test_request', {
detail: { value: testValue, timestamp: Date.now() }
}));
// Wait for background response
return new Promise((resolve) => {
let attempts = 0;
const checkInterval = setInterval(() => {
attempts++;
const computed = document.body.getAttribute('data-rentgen-computed');
if (computed) {
clearInterval(checkInterval);
resolve(parseInt(computed));
} else if (attempts > 50) {
clearInterval(checkInterval);
resolve(null);
}
}, 100);
});
}

View File

@ -1,131 +0,0 @@
#!/usr/bin/env python3
"""
test_verify.py - Minimal extension verification test
Verifies the extension background script is executing by testing
bidirectional communication with a simple addition operation.
"""
import sys
import time
import subprocess
import os
import signal
def is_tty():
"""Check if stdout is a TTY."""
return sys.stdout.isatty()
def red(text):
"""Return red text if TTY, otherwise plain text."""
if is_tty():
return f"\033[91m{text}\033[0m"
return text
def start_xvfb():
"""Start Xvfb virtual X server. Returns PID."""
xvfb = subprocess.Popen(
["Xvfb", ":99", "-screen", "0", "1024x768x24"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
os.environ["DISPLAY"] = ":99"
time.sleep(2)
return xvfb.pid
def start_webext():
"""Start web-ext with Marionette enabled. Returns PID."""
webext = subprocess.Popen(
["npx", "web-ext", "run",
"--arg=-marionette",
"--arg=--marionette-port",
"--arg=2828"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
return webext.pid
def test_addition():
"""Test background script via Marionette. Returns (success, result)."""
try:
from marionette_driver.marionette import Marionette
# Wait for Firefox to start
time.sleep(10)
# Connect to Marionette
client = Marionette(host='localhost', port=2828)
client.start_session()
# Navigate to any page (needed for content script injection)
client.navigate("https://example.com")
time.sleep(5)
# Test: background should compute (17 * 2) + 3 = 37
test_value = 17
expected = 37
# Load test library
test_lib_path = os.path.join(os.path.dirname(__file__), 'test-lib.js')
with open(test_lib_path, 'r') as f:
test_lib = f.read()
# Execute test
result = client.execute_script(
test_lib + "\nreturn testBackgroundComputation(arguments[0]);",
script_args=[test_value],
script_timeout=10000
)
client.close()
if result == expected:
return True, expected
else:
return False, result
except Exception as e:
return False, str(e)
def cleanup(xvfb_pid, webext_pid):
"""Kill processes."""
try:
os.kill(webext_pid, signal.SIGTERM)
except:
pass
try:
os.kill(xvfb_pid, signal.SIGTERM)
except:
pass
def main():
"""Main test."""
xvfb_pid = start_xvfb()
webext_pid = start_webext()
success, result = test_addition()
cleanup(xvfb_pid, webext_pid)
if not success:
print(red(f"FAIL: Expected 37, got {result}"))
return 1
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
sys.exit(130)
except Exception as e:
print(red(f"ERROR: {e}"))
sys.exit(1)

View File

@ -1,124 +0,0 @@
#!/bin/bash
# Verification script for ENABLE_TESTS functionality
# This script tests that the extension behaves correctly with and without ENABLE_TESTS
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
echo "==================================="
echo "ENABLE_TESTS Verification Script"
echo "==================================="
echo ""
# Function to run test and capture result
run_test() {
local test_name="$1"
echo "Running: $test_name"
# Check if test-content-script.js exists in lib/tests/
if [ -f "lib/tests/test-content-script.js" ]; then
echo " ✓ test-content-script.js found in lib/tests/"
else
echo " ✗ test-content-script.js NOT found in lib/tests/"
fi
# Check if ENABLE_TESTS condition in background.js
if grep -q 'if (false)' lib/background.js 2>/dev/null; then
echo " ✓ Test code is disabled (if (false) found)"
elif grep -q 'if (true)' lib/background.js 2>/dev/null; then
echo " ✓ Test code is enabled (if (true) found)"
else
echo " ? Could not determine test code state"
fi
# If we had Docker working, we would run the actual test here
# python3 tests/test_verify.py 2>&1 | tail -5
# For now, we just check the build artifacts
echo ""
}
# Clean previous builds
echo "Cleaning previous builds..."
rm -rf lib/
echo ""
# Test 1: Production build (without ENABLE_TESTS)
echo -e "${YELLOW}TEST 1: Production Build (without ENABLE_TESTS)${NC}"
echo "================================================"
npm run build > /dev/null 2>&1
run_test "Production Build"
# Expected:
# - lib/tests/ should NOT exist
# - background.js should have 'if (false)'
if [ ! -d "lib/tests" ] && grep -q 'if (false)' lib/background.js 2>/dev/null; then
echo -e "${GREEN}✓ PASS: Production build correctly excludes test code${NC}"
else
echo -e "${RED}✗ FAIL: Production build still contains test code${NC}"
fi
echo ""
# Clean for next test
rm -rf lib/
# Test 2: Test build (with ENABLE_TESTS=true)
echo -e "${YELLOW}TEST 2: Test Build (with ENABLE_TESTS=true)${NC}"
echo "============================================="
ENABLE_TESTS=true npm run build > /dev/null 2>&1
run_test "Test Build"
# Expected:
# - lib/tests/test-content-script.js should exist
# - background.js should have 'if (true)'
if [ -f "lib/tests/test-content-script.js" ] && grep -q 'if (true)' lib/background.js 2>/dev/null; then
echo -e "${GREEN}✓ PASS: Test build correctly includes test code${NC}"
else
echo -e "${RED}✗ FAIL: Test build missing test code${NC}"
fi
echo ""
# Summary
echo "==================================="
echo "SUMMARY"
echo "==================================="
echo ""
# Check both conditions for final verdict
PROD_OK=false
TEST_OK=false
# Re-test production build
rm -rf lib/
npm run build > /dev/null 2>&1
if [ ! -d "lib/tests" ] && grep -q 'if (false)' lib/background.js 2>/dev/null; then
PROD_OK=true
fi
# Re-test test build
rm -rf lib/
ENABLE_TESTS=true npm run build > /dev/null 2>&1
if [ -f "lib/tests/test-content-script.js" ] && grep -q 'if (true)' lib/background.js 2>/dev/null; then
TEST_OK=true
fi
if $PROD_OK && $TEST_OK; then
echo -e "${GREEN}✓ SUCCESS: ENABLE_TESTS mechanism works correctly!${NC}"
echo " - Production builds exclude test code"
echo " - Test builds include test code"
exit 0
else
echo -e "${RED}✗ FAILURE: ENABLE_TESTS mechanism has issues${NC}"
if ! $PROD_OK; then
echo " - Production build problem detected"
fi
if ! $TEST_OK; then
echo " - Test build problem detected"
fi
exit 1
fi

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