Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4a28e22
refactor: split background script into modular components with TypeSc…
ptgms Sep 22, 2025
145eb11
fix: make firefox background script work
ptgms Sep 22, 2025
1412bb1
feat: improve error handling with detailed error messages and UI updates
ptgms Sep 30, 2025
c2cc935
feat: add multi-language support
ptgms Sep 30, 2025
6f297e1
remove src folder if exists
ptgms Sep 30, 2025
ba08347
defer script, rename variable
Oct 21, 2025
8aeb951
add document interface
Oct 21, 2025
78a8b53
html
Oct 21, 2025
d95fc5a
tmp
Oct 21, 2025
60091f4
merge code
Oct 21, 2025
65f02a8
fix types, remove any
Oct 21, 2025
f7efcf9
add more types
Oct 21, 2025
e534bef
revert serviceresponse id number to string
Oct 21, 2025
4381737
clean
Oct 21, 2025
de4ffca
Group documents wihtout points
Oct 21, 2025
6547ea4
fix points without docs and docs without points always showing
Oct 21, 2025
1c86882
fix type error
Oct 21, 2025
39b074a
fix typo
Oct 21, 2025
e12cb1a
add pointListStyle options to settings page
shadowwwind Oct 22, 2025
14e8670
add default docStyle
shadowwwind Oct 22, 2025
0219c4d
check pointListStyle before rendering
shadowwwind Oct 22, 2025
405e067
re-add the unified pointsList
shadowwwind Oct 22, 2025
b55d34e
clean up
shadowwwind Oct 22, 2025
15a01fd
fix Unexpected negated condition.
shadowwwind Oct 22, 2025
54aa075
use for of
shadowwwind Oct 22, 2025
132a165
add alt text to icon
shadowwwind Oct 22, 2025
5fd853c
clean
shadowwwind Oct 22, 2025
775d3b1
move a constant to constants
ptgms Oct 29, 2025
b944b98
fix: remove dupped id's and unused elements, change style selector to…
shadowwwind Nov 4, 2025
b11a284
Merge branch 'tosdr:modules' into modules
shadowwwind Nov 18, 2025
1c327fb
[feat] sort docuements alphabetically
shadowwwind Nov 18, 2025
31557cc
[fix] move default listStyle to constants, improve types, fix typo
shadowwwind Nov 18, 2025
ac1986a
Merge branch 'master' into modules
shadowwwind Nov 27, 2025
81f92ed
merge
shadowwwind Nov 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/scripts/background/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { setLocal } from '../lib/chromeStorage';
import { checkIfUpdateNeeded } from './database';
import { donationReminderAllowed } from './donation';
import { initializePageAction } from './pageAction';
import { DEFAULT_LIST_STYLE } from "../constants";

export async function handleExtensionInstalled(reason:chrome.runtime.InstalledDetails): Promise<void> {
const donationAllowed = donationReminderAllowed(navigator.userAgent);
Expand All @@ -13,6 +14,7 @@ export async function handleExtensionInstalled(reason:chrome.runtime.InstalledDe
active: false,
allowedPlattform: donationAllowed,
},
pointListStyle: DEFAULT_LIST_STYLE
});

await checkIfUpdateNeeded(true, reason);
Expand Down
4 changes: 3 additions & 1 deletion src/scripts/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ export const API_HEADERS = {
apikey: atob('Y29uZ3JhdHMgb24gZ2V0dGluZyB0aGUga2V5IDpQ'),
};

export const SUPPORTED_LANGUAGES = ['en', 'de', 'nl', 'fr', 'es'] as const;
export const SUPPORTED_LANGUAGES = ['en', 'de', 'nl', 'fr', 'es'] as const;

export const DEFAULT_LIST_STYLE :"docCategories" | "unified" = "docCategories" as const;
246 changes: 183 additions & 63 deletions src/scripts/views/popup/service.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import { getApiUrl, getLanguage, isCuratorMode } from './state';
import { getApiUrl, getLanguage, isCuratorMode, getPointListStyle } from './state';
import { applyHeaderColor } from './theme';

interface ServicePoint {
status: string;
title: string;
case?: {
case: {
classification?: string;
localized_title?: string | null;
};
document_id?: number
}

interface ServiceDocument {
id: number
name: string
url: string
}

interface ServiceResponse {
name: string;
rating?: string;
points: ServicePoint[];
documents: ServiceDocument[]
}

interface SearchResponse {
Expand All @@ -23,6 +31,13 @@ interface SearchResponse {
}>;
}

interface FilteredPoints {
blocker: ServicePoint[];
bad: ServicePoint[];
good: ServicePoint[];
neutral: ServicePoint[];
}

export async function displayServiceDetails(
id: string,
options: { unverified?: boolean } = {}
Expand Down Expand Up @@ -52,7 +67,17 @@ export async function displayServiceDetails(
updatePointsCount(data.points.length);
revealLoadedState(options.unverified === true);

populateList(data.points);
const pointListStyle = getPointListStyle()

if (pointListStyle === "docCategories") {
populateListDocCategories(data.points, data.documents);
} else if (pointListStyle === "unified") {
populateListUnified(data.points)
} else {
console.error("Unsupported pointListStyle", pointListStyle);
}


} catch (error) {
hideLoadingState();
showErrorOverlay(
Expand Down Expand Up @@ -164,7 +189,19 @@ function revealLoadedState(unverified: boolean): void {
}
}

function populateList(points: ServicePoint[]): void {
function populateListUnified(allPoints: ServicePoint[]) {
const documentList = document.getElementById('documentList');
const doc = document.createElement('div');
const temp = `
<div class="">
<div id="pointList" class="pointList">
<a style="display: none">...</a>
</div>
</div>`
;
doc.innerHTML = temp.trim();
documentList!.appendChild(doc.firstChild!);

const pointsList = document.getElementById('pointList');
if (!pointsList) {
return;
Expand All @@ -173,79 +210,162 @@ function populateList(points: ServicePoint[]): void {
pointsList.style.display = 'block';
pointsList.innerHTML = '';

const filteredPoints = filterPoints(points);

appendPointGroup(filteredPoints.blocker, pointsList, false);
appendPointGroup(filteredPoints.bad, pointsList, false);
appendPointGroup(filteredPoints.good, pointsList, false);
appendPointGroup(filteredPoints.neutral, pointsList, true);
}
const filteredPoints = filterPoints(allPoints);

function filterPoints(points: ServicePoint[]): {
blocker: ServicePoint[];
bad: ServicePoint[];
good: ServicePoint[];
neutral: ServicePoint[];
} {
const curatedPoints = points.filter((point) => {
if (!isCuratorMode()) {
return point.status === 'approved';
}
return point.status === 'approved' || point.status === 'pending';
});

return {
blocker: curatedPoints.filter(
(point) => point.case?.classification === 'blocker'
),
bad: curatedPoints.filter(
(point) => point.case?.classification === 'bad'
),
good: curatedPoints.filter(
(point) => point.case?.classification === 'good'
),
neutral: curatedPoints.filter(
(point) => point.case?.classification === 'neutral'
),
};
createPointList(filteredPoints.blocker, pointsList, false);
createPointList(filteredPoints.bad, pointsList, false);
createPointList(filteredPoints.good, pointsList, false);
createPointList(filteredPoints.neutral, pointsList, true);
}

function appendPointGroup(
points: ServicePoint[],
container: HTMLElement,
isLastGroup: boolean
): void {
let added = 0;

points.forEach((point, index) => {
const wrapper = document.createElement('div');
const classification = point.case?.classification ?? 'neutral';
const pointTitle = point.case?.localized_title ?? point.title;
wrapper.innerHTML = `
<div class="point ${classification}">
<img src="icons/${classification}.svg">
<p>${pointTitle}</p>
${renderCuratorTag(point.status)}
function populateListDocCategories(allPoints: ServicePoint[], documents: ServiceDocument[]) {
const documentList = document.getElementById('documentList');
//sort docuements alphabetically
try {
documents.sort((a, b) =>
a.name.localeCompare(b.name)
)
} catch (error) {
console.warn(error)
}
console.log(documents)
// Split points by Document and display them seperatly
for (let i of documents) {
const element = i;

const docPoints = allPoints.filter((point:ServicePoint) => point.document_id === element.id)
const sortedPoints = filterPoints(docPoints)

if (sortedPoints.blocker.length + sortedPoints.bad.length + sortedPoints.neutral.length + sortedPoints.good.length > 0) {
const doc = document.createElement('div');
const temp = `
<div class="">
<div class="documentHeader">
<h3 class="documentTitle">${element.name}</h3>
<a href="${element.url}" target="_blank">Read Original></a>
</div>
<div id="pointList_${element.id}" class="pointList">
<a style="display: none">...</a>
</div>
</div>`;
doc.innerHTML = temp.trim();
documentList!.appendChild(doc.firstChild!);

const pointsList = document.getElementById(`pointList_${element.id}`)!

createSortetPoints(sortedPoints,pointsList)
} else { //documents without points
const docsWithoutPointsWraper = document.getElementById('docsWithoutPointsWraper')
const docsWithoutPoints = document.getElementById('docsWithoutPoints')

if (docsWithoutPoints?.style.display === "none") {
docsWithoutPoints.style.display = "block"
}
const doc = document.createElement('div');
const temp = `
<div class="documentHeader">
<h3 class="documentTitle">${element.name}</h3>
<a href="${element.url}" target="_blank">Read Original></a>
</div>`;
doc.innerHTML = temp.trim();
docsWithoutPointsWraper!.appendChild(doc.firstChild!);
}
}
//display points not liked to a document
const noDocPoints = allPoints.filter((point: ServicePoint) => point.document_id === null)
if (noDocPoints.length > 0) {
const doc = document.createElement('div');
const temp = `
<div class="">
<div class="documentHeader">
<h3 class="documentTitle">Points not linked to a Document</h3>
</div>
`.trim();
if (wrapper.firstChild) {
container.appendChild(wrapper.firstChild as HTMLElement);
<div id="pointList_unlinkedPoints" class="pointList">
<a style="display: none">...</a>
</div>
</div>`;
doc.innerHTML = temp.trim();
documentList!.appendChild(doc.firstChild!);
const sortedPoints = filterPoints(noDocPoints)
const pointsList = document.getElementById(`pointList_unlinkedPoints`)!
createSortetPoints(sortedPoints,pointsList)

}
}
function filterPoints(points:ServicePoint[]) {
if (isCuratorMode()) {
points = points.filter(
(point) =>
point.status === 'approved' || point.status === 'pending'
);
} else {
points = points.filter((point) => point.status === 'approved');
}
added += 1;
let filteredPoints:FilteredPoints = {
blocker: [],
bad: [],
good: [],
neutral: []
}
filteredPoints.blocker = points.filter(
(point) => point.case.classification === 'blocker'
);
filteredPoints.bad = points.filter(
(point) => point.case.classification === 'bad'
);
filteredPoints.good = points.filter(
(point) => point.case.classification === 'good'
);
filteredPoints.neutral = points.filter(
(point) => point.case.classification === 'neutral'
);
return filteredPoints
}

function createSortetPoints(sortedPoints:FilteredPoints,pointsList:HTMLElement) {
if (sortedPoints.blocker) {
createPointList(sortedPoints.blocker, pointsList, false);
}
if (sortedPoints.bad) {
createPointList(sortedPoints.bad, pointsList, false);
}
if (sortedPoints.good) {
createPointList(sortedPoints.good, pointsList, false);
}
if (sortedPoints.neutral) {
createPointList(sortedPoints.neutral, pointsList, true);
}
}

if (index !== points.length - 1) {
function createPointList(pointsFiltered: ServicePoint[], pointsList: HTMLElement, last: boolean) {
let added = 0;
for (let i = 0; i < pointsFiltered.length; i++) {
const point = document.createElement('div');
const pointTitle = pointsFiltered[i]!.case?.localized_title ?? pointsFiltered[i]!.title;

let temp = `
<div class="point ${pointsFiltered[i]!.case.classification}">
<img src="icons/${pointsFiltered[i]!.case.classification}.svg">
<p>${pointTitle}</p>
${renderCuratorTag(pointsFiltered[i]!.status)}
</div>`;
point.innerHTML = temp.trim();
pointsList.appendChild(point.firstChild!);
added++;
if (i !== pointsFiltered.length - 1) {
const divider = document.createElement('hr');
container.appendChild(divider);
pointsList.appendChild(divider);
}
});

if (added > 0 && !isLastGroup) {
}
if (added !== 0 && !last) {
const divider = document.createElement('hr');
divider.classList.add('group');
container.appendChild(divider);
pointsList.appendChild(divider);
}
}
Comment on lines +341 to 366
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: XSS vulnerability in createPointList.

Line 327 directly inserts pointTitle (derived from API data) into HTML via innerHTML on line 330. This is a critical XSS vulnerability.

Even though pointTitle uses a fallback to localized_title or title, both come from the API and must be sanitized:

 function createPointList(pointsFiltered: ServicePoint[], pointsList: HTMLElement, last: boolean) {
     let added = 0;
     for (let i = 0; i < pointsFiltered.length; i++) {
         const point = document.createElement('div');
-        const pointTitle = pointsFiltered[i]!.case?.localized_title ?? pointsFiltered[i]!.title;
+        const rawTitle = pointsFiltered[i]!.case?.localized_title ?? pointsFiltered[i]!.title;
+        const pointTitle = escapeHtml(rawTitle);

         let temp = `
         <div class="point ${pointsFiltered[i]!.case.classification}">

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 ast-grep (0.40.0)

[warning] 329-329: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: point.innerHTML = temp.trim()
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 329-329: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: point.innerHTML = temp.trim()
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)



function renderCuratorTag(status: string): string {
if (!isCuratorMode() || status === 'approved') {
return '';
Expand Down
12 changes: 10 additions & 2 deletions src/scripts/views/popup/state.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DEFAULT_API_URL } from '../../constants';
import { DEFAULT_API_URL} from '../../constants';
import { getLocal } from '../../lib/chromeStorage';
import {
SupportedLanguage,
Expand All @@ -8,11 +8,13 @@ import {
let curatorMode = false;
let apiUrl = DEFAULT_API_URL;
let language: SupportedLanguage = 'en';
let pointListStyle:"docCategories" | "unified" = "unified"

export interface PopupPreferences {
darkmode: boolean;
curatorMode: boolean;
language: SupportedLanguage;
pointListStyle:"docCategories" | "unified"
}

export function isCuratorMode(): boolean {
Expand All @@ -31,11 +33,16 @@ export function setApiUrl(url: string): void {
apiUrl = url;
}

export function getPointListStyle() {
return pointListStyle
}

export async function hydrateState(): Promise<PopupPreferences> {
const result = await getLocal(['darkmode', 'curatorMode', 'api', 'language']);
const result = await getLocal(['darkmode', 'curatorMode', 'api', 'language', 'pointListStyle']);

const darkmode = Boolean(result['darkmode']);
const storedCuratorMode = Boolean(result['curatorMode']);
pointListStyle = result['pointListStyle'] as "docCategories" | "unified"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add validation for type-asserted storage value.

The type assertion as "docCategories" | "unified" is unsafe because storage could contain any value. This could lead to runtime errors if the stored value is corrupted.

Apply this diff:

-    pointListStyle = result['pointListStyle'] as "docCategories" | "unified"
+    const storedStyle = result['pointListStyle'];
+    pointListStyle = (storedStyle === "docCategories" || storedStyle === "unified") 
+        ? storedStyle 
+        : "docCategories";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pointListStyle = result['pointListStyle'] as "docCategories" | "unified"
const storedStyle = result['pointListStyle'];
pointListStyle = (storedStyle === "docCategories" || storedStyle === "unified")
? storedStyle
: "docCategories";
🤖 Prompt for AI Agents
In src/scripts/views/popup/state.ts around line 45, the code unsafely
type-asserts a storage value to "docCategories" | "unified"; validate the value
at runtime instead of using a direct type assertion. Read the raw value, check
it against an allowed set (e.g., ["docCategories","unified"]), and if it matches
assign it, otherwise assign a safe default (or remove the key) and optionally
log/debug the unexpected value; ensure the variable ends up typed correctly
without relying on an unchecked "as" cast.

setCuratorMode(storedCuratorMode);

const api = result['api'];
Expand All @@ -52,6 +59,7 @@ export async function hydrateState(): Promise<PopupPreferences> {
darkmode,
curatorMode: storedCuratorMode,
language: resolvedLanguage,
pointListStyle,
};
}

Expand Down
Loading