From fe17014affbdc561eda00c3a2e06333778d5376b Mon Sep 17 00:00:00 2001 From: ToxicCrzay270 <185776014+ToxicCrzay270@users.noreply.github.com> Date: Fri, 15 May 2026 00:41:37 +0200 Subject: [PATCH] Add Opera cache cleaner extension baseline --- .../.codex/project.md | 74 ++++ .../.gitea/workflows/build.yml | 37 ++ .../.gitea/workflows/release-dry-run.yml | 43 ++ .../.gitea/workflows/repo-cleanup.yml | 31 ++ .../.gitea/workflows/security-scan.yml | 34 ++ .../.gitea/workflows/template-compliance.yml | 38 ++ opera-cache-cleaner-extension/.gitignore | 32 ++ opera-cache-cleaner-extension/AGENTS.md | 41 ++ opera-cache-cleaner-extension/CHANGELOG.md | 8 + opera-cache-cleaner-extension/CONTRIBUTING.md | 25 ++ opera-cache-cleaner-extension/README.md | 66 ++++ opera-cache-cleaner-extension/SECURITY.md | 21 + opera-cache-cleaner-extension/background.js | 373 ++++++++++++++++++ .../docs/agent-handoff.md | 17 + .../docs/release-checklist.md | 34 ++ .../docs/security-review.md | 54 +++ .../icons/icon128.png | Bin 0 -> 2631 bytes .../icons/icon16.png | Bin 0 -> 504 bytes .../icons/icon48.png | Bin 0 -> 1169 bytes opera-cache-cleaner-extension/manifest.json | 28 ++ opera-cache-cleaner-extension/popup/popup.css | 195 +++++++++ .../popup/popup.html | 78 ++++ opera-cache-cleaner-extension/popup/popup.js | 312 +++++++++++++++ 23 files changed, 1541 insertions(+) create mode 100644 opera-cache-cleaner-extension/.codex/project.md create mode 100644 opera-cache-cleaner-extension/.gitea/workflows/build.yml create mode 100644 opera-cache-cleaner-extension/.gitea/workflows/release-dry-run.yml create mode 100644 opera-cache-cleaner-extension/.gitea/workflows/repo-cleanup.yml create mode 100644 opera-cache-cleaner-extension/.gitea/workflows/security-scan.yml create mode 100644 opera-cache-cleaner-extension/.gitea/workflows/template-compliance.yml create mode 100644 opera-cache-cleaner-extension/.gitignore create mode 100644 opera-cache-cleaner-extension/AGENTS.md create mode 100644 opera-cache-cleaner-extension/CHANGELOG.md create mode 100644 opera-cache-cleaner-extension/CONTRIBUTING.md create mode 100644 opera-cache-cleaner-extension/README.md create mode 100644 opera-cache-cleaner-extension/SECURITY.md create mode 100644 opera-cache-cleaner-extension/background.js create mode 100644 opera-cache-cleaner-extension/docs/agent-handoff.md create mode 100644 opera-cache-cleaner-extension/docs/release-checklist.md create mode 100644 opera-cache-cleaner-extension/docs/security-review.md create mode 100644 opera-cache-cleaner-extension/icons/icon128.png create mode 100644 opera-cache-cleaner-extension/icons/icon16.png create mode 100644 opera-cache-cleaner-extension/icons/icon48.png create mode 100644 opera-cache-cleaner-extension/manifest.json create mode 100644 opera-cache-cleaner-extension/popup/popup.css create mode 100644 opera-cache-cleaner-extension/popup/popup.html create mode 100644 opera-cache-cleaner-extension/popup/popup.js diff --git a/opera-cache-cleaner-extension/.codex/project.md b/opera-cache-cleaner-extension/.codex/project.md new file mode 100644 index 0000000..4027059 --- /dev/null +++ b/opera-cache-cleaner-extension/.codex/project.md @@ -0,0 +1,74 @@ +# Codex Project Notes + +## Project + +`Opera Cache Cleaner` is an Opera-compatible Chromium extension for clearing only the browser cache from a toolbar popup. It supports immediate cleanup and an optional timer. + +Repository: + +```text +Toxic/Opera-Extensions +``` + +Gitea URL: + +```text +https://git.wilkensxl.de/Toxic/Opera-Extensions.git +``` + +## Commands + +Use these commands as the source of truth from the repository root: + +```text +Syntax check background: node --check background.js +Syntax check popup: node --check popup/popup.js +Build package: mkdir -p dist && zip -r dist/opera-cache-cleaner-extension.zip manifest.json background.js popup icons -x "*.DS_Store" +``` + +No install, lint, test, README generation, or dependency audit command exists. The project has no package manifest and no vendored dependencies. + +## Stack + +```text +Manifest V3 Chromium extension using plain HTML, CSS, and JavaScript. +``` + +Package manager or build tool: + +```text +None. +``` + +## Build Artifacts + +Release artifacts are produced in: + +```text +dist/ +``` + +Expected file: + +```text +opera-cache-cleaner-extension.zip +``` + +## Security Rules + +- Keep the extension limited to `browsingData`, `storage`, and `alarms` permissions unless a feature explicitly requires more. +- The extension must clear only cache data through `chrome.browsingData.remove(..., { cache: true })`. +- Do not request cookie, history, downloads, tabs, or host permissions without a documented user request. +- Do not add external network calls. +- Do not commit secrets, tokens, `.env` files, certificates, or private keys. + +## Release Rules + +Before a release: + +1. run the syntax checks, +2. review `docs/security-review.md`, +3. update `CHANGELOG.md`, +4. build `dist/opera-cache-cleaner-extension.zip`, +5. load the unpacked extension in Opera for a manual smoke test, +6. create a tag and release only when the user explicitly asks for it. diff --git a/opera-cache-cleaner-extension/.gitea/workflows/build.yml b/opera-cache-cleaner-extension/.gitea/workflows/build.yml new file mode 100644 index 0000000..3b93ef7 --- /dev/null +++ b/opera-cache-cleaner-extension/.gitea/workflows/build.yml @@ -0,0 +1,37 @@ +name: Build + +on: + push: + branches: + - main + - master + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check background script syntax + run: node --check background.js + + - name: Check popup script syntax + run: node --check popup/popup.js + + - name: Build extension archive + shell: bash + run: | + rm -rf dist + mkdir -p dist/package + cp manifest.json background.js dist/package/ + cp -R popup icons dist/package/ + cd dist/package + zip -r ../opera-cache-cleaner-extension.zip . + + - name: Upload extension archive + uses: actions/upload-artifact@v3 + with: + name: opera-cache-cleaner-extension + path: dist/opera-cache-cleaner-extension.zip diff --git a/opera-cache-cleaner-extension/.gitea/workflows/release-dry-run.yml b/opera-cache-cleaner-extension/.gitea/workflows/release-dry-run.yml new file mode 100644 index 0000000..e9817bc --- /dev/null +++ b/opera-cache-cleaner-extension/.gitea/workflows/release-dry-run.yml @@ -0,0 +1,43 @@ +name: Release Dry Run + +on: + push: + branches: + - main + - master + workflow_dispatch: + +jobs: + release-dry-run: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Verify release documentation + shell: bash + run: | + test -f README.md + test -f CHANGELOG.md + test -f SECURITY.md + test -f docs/release-checklist.md + test -f docs/security-review.md + + - name: Check for unresolved template placeholders + shell: bash + run: | + if grep -RInE 'PROJECT[_]NAME|PROJECT[_]DESCRIPTION|REPOSITORY[_]OWNER|REPOSITORY[_]NAME|PACKAGE[_]NAME|ARTIFACT[_]NAME|ARTIFACT[_]OUTPUT[_]DIRECTORY|BUILD[_]COMMAND|TEST[_]COMMAND|LINT[_]COMMAND|AUDIT[_]COMMAND' . --exclude-dir=.git --exclude-dir=dist; then + echo "Unresolved template placeholders found." + exit 1 + fi + + - name: Build extension archive + shell: bash + run: | + rm -rf dist + mkdir -p dist/package + cp manifest.json background.js dist/package/ + cp -R popup icons dist/package/ + cd dist/package + zip -r ../opera-cache-cleaner-extension.zip . + test -s ../opera-cache-cleaner-extension.zip diff --git a/opera-cache-cleaner-extension/.gitea/workflows/repo-cleanup.yml b/opera-cache-cleaner-extension/.gitea/workflows/repo-cleanup.yml new file mode 100644 index 0000000..85cb86e --- /dev/null +++ b/opera-cache-cleaner-extension/.gitea/workflows/repo-cleanup.yml @@ -0,0 +1,31 @@ +name: Repository Cleanup + +on: + schedule: + - cron: "0 5 * * 1" + workflow_dispatch: + +jobs: + cleanup-report: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Report generated or sensitive tracked files + shell: bash + run: | + git ls-files | grep -E '(^dist/|^build/|^out/|\.log$|\.tmp$|\.env|\.pem$|\.key$|\.token$)' && { + echo "Tracked generated or sensitive-looking files found." + exit 1 + } || true + + - name: Report large tracked files + shell: bash + run: | + large_files="$(git ls-files -z | xargs -0 du -k | awk '$1 > 1024 { print }')" + if [ -n "$large_files" ]; then + echo "$large_files" + echo "Tracked files above 1 MiB should be reviewed." + exit 1 + fi diff --git a/opera-cache-cleaner-extension/.gitea/workflows/security-scan.yml b/opera-cache-cleaner-extension/.gitea/workflows/security-scan.yml new file mode 100644 index 0000000..1b49480 --- /dev/null +++ b/opera-cache-cleaner-extension/.gitea/workflows/security-scan.yml @@ -0,0 +1,34 @@ +name: Security Scan + +on: + schedule: + - cron: "0 4 * * 1" + workflow_dispatch: + +jobs: + security-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check JavaScript syntax + run: | + node --check background.js + node --check popup/popup.js + + - name: Scan for risky patterns + shell: bash + run: | + if grep -RInE 'eval\s*\(|new Function|innerHTML\s*=|insertAdjacentHTML|fetch\s*\(|XMLHttpRequest|chrome\.tabs|chrome\.cookies|chrome\.history' background.js popup manifest.json; then + echo "Review the matches above before release." + exit 1 + fi + + - name: Check manifest permissions + shell: bash + run: | + if grep -qE '"(tabs|cookies|history|downloads|)"' manifest.json; then + echo "Unexpected broad permission found in manifest.json." + exit 1 + fi diff --git a/opera-cache-cleaner-extension/.gitea/workflows/template-compliance.yml b/opera-cache-cleaner-extension/.gitea/workflows/template-compliance.yml new file mode 100644 index 0000000..d0b1bcd --- /dev/null +++ b/opera-cache-cleaner-extension/.gitea/workflows/template-compliance.yml @@ -0,0 +1,38 @@ +name: Template Compliance + +on: + push: + branches: + - main + - master + pull_request: + workflow_dispatch: + +jobs: + template-compliance: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Verify Codex baseline files + shell: bash + run: | + test -f AGENTS.md + test -f .codex/project.md + test -f SECURITY.md + test -f CHANGELOG.md + test -f docs/security-review.md + test -f docs/release-checklist.md + test -f .gitea/workflows/build.yml + test -f .gitea/workflows/security-scan.yml + test -f .gitea/workflows/repo-cleanup.yml + test -f .gitea/workflows/release-dry-run.yml + + - name: Check for unresolved template placeholders + shell: bash + run: | + if grep -RInE 'PROJECT[_]NAME|PROJECT[_]DESCRIPTION|REPOSITORY[_]OWNER|REPOSITORY[_]NAME|PACKAGE[_]NAME|ARTIFACT[_]NAME|ARTIFACT[_]OUTPUT[_]DIRECTORY|BUILD[_]COMMAND|TEST[_]COMMAND|LINT[_]COMMAND|AUDIT[_]COMMAND' . --exclude-dir=.git --exclude-dir=dist; then + echo "Unresolved template placeholders found." + exit 1 + fi diff --git a/opera-cache-cleaner-extension/.gitignore b/opera-cache-cleaner-extension/.gitignore new file mode 100644 index 0000000..939ecdb --- /dev/null +++ b/opera-cache-cleaner-extension/.gitignore @@ -0,0 +1,32 @@ +# Build outputs +dist/ +build/ +out/ +release/ + +# Logs and temporary files +*.log +*.tmp +*.temp +.cache/ + +# Local environment and secrets +.env +.env.* +!.env.example +*.pem +*.key +*.pfx +*.p12 +*.crt +*.cer +*.token +secrets/ + +# OS and editor files +.DS_Store +Thumbs.db +.idea/ +.vscode/ +*.swp +*.swo diff --git a/opera-cache-cleaner-extension/AGENTS.md b/opera-cache-cleaner-extension/AGENTS.md new file mode 100644 index 0000000..888321c --- /dev/null +++ b/opera-cache-cleaner-extension/AGENTS.md @@ -0,0 +1,41 @@ +# Agent Instructions + +## Project + +Opera Cache Cleaner is an Opera-compatible Chromium extension that clears only the browser cache for a selected time range and can optionally schedule repeated cache cleanup. + +## Repository Rules + +- Preserve the plain Manifest V3 extension structure. Do not add a framework unless the user explicitly asks for it. +- Keep runtime code in `background.js` and `popup/`. +- Keep permissions minimal. The extension currently uses only `browsingData`, `storage`, and `alarms`. +- Do not add host permissions, network calls, cookie/history access, or broad browser permissions without documenting the reason. +- Do not create a release unless the user explicitly asks for one. +- Keep `.codex/project.md` aligned when commands, artifact paths, or release rules change. + +## Commands + +Use these commands from the repository root: + +```bash +node --check background.js +node --check popup/popup.js +mkdir -p dist && zip -r dist/opera-cache-cleaner-extension.zip manifest.json background.js popup icons -x "*.DS_Store" +``` + +There is no package manager, dependency install, lint, or dependency audit command for the current project. + +## Artifacts + +Expected release artifact: + +```text +dist/opera-cache-cleaner-extension.zip +``` + +## Finish Checklist + +- `git diff --check` passes when the project is inside a Git repository. +- `node --check background.js` passes. +- `node --check popup/popup.js` passes. +- Release documentation is updated when release behavior changes. diff --git a/opera-cache-cleaner-extension/CHANGELOG.md b/opera-cache-cleaner-extension/CHANGELOG.md new file mode 100644 index 0000000..962825b --- /dev/null +++ b/opera-cache-cleaner-extension/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +All notable changes to this project are documented here. + +## Unreleased + +- Added Codex repository baseline files and Gitea workflow checks. +- Documented release, security, and maintenance expectations. diff --git a/opera-cache-cleaner-extension/CONTRIBUTING.md b/opera-cache-cleaner-extension/CONTRIBUTING.md new file mode 100644 index 0000000..17a17d0 --- /dev/null +++ b/opera-cache-cleaner-extension/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# Contributing + +## Development + +This extension has no package manager or dependency install step. Edit the plain JavaScript, HTML, CSS, and manifest files directly. + +Run the syntax checks before submitting changes: + +```bash +node --check background.js +node --check popup/popup.js +``` + +## Manual Testing + +1. Open `opera://extensions`. +2. Enable developer mode. +3. Load this folder as an unpacked extension. +4. Open the popup and verify cache clearing, timer save, timer disable, and status display. + +## Security Expectations + +- Do not add host permissions unless required by a documented feature. +- Do not add cookie, history, downloads, or tabs permissions without explicit review. +- Do not add external network calls. diff --git a/opera-cache-cleaner-extension/README.md b/opera-cache-cleaner-extension/README.md new file mode 100644 index 0000000..6c99549 --- /dev/null +++ b/opera-cache-cleaner-extension/README.md @@ -0,0 +1,66 @@ +# Opera Cache Cleaner Extension + +Eine Opera-kompatible Chromium-Erweiterung zum Loeschen des Browser-Caches ueber ein Toolbar-Popup. Zusaetzlich kann ein einmaliger oder wiederholter Timer eingerichtet werden. + +## Funktionen + +- Cache fuer einen auswaehlbaren Zeitraum loeschen. +- Zeitraeume: letzte Stunde, letzte 24 Stunden, letzte 7 Tage, letzte 4 Wochen, gesamter Zeitraum. +- Timer mit Intervall in Minuten, Stunden oder Tagen. +- Optionaler Repeat-Modus fuer wiederholtes Loeschen. +- Statusanzeige fuer letzte Cache-Loeschung, Timer-Status und naechsten geplanten Lauf. + +## Datenschutz + +Die Erweiterung loescht ausschliesslich den Browser-Cache ueber `chrome.browsingData.remove({ since }, { cache: true })`. + +Sie loescht keine Cookies, keinen Verlauf, keine Downloads und keine Passwoerter. + +## Installation in Opera + +1. Oeffne `opera://extensions`. +2. Aktiviere den Entwicklermodus. +3. Klicke auf `Entpackte Erweiterung laden`. +4. Waehle diesen Ordner aus: + + ```text + D:\Codex\Opera-Extensions\opera-cache-cleaner-extension + ``` + +5. Oeffne die Erweiterung ueber das Toolbar-Icon. + +## Entwicklung + +Die Extension verwendet plain HTML, CSS und JavaScript ohne Paketmanager. Fuehre vor Aenderungen oder Releases diese Checks aus: + +```bash +node --check background.js +node --check popup/popup.js +``` + +Ein Release-Archiv kann so gebaut werden: + +```bash +mkdir -p dist && zip -r dist/opera-cache-cleaner-extension.zip manifest.json background.js popup icons -x "*.DS_Store" +``` + +## Verwendung + +1. Waehle im Popup den gewuenschten Zeitraum aus. +2. Klicke auf `Cache jetzt leeren`, um den Cache sofort zu loeschen. +3. Fuer einen Timer: + - Intervall eingeben. + - Einheit auswaehlen. + - Optional `Wiederholen` aktivieren. + - `Timer speichern` klicken. +4. Mit `Timer deaktivieren` wird der aktive Timer entfernt. + +## Berechtigungen + +Die Erweiterung verwendet nur diese Permissions: + +- `browsingData` +- `storage` +- `alarms` + +Es werden keine Host-Permissions verwendet. Die Erweiterung fordert keine `tabs`, `cookies` oder `history` Permission an. diff --git a/opera-cache-cleaner-extension/SECURITY.md b/opera-cache-cleaner-extension/SECURITY.md new file mode 100644 index 0000000..1c9fc0a --- /dev/null +++ b/opera-cache-cleaner-extension/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| --- | --- | +| Latest | Yes | + +## Reporting A Vulnerability + +Please report security issues privately to the project owner. + +Do not include secrets, production data, or private credentials in public issues. + +## Project Security Principles + +- Keep browser permissions minimal. +- Clear only browser cache data. +- Do not add external network calls. +- Do not persist sensitive browsing data. +- Keep release artifacts reproducible from tracked files. diff --git a/opera-cache-cleaner-extension/background.js b/opera-cache-cleaner-extension/background.js new file mode 100644 index 0000000..677a8c1 --- /dev/null +++ b/opera-cache-cleaner-extension/background.js @@ -0,0 +1,373 @@ +const ALARM_NAME = "cacheCleanerTimer"; +const DEFAULT_RANGE_KEY = "last_24h"; +const MAX_INTERVAL_MINUTES = 30 * 24 * 60; +const INTERVAL_UNITS = new Set(["minutes", "hours", "days"]); + +const RANGE_OPTIONS = { + last_hour: { + label: "Letzte Stunde", + sinceMs: 60 * 60 * 1000 + }, + last_24h: { + label: "Letzte 24 Stunden", + sinceMs: 24 * 60 * 60 * 1000 + }, + last_7d: { + label: "Letzte 7 Tage", + sinceMs: 7 * 24 * 60 * 60 * 1000 + }, + last_4w: { + label: "Letzte 4 Wochen", + sinceMs: 28 * 24 * 60 * 60 * 1000 + }, + all_time: { + label: "Gesamter Zeitraum", + sinceMs: null + } +}; + +const DEFAULT_TIMER_CONFIG = { + enabled: false, + repeat: false, + rangeKey: DEFAULT_RANGE_KEY, + intervalValue: 1, + intervalUnit: "hours", + intervalMinutes: 60, + nextRun: null +}; + +function getSinceByRange(rangeKey) { + const option = RANGE_OPTIONS[rangeKey]; + + if (!option) { + throw new Error("Unbekannter Zeitraum."); + } + + return option.sinceMs === null ? 0 : Date.now() - option.sinceMs; +} + +function getStorage(keys) { + return new Promise((resolve, reject) => { + chrome.storage.local.get(keys, (result) => { + const error = chrome.runtime.lastError; + + if (error) { + reject(new Error(error.message)); + return; + } + + resolve(result || {}); + }); + }); +} + +function setStorage(values) { + return new Promise((resolve, reject) => { + chrome.storage.local.set(values, () => { + const error = chrome.runtime.lastError; + + if (error) { + reject(new Error(error.message)); + return; + } + + resolve(); + }); + }); +} + +function createAlarm(details) { + return new Promise((resolve, reject) => { + try { + const result = chrome.alarms.create(ALARM_NAME, details); + const error = chrome.runtime.lastError; + + if (error) { + reject(new Error(error.message)); + return; + } + + if (result && typeof result.then === "function") { + result.then(resolve).catch(reject); + return; + } + + resolve(); + } catch (error) { + reject(error); + } + }); +} + +function clearAlarmOnly() { + return new Promise((resolve, reject) => { + chrome.alarms.clear(ALARM_NAME, () => { + const error = chrome.runtime.lastError; + + if (error) { + reject(new Error(error.message)); + return; + } + + resolve(); + }); + }); +} + +function validateTimerConfig(config) { + if (!config || typeof config !== "object") { + throw new Error("Timer-Konfiguration fehlt."); + } + + if (!RANGE_OPTIONS[config.rangeKey]) { + throw new Error("Unbekannter Zeitraum."); + } + + if (!INTERVAL_UNITS.has(config.intervalUnit)) { + throw new Error("Unbekannte Timer-Einheit."); + } + + if (!Number.isFinite(config.intervalValue)) { + throw new Error("Das Intervall muss eine Zahl sein."); + } + + if (config.intervalValue <= 0) { + throw new Error("Das Intervall muss groesser als 0 sein."); + } + + if (!Number.isFinite(config.intervalMinutes)) { + throw new Error("Das Intervall konnte nicht berechnet werden."); + } + + if (config.intervalMinutes < 1) { + throw new Error("Das Mindestintervall betraegt 1 Minute."); + } + + if (config.intervalMinutes > MAX_INTERVAL_MINUTES) { + throw new Error("Das Maximalintervall betraegt 30 Tage."); + } +} + +async function clearCacheByRange(rangeKey) { + const resolvedRangeKey = RANGE_OPTIONS[rangeKey] ? rangeKey : DEFAULT_RANGE_KEY; + const since = getSinceByRange(resolvedRangeKey); + + // Only the cache flag is set so cookies, history, downloads, and passwords stay untouched. + await new Promise((resolve, reject) => { + chrome.browsingData.remove( + { since }, + { cache: true }, + () => { + const error = chrome.runtime.lastError; + + if (error) { + reject(new Error(error.message)); + return; + } + + resolve(); + } + ); + }); + + const lastRun = Date.now(); + await setStorage({ + selectedRange: resolvedRangeKey, + lastRun + }); + + return { + lastRun, + rangeKey: resolvedRangeKey + }; +} + +async function setupAlarm(config) { + validateTimerConfig(config); + + const intervalMinutes = config.intervalMinutes; + const nextRun = Date.now() + intervalMinutes * 60 * 1000; + const timerConfig = { + enabled: true, + repeat: Boolean(config.repeat), + rangeKey: config.rangeKey, + intervalValue: config.intervalValue, + intervalUnit: config.intervalUnit, + intervalMinutes, + nextRun + }; + const alarmDetails = timerConfig.repeat + ? { + delayInMinutes: intervalMinutes, + periodInMinutes: intervalMinutes + } + : { + delayInMinutes: intervalMinutes + }; + + // Replacing the alarm avoids duplicate timers after repeated saves. + await clearAlarmOnly(); + await createAlarm(alarmDetails); + await setStorage({ + selectedRange: timerConfig.rangeKey, + timerConfig + }); + + return timerConfig; +} + +async function clearAlarm() { + const { timerConfig } = await getStorage(["timerConfig"]); + const disabledConfig = { + ...DEFAULT_TIMER_CONFIG, + ...(timerConfig || {}), + enabled: false, + nextRun: null + }; + + await clearAlarmOnly(); + await setStorage({ timerConfig: disabledConfig }); + + return disabledConfig; +} + +async function restoreAlarmOnStartup() { + const { timerConfig } = await getStorage(["timerConfig"]); + + if (!timerConfig || timerConfig.enabled !== true) { + return; + } + + validateTimerConfig(timerConfig); + + const now = Date.now(); + let nextRun = Number(timerConfig.nextRun); + let delayInMinutes; + + // Extension restarts can miss an alarm, so restart from a future run instead of firing immediately. + if (Number.isFinite(nextRun) && nextRun > now) { + delayInMinutes = Math.max(1, Math.ceil((nextRun - now) / 60000)); + nextRun = now + delayInMinutes * 60000; + } else { + delayInMinutes = timerConfig.intervalMinutes; + nextRun = now + timerConfig.intervalMinutes * 60000; + } + + const restoredConfig = { + ...timerConfig, + nextRun + }; + const alarmDetails = restoredConfig.repeat + ? { + delayInMinutes, + periodInMinutes: restoredConfig.intervalMinutes + } + : { + delayInMinutes + }; + + await createAlarm(alarmDetails); + await setStorage({ timerConfig: restoredConfig }); +} + +async function handleTimerAlarm(alarm) { + if (!alarm || alarm.name !== ALARM_NAME) { + return; + } + + const { timerConfig } = await getStorage(["timerConfig"]); + + if (!timerConfig || timerConfig.enabled !== true) { + return; + } + + const rangeKey = RANGE_OPTIONS[timerConfig.rangeKey] ? timerConfig.rangeKey : DEFAULT_RANGE_KEY; + await clearCacheByRange(rangeKey); + + if (timerConfig.repeat) { + const nextRun = Date.now() + timerConfig.intervalMinutes * 60 * 1000; + await setStorage({ + timerConfig: { + ...timerConfig, + rangeKey, + nextRun + } + }); + return; + } + + await setStorage({ + timerConfig: { + ...timerConfig, + rangeKey, + enabled: false, + nextRun: null + } + }); +} + +function sendError(sendResponse, error) { + sendResponse({ + ok: false, + error: error && error.message ? error.message : "Unbekannter Fehler." + }); +} + +chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { + (async () => { + if (!message || typeof message.type !== "string") { + throw new Error("Unbekannte Nachricht."); + } + + if (message.type === "CLEAR_CACHE") { + const result = await clearCacheByRange(message.rangeKey); + sendResponse({ + ok: true, + ...result + }); + return; + } + + if (message.type === "SET_TIMER") { + const timerConfig = await setupAlarm(message.config); + sendResponse({ + ok: true, + timerConfig + }); + return; + } + + if (message.type === "CLEAR_TIMER") { + const timerConfig = await clearAlarm(); + sendResponse({ + ok: true, + timerConfig + }); + return; + } + + throw new Error("Unbekannter Nachrichtentyp."); + })().catch((error) => { + sendError(sendResponse, error); + }); + + return true; +}); + +chrome.alarms.onAlarm.addListener((alarm) => { + handleTimerAlarm(alarm).catch((error) => { + console.error("Timer konnte den Cache nicht loeschen.", error); + }); +}); + +chrome.runtime.onInstalled.addListener(() => { + restoreAlarmOnStartup().catch((error) => { + console.error("Timer konnte nicht wiederhergestellt werden.", error); + }); +}); + +chrome.runtime.onStartup.addListener(() => { + restoreAlarmOnStartup().catch((error) => { + console.error("Timer konnte nicht wiederhergestellt werden.", error); + }); +}); diff --git a/opera-cache-cleaner-extension/docs/agent-handoff.md b/opera-cache-cleaner-extension/docs/agent-handoff.md new file mode 100644 index 0000000..ede6ee4 --- /dev/null +++ b/opera-cache-cleaner-extension/docs/agent-handoff.md @@ -0,0 +1,17 @@ +# Agent Handoff + +## Current State + +The Codex repository baseline has been applied to the local Opera Cache Cleaner extension. + +## Notes For Next Agent + +- The project is not currently a Git repository in this workspace. +- Expected Gitea repository: `https://git.wilkensxl.de/Toxic/Opera-Extensions.git`. +- The project has no package manager and no external dependencies. +- Use syntax checks and manual Opera extension testing as the main verification path. + +## Open Items + +- Initialize or connect a Git repository if the project should be pushed to Gitea. +- Confirm whether this extension should live at the repository root or inside `opera-cache-cleaner-extension/` before enabling package publishing or Gitea API polling. diff --git a/opera-cache-cleaner-extension/docs/release-checklist.md b/opera-cache-cleaner-extension/docs/release-checklist.md new file mode 100644 index 0000000..f00a1d7 --- /dev/null +++ b/opera-cache-cleaner-extension/docs/release-checklist.md @@ -0,0 +1,34 @@ +# Release Checklist + +## Version + +- [ ] Version number updated in `manifest.json`. +- [ ] `CHANGELOG.md` updated. +- [ ] README checked for current installation and usage instructions. + +## Quality + +- [ ] Working tree is clean. +- [ ] `node --check background.js` passes. +- [ ] `node --check popup/popup.js` passes. +- [ ] Manual unpacked-extension smoke test in Opera passes. + +## Security + +- [ ] `docs/security-review.md` is current. +- [ ] No new permissions were added without review. +- [ ] No secrets are committed. +- [ ] Release artifact does not contain local config files. + +## Artifacts + +- [ ] `dist/opera-cache-cleaner-extension.zip` exists. +- [ ] Zip contains `manifest.json`, `background.js`, `popup/`, and `icons/`. +- [ ] Zip does not contain `dist/`, `.git/`, `.codex/`, `.gitea/`, or docs-only files. + +## Release + +- [ ] Git tag created. +- [ ] Release notes written. +- [ ] Release published. +- [ ] Post-release download smoke test completed. diff --git a/opera-cache-cleaner-extension/docs/security-review.md b/opera-cache-cleaner-extension/docs/security-review.md new file mode 100644 index 0000000..e30fc04 --- /dev/null +++ b/opera-cache-cleaner-extension/docs/security-review.md @@ -0,0 +1,54 @@ +# Security Review + +## Scope + +Project: + +```text +Opera Cache Cleaner +``` + +Reviewed version or commit: + +```text +1.0.0 local workspace +``` + +## Code Patterns Checked + +- [x] No `eval`. +- [x] No dynamic `Function` constructor. +- [x] No unsafe HTML injection found in the reviewed code. +- [x] No shell execution. +- [x] No external network calls. +- [x] No secrets committed in the current source files. +- [x] No unsafe file writes. Browser data changes are limited to cache removal. + +## Dependency Review + +Command: + +```bash +No dependency audit command exists because the project has no package manifest or external dependencies. +``` + +Result: + +```text +Not applicable. +``` + +## Runtime Review + +- [x] Manifest uses least-privilege permissions for the current feature set. +- [x] No host permissions are declared. +- [x] Local storage is used only for selected range, timer settings, and last run timestamp. +- [x] Cache clearing uses `chrome.browsingData.remove({ since }, { cache: true })`. + +## Release Notes + +Known residual risks: + +```text +No automated browser-extension integration tests exist. Perform an unpacked-extension smoke test in Opera before release. +``` diff --git a/opera-cache-cleaner-extension/icons/icon128.png b/opera-cache-cleaner-extension/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..75cba10957669bb3486640141704b840d2ce5e61 GIT binary patch literal 2631 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 literal 0 HcmV?d00001 diff --git a/opera-cache-cleaner-extension/icons/icon16.png b/opera-cache-cleaner-extension/icons/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..d4f2149336ab8439269c375084fbfedcd2bb2977 GIT binary patch literal 504 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0gXvSK~y+TV_={q zIK!i(ahhM>;UvGl0lFMcu%AJm<21j1`x$=y?`Qb+|D)mMC%LpF(DlFp$fnc$`qyC^ zu9yb=zv-R)|GG;ovLVkwu0q!fw)qU7-hGfFkf!f9@BK#xKOa8*e=nsHY{+Roz4sU{ zIm53%9j5v3_a9(QAo|nc(_qao@b%o){}Ixcdk^aIq$)Q+#?6Fhf8g{iIPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1SUyDK~!i%?O8o& z6hRnGDWbb}T16`nF}pU3MG8R+#U>z%2!WuDKTWJmBcu?57K%-Rh1f&{5kwHK5KW;7 z$!@r|-Q0G7ZT=e!wQS!oVpyj}4ePFJ+7;KdJq4Ft)4uPTcGvL6-P^dYhN5iR zI|9)xG7C%CE$+LnD4X_{QNy|!tR@S}MJz-^b_Blc7}f#{D2P~)4j!{Gz;BCO^+Xz0 z5070l9MkTV22$|Nz7B8l*x4%Iwx5)2RD?^hhTL!r>l_ad$M>hZy>Is(d#hi*dP!J% z;drx`u6pIxcBwtnG3`Fqk{NyuSGdjVt^2*1^H;p($!XTA#M+M~??caZzmKt%ztXV+F#iuVLg8^7&l`o6L0wCCRP@RZjm04pVaMJRiA(3eq-yfDFI~`&-M69o^ zMLvtbMCPvD?qsd82o5Z1Kx7i!yk=Pf*W?kzrh73n(!Vgzzrp3p^+g-fwk@gE6ij-1OKsUH3W*iq}y0X2u!-n zsN8YHw7XejD)4T}9fjnRS%iQ2=pXcV1(StaAOUa|6ET11fiy;yS#Dz@@ERa>CNRkj zvO=};0rNjMV(Vf$tP(DP!Y&VxhOtB3r%1evu%^s#O?!YlW}<$_L^#dH0j{LVHUdkm zB^MC=s(28IJRbTy{2GCcpZJWp(Y}HjRvC{AtR*)vi30waq03UC;zgvzM6E2u=hNVq z+zAI=ZW`81P1z<7{L9b3fn`CO#0yBSL#jvwr#3QG+J7jkqVe{G1;|t$Gy + + + + + Opera Cache Cleaner + + + +
+
+ +

Opera Cache Cleaner

+
+ +
+

Cache löschen

+ + +
+ +
+

Timer

+
+ + +
+ + + +
+ + +
+
+ +
+

Status

+
+
+
Letzte Cache-Löschung
+
Noch nie
+
+
+
Timer
+
Inaktiv
+
+
+
Nächster Lauf
+
Nicht geplant
+
+
+
Meldung
+
Keine Fehler.
+
+
+
+
+ + + + diff --git a/opera-cache-cleaner-extension/popup/popup.js b/opera-cache-cleaner-extension/popup/popup.js new file mode 100644 index 0000000..47a69bf --- /dev/null +++ b/opera-cache-cleaner-extension/popup/popup.js @@ -0,0 +1,312 @@ +const DEFAULT_RANGE_KEY = "last_24h"; +const MAX_INTERVAL_MINUTES = 30 * 24 * 60; + +const RANGE_OPTIONS = { + last_hour: { + label: "Letzte Stunde", + sinceMs: 60 * 60 * 1000 + }, + last_24h: { + label: "Letzte 24 Stunden", + sinceMs: 24 * 60 * 60 * 1000 + }, + last_7d: { + label: "Letzte 7 Tage", + sinceMs: 7 * 24 * 60 * 60 * 1000 + }, + last_4w: { + label: "Letzte 4 Wochen", + sinceMs: 28 * 24 * 60 * 60 * 1000 + }, + all_time: { + label: "Gesamter Zeitraum", + sinceMs: null + } +}; + +const UNIT_FACTORS = { + minutes: 1, + hours: 60, + days: 24 * 60 +}; + +const elements = {}; + +function getStorage(keys) { + return new Promise((resolve, reject) => { + chrome.storage.local.get(keys, (result) => { + const error = chrome.runtime.lastError; + + if (error) { + reject(new Error(error.message)); + return; + } + + resolve(result || {}); + }); + }); +} + +function setStorage(values) { + return new Promise((resolve, reject) => { + chrome.storage.local.set(values, () => { + const error = chrome.runtime.lastError; + + if (error) { + reject(new Error(error.message)); + return; + } + + resolve(); + }); + }); +} + +function sendMessage(message) { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage(message, (response) => { + const error = chrome.runtime.lastError; + + if (error) { + reject(new Error(error.message)); + return; + } + + if (!response || response.ok !== true) { + reject(new Error(response && response.error ? response.error : "Die Aktion konnte nicht ausgefuehrt werden.")); + return; + } + + resolve(response); + }); + }); +} + +function populateRangeOptions() { + elements.rangeSelect.textContent = ""; + + Object.entries(RANGE_OPTIONS).forEach(([value, option]) => { + const item = document.createElement("option"); + item.value = value; + item.textContent = option.label; + elements.rangeSelect.appendChild(item); + }); +} + +function formatTimestamp(timestamp, fallbackText) { + if (!Number.isFinite(timestamp)) { + return fallbackText; + } + + const date = new Date(timestamp); + + if (Number.isNaN(date.getTime())) { + return fallbackText; + } + + return new Intl.DateTimeFormat("de-DE", { + dateStyle: "short", + timeStyle: "medium" + }).format(date); +} + +function setMessage(text, state) { + elements.messageValue.textContent = text; + elements.messageValue.classList.remove("message-error", "message-success"); + + if (state === "error") { + elements.messageValue.classList.add("message-error"); + } + + if (state === "success") { + elements.messageValue.classList.add("message-success"); + } +} + +function setBusy(isBusy) { + elements.clearNowButton.disabled = isBusy; + elements.saveTimerButton.disabled = isBusy; + elements.clearTimerButton.disabled = isBusy; +} + +function renderStatus(lastRun, timerConfig) { + const timer = timerConfig || {}; + + elements.lastRunValue.textContent = formatTimestamp(Number(lastRun), "Noch nie"); + elements.timerStateValue.textContent = timer.enabled + ? `Aktiv (${timer.repeat ? "Wiederholung" : "einmalig"})` + : "Inaktiv"; + elements.nextRunValue.textContent = timer.enabled + ? formatTimestamp(Number(timer.nextRun), "Nicht geplant") + : "Nicht geplant"; +} + +function applyState(state) { + const selectedRange = RANGE_OPTIONS[state.selectedRange] ? state.selectedRange : DEFAULT_RANGE_KEY; + const timerConfig = state.timerConfig || {}; + + elements.rangeSelect.value = selectedRange; + + if (Number.isFinite(timerConfig.intervalValue) && timerConfig.intervalValue > 0) { + elements.intervalValue.value = String(timerConfig.intervalValue); + } + + if (UNIT_FACTORS[timerConfig.intervalUnit]) { + elements.intervalUnit.value = timerConfig.intervalUnit; + } + + elements.repeatCheckbox.checked = Boolean(timerConfig.repeat); + renderStatus(state.lastRun, timerConfig); +} + +async function loadState() { + const state = await getStorage(["selectedRange", "lastRun", "timerConfig"]); + applyState(state); +} + +function validateTimerInput() { + const rangeKey = elements.rangeSelect.value; + const intervalText = elements.intervalValue.value.trim(); + const intervalValue = Number(intervalText); + const intervalUnit = elements.intervalUnit.value; + + if (!RANGE_OPTIONS[rangeKey]) { + throw new Error("Bitte einen gueltigen Zeitraum auswaehlen."); + } + + if (intervalText === "" || !Number.isFinite(intervalValue)) { + throw new Error("Das Intervall muss eine Zahl sein."); + } + + if (intervalValue <= 0) { + throw new Error("Das Intervall muss groesser als 0 sein."); + } + + if (!UNIT_FACTORS[intervalUnit]) { + throw new Error("Bitte eine gueltige Einheit auswaehlen."); + } + + const intervalMinutes = intervalValue * UNIT_FACTORS[intervalUnit]; + + if (intervalMinutes < 1) { + throw new Error("Das Mindestintervall betraegt 1 Minute."); + } + + if (intervalMinutes > MAX_INTERVAL_MINUTES) { + throw new Error("Das Maximalintervall betraegt 30 Tage."); + } + + return { + rangeKey, + repeat: elements.repeatCheckbox.checked, + intervalValue, + intervalUnit, + intervalMinutes + }; +} + +async function saveSelectedRange() { + try { + await setStorage({ selectedRange: elements.rangeSelect.value }); + } catch (error) { + setMessage(error.message, "error"); + } +} + +async function handleClearNow() { + setBusy(true); + setMessage("Cache wird geloescht.", "success"); + + try { + const rangeKey = elements.rangeSelect.value; + await sendMessage({ + type: "CLEAR_CACHE", + rangeKey + }); + await loadState(); + setMessage("Cache wurde geleert.", "success"); + } catch (error) { + setMessage(error.message, "error"); + } finally { + setBusy(false); + } +} + +async function handleSaveTimer() { + setBusy(true); + + try { + const config = validateTimerInput(); + await sendMessage({ + type: "SET_TIMER", + config + }); + await loadState(); + setMessage("Timer gespeichert.", "success"); + } catch (error) { + setMessage(error.message, "error"); + } finally { + setBusy(false); + } +} + +async function handleClearTimer() { + setBusy(true); + + try { + await sendMessage({ type: "CLEAR_TIMER" }); + await loadState(); + setMessage("Timer deaktiviert.", "success"); + } catch (error) { + setMessage(error.message, "error"); + } finally { + setBusy(false); + } +} + +function cacheElements() { + elements.rangeSelect = document.getElementById("rangeSelect"); + elements.clearNowButton = document.getElementById("clearNowButton"); + elements.intervalValue = document.getElementById("intervalValue"); + elements.intervalUnit = document.getElementById("intervalUnit"); + elements.repeatCheckbox = document.getElementById("repeatCheckbox"); + elements.saveTimerButton = document.getElementById("saveTimerButton"); + elements.clearTimerButton = document.getElementById("clearTimerButton"); + elements.lastRunValue = document.getElementById("lastRunValue"); + elements.timerStateValue = document.getElementById("timerStateValue"); + elements.nextRunValue = document.getElementById("nextRunValue"); + elements.messageValue = document.getElementById("messageValue"); +} + +function bindEvents() { + elements.rangeSelect.addEventListener("change", saveSelectedRange); + elements.clearNowButton.addEventListener("click", handleClearNow); + elements.saveTimerButton.addEventListener("click", handleSaveTimer); + elements.clearTimerButton.addEventListener("click", handleClearTimer); + + chrome.storage.onChanged.addListener((changes, areaName) => { + if (areaName !== "local") { + return; + } + + if (changes.lastRun || changes.timerConfig || changes.selectedRange) { + loadState().catch((error) => { + setMessage(error.message, "error"); + }); + } + }); +} + +async function init() { + cacheElements(); + populateRangeOptions(); + bindEvents(); + + try { + await loadState(); + } catch (error) { + setMessage(error.message, "error"); + } +} + +document.addEventListener("DOMContentLoaded", init);