diff --git a/extended-request.ts b/extended-request.ts
index d0af544..2a89421 100644
--- a/extended-request.ts
+++ b/extended-request.ts
@@ -1,5 +1,10 @@
import { StolenDataEntry } from "./stolen-data-entry";
-import { getshorthost, parseCookie, Request } from "./util";
+import {
+ flattenObjectEntries,
+ getshorthost,
+ parseCookie,
+ Request,
+} from "./util";
type NameValue = { name: string; value: string };
@@ -151,9 +156,13 @@ export default class ExtendedRequest {
if (!this.hasCookie() || this.getCookie() === undefined) {
return [];
}
- return Object.entries(parseCookie(this.getCookie()))
- .map(([key, value]) => [key, value || ""])
- .map(([key, value]) => new StolenDataEntry(this, "cookie", key, value));
+ return flattenObjectEntries(
+ Object.entries(parseCookie(this.getCookie()))
+ .map(([key, value]) => [key, value || ""])
+ .map(([key, value]) => {
+ return [key, StolenDataEntry.parseValue(value)];
+ })
+ ).map(([key, value]) => new StolenDataEntry(this, "cookie", key, value));
}
hasReferer() {
@@ -174,50 +183,56 @@ export default class ExtendedRequest {
if (!path.includes(";")) {
return [];
}
- return path
- .split(";")
- .map((e) => e.split("="))
- .map(([key, value]) => [key, value || ""])
- .map(
- ([key, value]) =>
- new StolenDataEntry(this, "pathname", key, decodeURIComponent(value))
- );
+ return flattenObjectEntries(
+ path
+ .split(";")
+ .map((e) => e.split("="))
+ .map(([key, value]) => [key, value || ""])
+ .map(([key, value]) => {
+ return [key, StolenDataEntry.parseValue(decodeURIComponent(value))];
+ })
+ ).map(([key, value]) => new StolenDataEntry(this, "pathname", key, value));
}
getQueryParams(): StolenDataEntry[] {
const url = new URL(this.data.url);
- return Array.from((url.searchParams as any).entries())
- .map(([key, value]) => [key, value || ""])
- .map(([key, value]) => {
- try {
- value = decodeURIComponent(value);
- } catch (e) {}
- return new StolenDataEntry(this, "queryparams", key, value);
- });
+ return flattenObjectEntries(
+ Array.from((url.searchParams as any).entries())
+ .map(([key, value]) => [key, value || ""])
+ .map(([key, value]) => {
+ return [key, StolenDataEntry.parseValue(decodeURIComponent(value))];
+ })
+ ).map(([key, value]) => {
+ return new StolenDataEntry(this, "queryparams", key, value);
+ });
}
getHeadersData(): StolenDataEntry[] {
- return this.data.requestHeaders
- .filter((header) => {
- for (const regex of whitelisted_cookies) {
- if (regex.test(header.name)) {
- return false;
+ return flattenObjectEntries(
+ this.data.requestHeaders
+ .filter((header) => {
+ for (const regex of whitelisted_cookies) {
+ if (regex.test(header.name)) {
+ return false;
+ }
}
- }
- return true;
- })
- .map(
- (header) =>
- new StolenDataEntry(this, "header", header.name, header.value)
- );
+ return true;
+ })
+ .map((header) => {
+ return [
+ header.name,
+ StolenDataEntry.parseValue(decodeURIComponent(header.value)),
+ ];
+ })
+ ).map(([key, value]) => new StolenDataEntry(this, "header", key, value));
}
hasMark() {
- return this.stolenData.some((data) => data.hasMark());
+ return this.stolenData.some((data) => data.isMarked);
}
getMarkedEntries() {
- return this.stolenData.filter((data) => data.hasMark());
+ return this.stolenData.filter((data) => data.isMarked);
}
getHost() {
diff --git a/mark.ts b/mark.ts
index 6fa4f13..e69de29 100644
--- a/mark.ts
+++ b/mark.ts
@@ -1,32 +0,0 @@
-import { Classifications, StolenDataEntry } from "./stolen-data-entry";
-
-export default class Mark {
- classification: keyof typeof Classifications;
- constructor(public entry: StolenDataEntry, public key: string) {
- this.classification = entry.classification;
- }
-
- getParsedValue() {
- return this.entry.getParsedValue(this.key);
- }
-
- get shorthost() {
- return this.entry.request.shorthost;
- }
-
- get source() {
- return this.entry.source;
- }
-
- get name() {
- return this.entry.name;
- }
-
- get originalURL() {
- return this.entry.request.originalURL;
- }
-
- get valuePreview(): string {
- return this.entry.getValuePreview(this.key);
- }
-}
diff --git a/package.json b/package.json
index c79b856..ef73da9 100644
--- a/package.json
+++ b/package.json
@@ -4,8 +4,9 @@
"description": "",
"main": "email-template-harsh.js",
"scripts": {
- "build": "npx esbuild sidebar/sidebar.tsx report-window/report-window.tsx --bundle background.ts --bundle --outdir=./lib",
- "watch": "npm run build -- --watch"
+ "build": "npx esbuild sidebar/sidebar.tsx test.ts report-window/report-window.tsx --bundle background.ts --bundle --outdir=./lib",
+ "watch": "npm run build -- --watch",
+ "typecheck": "tsc"
},
"repository": {
"type": "git",
diff --git a/report-window/domain-summary.tsx b/report-window/domain-summary.tsx
index 9350658..02c6510 100644
--- a/report-window/domain-summary.tsx
+++ b/report-window/domain-summary.tsx
@@ -25,18 +25,11 @@ export default function DomainSummary({
Właściciel domeny {cluster.id} otrzymał:{" "}
Mój adres IP
- {cluster.getRepresentativeMarks().map((mark) => (
+ {cluster.getRepresentativeStolenData().map((entry) => (
- {emailClassifications[mark.classification]}{" "}
- {emailSources[mark.source]} (nazwa: {mark.name}
,{" "}
- {mark.key ? (
- <>
- pozycja {mark.key}
,
- >
- ) : (
- ""
- )}
- wartość: {mark.valuePreview}
)
+ {emailClassifications[entry.classification]}{" "}
+ {emailSources[entry.source]} (nazwa: {entry.name}
,{" "}
+ wartość: {entry.getValuePreview()}
)
))}
diff --git a/report-window/email-template.tsx b/report-window/email-template.tsx
index 2406262..49f395d 100644
--- a/report-window/email-template.tsx
+++ b/report-window/email-template.tsx
@@ -1,16 +1,16 @@
import React, { useState } from "react";
-import Mark from "../mark";
import { RequestCluster } from "../request-cluster";
+import { StolenDataEntry } from "../stolen-data-entry";
import { getDate, toBase64 } from "../util";
import DomainSummary from "./domain-summary";
type PopupState = "not_clicked" | "clicked_but_no_reject_all";
export default function EmailTemplate({
- marks,
+ entries,
clusters,
}: {
- marks: Mark[];
+ entries: StolenDataEntry[];
clusters: Record;
version: number;
}): JSX.Element {
@@ -68,8 +68,8 @@ export default function EmailTemplate({
) : null}
Dzień dobry, w dniu {getDate()} odwiedziłem stronę{" "}
- {marks[0].originalURL}. Strona ta wysłała moje dane osobowe do podmiotów
- trzecich - bez mojej zgody.{" "}
+ {entries[0].request.originalURL}. Strona ta wysłała moje dane osobowe do
+ podmiotów trzecich - bez mojej zgody.{" "}
{Object.values(clusters)
diff --git a/report-window/har-converter.tsx b/report-window/har-converter.tsx
index 7103546..189151a 100644
--- a/report-window/har-converter.tsx
+++ b/report-window/har-converter.tsx
@@ -1,18 +1,18 @@
import React, { useEffect, useState } from "react";
import { HAREntry } from "../extended-request";
-import Mark from "../mark";
+import { StolenDataEntry } from "../stolen-data-entry";
import { getshorthost, unique } from "../util";
function handleNewFile(
element: HTMLInputElement,
- marks: Mark[],
+ entries: StolenDataEntry[],
setFiltered: (Blob) => void
): void {
const reader = new FileReader();
reader.addEventListener("load", () => {
const content = JSON.parse(reader.result as string);
content.log.entries = content.log.entries.filter((har_entry: HAREntry) =>
- marks.some((mark) => mark.entry.matchesHAREntry(har_entry))
+ entries.some((entry) => entry.matchesHAREntry(har_entry))
);
setFiltered(
new Blob([JSON.stringify(content)], { type: "application/json" })
@@ -21,8 +21,8 @@ function handleNewFile(
reader.readAsText(element.files[0]);
}
-function generateFakeHAR(marks: Mark[]) {
- const requests = marks.map((mark) => mark.entry.request);
+function generateFakeHAR(entries: StolenDataEntry[]) {
+ const requests = entries.map((entry) => entry.request);
return {
log: {
version: "1.2",
@@ -50,12 +50,16 @@ function generateFakeHAR(marks: Mark[]) {
};
}
-export default function HARConverter({ marks }: { marks: Mark[] }) {
+export default function HARConverter({
+ entries,
+}: {
+ entries: StolenDataEntry[];
+}) {
const [filtered, setFiltered] = useState(null);
const [filename, setFilename] = useState("");
const [fakeHAR, setFakeHAR] = useState>();
useEffect(() => {
- setFakeHAR(generateFakeHAR(marks));
+ setFakeHAR(generateFakeHAR(entries));
}, []);
return (
@@ -65,7 +69,7 @@ export default function HARConverter({ marks }: { marks: Mark[] }) {
accept=".har"
onChange={(e) => {
setFilename(e.target.files[0].name);
- handleNewFile(e.target, marks, setFiltered);
+ handleNewFile(e.target, entries, setFiltered);
}}
/>
{(filtered && (
@@ -82,7 +86,7 @@ export default function HARConverter({ marks }: { marks: Mark[] }) {
new Blob([JSON.stringify(fakeHAR)], { type: "application/json" })
)}
download={`${getshorthost(
- marks[0].originalURL
+ entries[0].request.originalURL
)}-${new Date().toJSON()}-trimmed.har`}
>
Pobierz "zfałszowany" HAR
diff --git a/report-window/report-window.tsx b/report-window/report-window.tsx
index ecaf70b..b47ad16 100644
--- a/report-window/report-window.tsx
+++ b/report-window/report-window.tsx
@@ -15,9 +15,10 @@ function Report() {
setCounter((c) => c + 1);
}
const clusters = getMemory().getClustersForOrigin(origin);
- const marks = Object.values(clusters)
- .map((cluster) => cluster.getRepresentativeMarks())
- .reduce(reduceConcat, []);
+ const entries = Object.values(clusters)
+ .map((cluster) => cluster.getRepresentativeStolenData())
+ .reduce(reduceConcat, [])
+ .filter((entry) => entry.isMarked);
return (
Generuj treść maila dla {origin}
@@ -31,37 +32,36 @@ function Report() {
- {marks.map((mark) => (
+ {entries.map((entry) => (
- {mark.shorthost}
+ {entry.request.shorthost}
- {mark.source}:{mark.name}
- {mark.key}
+ {entry.source}:{entry.name}
- {mark.valuePreview}
+ {entry.getValuePreview()}
{/* always gonna have
one key, because unwrapEntry is called above */}
{
- mark.classification = e.target
+ entry.classification = e.target
.value as keyof typeof Classifications;
refresh();
}}
@@ -81,8 +81,8 @@ function Report() {
))}
-
-
+
+
);
}
diff --git a/request-cluster.ts b/request-cluster.ts
index c953258..0b698c4 100644
--- a/request-cluster.ts
+++ b/request-cluster.ts
@@ -1,10 +1,6 @@
import { EventEmitter } from "events";
import ExtendedRequest from "./extended-request";
-import {
- MergedStolenDataEntry,
- Sources,
- StolenDataEntry,
-} from "./stolen-data-entry";
+import { Sources, StolenDataEntry } from "./stolen-data-entry";
import { allSubhosts, isSameURL, reduceConcat, unique } from "./util";
@@ -34,13 +30,16 @@ export class RequestCluster extends EventEmitter {
return false;
}
- getStolenData(filter: {
- minValueLength: number;
- cookiesOnly: boolean;
- cookiesOrOriginOnly: boolean;
- }): MergedStolenDataEntry[] {
+ getRepresentativeStolenData(
+ filter: {
+ minValueLength: number;
+ cookiesOnly: boolean;
+ cookiesOrOriginOnly: boolean;
+ } = { minValueLength: 0, cookiesOnly: false, cookiesOrOriginOnly: false }
+ ): StolenDataEntry[] {
return this.requests
.map((request) => request.stolenData)
+
.reduce((a, b) => a.concat(b), [])
.filter((entry) => {
return entry.value.length >= filter.minValueLength;
@@ -52,33 +51,64 @@ export class RequestCluster extends EventEmitter {
entry.source === "cookie" ||
entry.classification === "history"
)
- .sort((entryA, entryB) => (entryA.name > entryB.name ? -1 : 1))
- .filter((element, index, array) => {
- // remove duplicates by name/value
+ .sort((entry1, entry2) => {
+ if (entry1.value > entry2.value) {
+ return -1;
+ } else if (entry1.value < entry2.value) {
+ return 1;
+ } else {
+ const indexA = source_priority.indexOf(entry1.source);
+ const indexB = source_priority.indexOf(entry2.source);
+ if (indexA < indexB) {
+ return -1;
+ } else if (indexA > indexB) {
+ return 1;
+ } else {
+ return entry1.value.length > entry2.value.length ? -1 : 1;
+ }
+ }
+ })
+ .filter((_, index, array) => {
+ // removing value duplicates
if (index == 0) {
return true;
}
if (
- element.name != array[index - 1].name ||
- element.value != array[index - 1].value
+ array[index].getValuePreview() ===
+ array[index - 1].getValuePreview() ||
+ (array[index].classification === "history" &&
+ array[index - 1].classification === "history") || // if they're both history, then the first one is the longest
+ isSameURL(array[index].value, array[index - 1].value)
) {
+ return false;
+ } else {
return true;
}
})
- .sort((entryA, entryB) => (entryA.value > entryB.value ? -1 : 1))
- .reduce(
- (acc: MergedStolenDataEntry[], entry: StolenDataEntry) => {
- // group by value
- const last_entry = acc.slice(-1)[0];
- if (last_entry.hasValue(entry.value)) {
- last_entry.mergeWith(entry);
+ .sort((entry1, entry2) => {
+ if (entry1.name < entry2.name) {
+ return -1;
+ } else if (entry1.name > entry2.name) {
+ return 1;
+ } else {
+ if (entry1.value.length > entry2.value.length) {
+ return 1;
} else {
- acc.push(new MergedStolenDataEntry([entry]));
+ return -1;
}
- return acc;
- },
- [new MergedStolenDataEntry([])] as MergedStolenDataEntry[]
- )
+ }
+ })
+ .filter((_, index, array) => {
+ // removing name duplicates, keeping only the first - which is the longest. Some data loss may occur.
+ if (index == 0) {
+ return true;
+ }
+ if (array[index].name === array[index - 1].name) {
+ return false;
+ } else {
+ return true;
+ }
+ })
.sort((entry1, entry2) =>
entry1.getPriority() > entry2.getPriority() ? -1 : 1
);
@@ -125,49 +155,4 @@ export class RequestCluster extends EventEmitter {
exposesOrigin() {
return this.requests.some((request) => request.exposesOrigin());
}
-
- getMarks() {
- return this.requests
- .map((request) => request.getMarkedEntries())
- .reduce(reduceConcat, [])
- .map((entry) => entry.marks)
- .reduce(reduceConcat, []);
- }
-
- getRepresentativeMarks() {
- // removes duplicates so the email/HAR file is shorter
- return this.getMarks()
- .sort((markA, markB) => {
- if (markA.entry.value > markB.entry.value) {
- return -1;
- } else if (markA.entry.value < markB.entry.value) {
- return 1;
- } else {
- const indexA = source_priority.indexOf(markA.source);
- const indexB = source_priority.indexOf(markB.source);
- if (indexA < indexB) {
- return -1;
- } else if (indexA > indexB) {
- return 1;
- } else {
- return markA.entry.value.length > markB.entry.value.length ? -1 : 1;
- }
- }
- })
- .filter((_, index, array) => {
- if (index == 0) {
- return true;
- }
- if (
- array[index].valuePreview === array[index - 1].valuePreview ||
- (array[index].classification === "history" &&
- array[index - 1].classification === "history") || // if they're both history, then the first one is the longest
- isSameURL(array[index].entry.value, array[index - 1].entry.value)
- ) {
- return false;
- } else {
- return true;
- }
- });
- }
}
diff --git a/sidebar/stolen-data-cluster.tsx b/sidebar/stolen-data-cluster.tsx
index 23c13f9..5b344a7 100644
--- a/sidebar/stolen-data-cluster.tsx
+++ b/sidebar/stolen-data-cluster.tsx
@@ -1,96 +1,36 @@
import React from "react";
import { getMemory } from "../memory";
import { RequestCluster } from "../request-cluster";
-import { MergedStolenDataEntry, Sources } from "../stolen-data-entry";
+import { Sources, StolenDataEntry } from "../stolen-data-entry";
-import { hyphenate, useEmitter } from "../util";
+import { useEmitter } from "../util";
const MAX_STRING_VALUE_LENGTH = 100;
-function StolenDataValueTable({
- entry,
- prefixKey = "",
-}: {
- entry: MergedStolenDataEntry;
- prefixKey: string;
-}) {
- return (
-
- {entry.getDecodingsApplied().includes("base64") ? (
-
- "base64"
-
- ) : (
- ""
- )}
-
-
- {Object.keys(entry.getParsedValues(prefixKey)[0]).map((key) => {
- const subkey = `${prefixKey}.${key}`;
- return (
-
- {
- entry.toggleMark(subkey);
- e.stopPropagation();
- }}
- style={{
- border: entry.hasMark(subkey)
- ? "2px solid red"
- : "2px solid transparent",
- }}
- >
- {hyphenate(key)}
-
-
-
-
-
- );
- })}
-
-
-
- );
-}
-
function StolenDataValue({
entry,
- prefixKey = "",
}: {
- entry: MergedStolenDataEntry;
+ entry: StolenDataEntry;
prefixKey?: string;
}) {
const [version] = useEmitter(entry);
- const value = entry.getParsedValues(prefixKey)[0];
let body = null;
- if (!value) {
+ if (!entry.value) {
body = <>>;
- } else if (typeof value === "string") {
- const content = entry.getParsedValues(prefixKey)[0] as string;
+ } else {
body = (
-
- {content.slice(0, MAX_STRING_VALUE_LENGTH)}{" "}
- {content.length > MAX_STRING_VALUE_LENGTH ? "(...)" : ""}
+
+ {entry.value.slice(0, MAX_STRING_VALUE_LENGTH)}{" "}
+ {entry.value.length > MAX_STRING_VALUE_LENGTH ? "(...)" : ""}
);
- } else {
- body =
;
}
return (
{
- entry.toggleMark(prefixKey);
+ entry.toggleMark();
e.stopPropagation();
}}
- data-marks={entry.getMarkedValues().join(", ")}
>
{body}
@@ -108,27 +48,32 @@ function StolenDataRow({
entry,
cluster,
}: {
- entry: MergedStolenDataEntry;
+ entry: StolenDataEntry;
cluster: RequestCluster;
}) {
const [version] = useEmitter(entry);
return (
+
+ entry.toggleMark()}
+ />
+
entry.toggleMark("")}
+ onClick={() => entry.toggleMark()}
>
- {entry.getNames().map(hyphenate).join(", ")}
+ {entry.name}
- {entry.getSources().map((source) => icons[source])}
+ {[entry.source].map((source) => icons[source])}
@@ -172,13 +117,17 @@ export default function StolenDataCluster({
{cluster
- .getStolenData({ minValueLength, cookiesOnly, cookiesOrOriginOnly })
+ .getRepresentativeStolenData({
+ minValueLength,
+ cookiesOnly,
+ cookiesOrOriginOnly,
+ })
.map((entry) => (
))}
diff --git a/stolen-data-entry.ts b/stolen-data-entry.ts
index 59af733..4c6cf82 100644
--- a/stolen-data-entry.ts
+++ b/stolen-data-entry.ts
@@ -1,7 +1,6 @@
-import { TCModel } from "@iabtcf/core";
+// import { TCModel } from "@iabtcf/core";
import { EventEmitter } from "events";
import ExtendedRequest, { HAREntry } from "./extended-request";
-import Mark from "./mark";
import {
getshorthost,
@@ -9,8 +8,6 @@ import {
isJSONObject,
isURL,
parseToObject,
- reduceConcat,
- unique,
} from "./util";
export type Sources = "cookie" | "pathname" | "queryparams" | "header";
@@ -35,9 +32,9 @@ export type DecodingSchema = "base64";
export class StolenDataEntry extends EventEmitter {
public isIAB = false;
- public iab: TCModel | null = null;
+ // public iab: TCModel | null = null;
public id: number;
- public marks: Mark[] = [];
+ private marked = false;
public classification: keyof typeof Classifications;
public decoding_applied: DecodingSchema = null;
@@ -77,8 +74,8 @@ export class StolenDataEntry extends EventEmitter {
return priority;
}
- mergeWith(entry: StolenDataEntry): MergedStolenDataEntry {
- return new MergedStolenDataEntry([this, entry]);
+ get isMarked() {
+ return this.marked;
}
hasValue(value: string) {
@@ -86,6 +83,9 @@ export class StolenDataEntry extends EventEmitter {
}
static parseValue(value: unknown): string | Record {
+ if (isBase64JSON(value)) {
+ return StolenDataEntry.parseValue({ base64: JSON.parse(atob(value)) });
+ }
if (value === undefined) {
return "";
}
@@ -144,29 +144,21 @@ export class StolenDataEntry extends EventEmitter {
return object;
}
- addMark(key: string) {
- this.marks.push(new Mark(this, key));
+ mark() {
+ this.marked = true;
this.emit("change");
}
- hasMark(key?: string) {
- if (key) {
- return this.marks.some((k) => k.key == key);
- } else {
- return this.marks.length > 0;
- }
- }
-
- removeMark(key: string) {
- this.marks = this.marks.filter((mark) => mark.key != key);
+ unmark() {
+ this.marked = false;
this.emit("change");
}
- toggleMark(key: string) {
- if (this.hasMark(key)) {
- this.removeMark(key);
+ toggleMark() {
+ if (this.marked) {
+ this.unmark();
} else {
- this.addMark(key);
+ this.mark();
}
}
@@ -199,7 +191,6 @@ export class StolenDataEntry extends EventEmitter {
}
getValuePreview(key = ""): string {
- console.log("getValuePreview", key, this.getParsedValue(key));
const value = this.getParsedValue(key);
const str =
typeof value === "object" && value[Symbol.for("originalString")]
@@ -218,90 +209,8 @@ export class StolenDataEntry extends EventEmitter {
return str;
}
}
-}
-
-export class MergedStolenDataEntry extends EventEmitter {
- constructor(public entries: StolenDataEntry[]) {
- super();
- const all_marks = unique(
- entries.map((entry) => entry.marks).reduce(reduceConcat, [])
- );
- for (const entry of entries) {
- entry.marks = all_marks;
- }
- // getMemory().emit("change"); // to trigger render
- }
-
- on(event: string, listener: () => void) {
- for (const entry of this.entries) {
- entry.on(event, listener);
- }
- return this;
- }
-
- removeListener(event: string, listener: () => void) {
- for (const entry of this.entries) {
- entry.removeListener(event, listener);
- }
- return this;
- }
-
- hasValue(value: string) {
- return this.entries.some((entry) => entry.value === value);
- }
-
- mergeWith(entry: StolenDataEntry) {
- this.entries.push(entry);
- return this;
- }
-
- getPriority() {
- return Math.max(...this.entries.map((entry) => entry.getPriority()));
- }
getUniqueKey() {
- return `${this.getNames().join(":")};${this.entries
- .map((e) => e.id)
- .join(":")};`;
- }
-
- getNames(): string[] {
- return unique(this.entries.map((e) => e.name));
- }
-
- getSources(): string[] {
- return unique(this.entries.map((e) => e.source));
- }
-
- getValues() {
- return unique(this.entries.map((e) => e.value));
- }
-
- getParsedValues(key_path: string) {
- return Array.from(
- new Set(this.entries.map((e) => e.getParsedValue(key_path)))
- );
- }
-
- addMark(key: string) {
- this.entries.forEach((entry) => entry.addMark(key));
- }
-
- getMarkedValues() {
- return this.entries
- .map((entry) => entry.marks)
- .reduce((a, b) => a.concat(b), []);
- }
-
- hasMark(key: string): boolean {
- return this.entries.some((entry) => entry.hasMark(key));
- }
-
- toggleMark(key: string): void {
- this.entries.forEach((entry) => entry.toggleMark(key));
- }
-
- getDecodingsApplied(): DecodingSchema[] {
- return unique(this.entries.map((entry) => entry.decoding_applied));
+ return this.request.shorthost + ";" + this.name + ";" + this.value;
}
}
diff --git a/test.ts b/test.ts
new file mode 100644
index 0000000..0efadd7
--- /dev/null
+++ b/test.ts
@@ -0,0 +1,3 @@
+import { flattenObject } from "./util";
+
+console.log(flattenObject({ a: { b: { c: [1, 2, 3] } } }));
diff --git a/util.ts b/util.ts
index 07b0870..f7ffc96 100644
--- a/util.ts
+++ b/util.ts
@@ -183,3 +183,31 @@ export function isBase64(s: string): boolean {
export function isBase64JSON(s: unknown): s is string {
return typeof s === "string" && isBase64(s) && isJSONObject(atob(s));
}
+
+export function flattenObject(
+ obj: Record
+): [string, string][] {
+ const ret: [string, string][] = [];
+ for (const [key, value] of Object.entries(obj)) {
+ const value = obj[key];
+ if (value === null) {
+ ret.push([key, "null"]);
+ continue;
+ }
+ if (typeof value === "object") {
+ const flattened = flattenObject(value as Record);
+ for (const [subkey, subvalue] of flattened) {
+ ret.push([`${key}.${subkey}`, subvalue]);
+ }
+ } else {
+ ret.push([key, value.toString()]);
+ }
+ }
+ return ret;
+}
+
+export function flattenObjectEntries(
+ entries: [string, unknown][]
+): [string, string][] {
+ return flattenObject(Object.fromEntries(entries));
+}