Skip to content

Commit f2d0396

Browse files
fix(categories): keep Top Categories colors when category query contains encoded segments (#781)
* fix(categories): decode URL-encoded segments in category color lookup * fix(categories): apply URL-decode normalization to get_category_score Same URL-encoding vulnerability existed in get_category_score as was fixed in get_category_color. Encoded segments like 'Work%20Project' would fail to match stored decoded names and silently return score 0. Adds a regression test mirroring the one for get_category_color. * refactor(categories): extract normalizeSegments helper to remove duplication
1 parent f5ff831 commit f2d0396

2 files changed

Lines changed: 46 additions & 2 deletions

File tree

src/stores/categories.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@ function getScoreFromCategory(c: Category, allCats: Category[]): number {
3333
}
3434
}
3535

36+
// Normalize URL-encoded category segments (e.g. "Work%20Project" → "Work Project").
37+
// Route query params can arrive encoded while category names are stored decoded.
38+
function normalizeSegments(cat: string[]): string[] {
39+
return (cat || []).map(segment => {
40+
try {
41+
return decodeURIComponent(segment);
42+
} catch {
43+
return segment;
44+
}
45+
});
46+
}
47+
3648
export const useCategoryStore = defineStore('categories', {
3749
state: (): State => ({
3850
classes: [],
@@ -99,12 +111,12 @@ export const useCategoryStore = defineStore('categories', {
99111
},
100112
get_category_color() {
101113
return (cat: string[]): string => {
102-
return getColorFromCategory(this.get_category(cat), this.classes);
114+
return getColorFromCategory(this.get_category(normalizeSegments(cat)), this.classes);
103115
};
104116
},
105117
get_category_score() {
106118
return (cat: string[]): number => {
107-
return getScoreFromCategory(this.get_category(cat), this.classes);
119+
return getScoreFromCategory(this.get_category(normalizeSegments(cat)), this.classes);
108120
};
109121
},
110122
category_select() {

test/unit/store/categories.test.node.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,36 @@ describe('categories store', () => {
125125
1
126126
);
127127
});
128+
129+
test('get_category_color decodes URL-encoded category segments', () => {
130+
categoryStore.load([
131+
{
132+
name: ['Work Project'],
133+
rule: { type: 'regex', regex: 'work-project' },
134+
data: { color: '#123456' },
135+
} as Category,
136+
]);
137+
138+
const decoded = categoryStore.get_category_color(['Work Project']);
139+
const encoded = categoryStore.get_category_color(['Work%20Project']);
140+
141+
expect(decoded).toEqual('#123456');
142+
expect(encoded).toEqual('#123456');
143+
});
144+
145+
test('get_category_score decodes URL-encoded category segments', () => {
146+
categoryStore.load([
147+
{
148+
name: ['Work Project'],
149+
rule: { type: 'regex', regex: 'work-project' },
150+
data: { score: 42 },
151+
} as Category,
152+
]);
153+
154+
const decoded = categoryStore.get_category_score(['Work Project']);
155+
const encoded = categoryStore.get_category_score(['Work%20Project']);
156+
157+
expect(decoded).toEqual(42);
158+
expect(encoded).toEqual(42);
159+
});
128160
});

0 commit comments

Comments
 (0)