typecheck fixes checkpoint
This commit is contained in:
parent
c86a3e2c23
commit
e1d97f0411
@ -39,10 +39,12 @@ export default function EmailContent({
|
||||
function copyTextToClipboard() {
|
||||
// Should be changed in the future to Clipboard API (https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write#browser_compatibility)
|
||||
let r = document.createRange();
|
||||
r.selectNode(document.querySelector('.mail-container__content'));
|
||||
window.getSelection().addRange(r);
|
||||
const container = document.querySelector('.mail-container__content');
|
||||
if (!container) return;
|
||||
r.selectNode(container);
|
||||
window.getSelection()?.addRange(r);
|
||||
document.execCommand('copy');
|
||||
window.getSelection().removeAllRanges();
|
||||
window.getSelection()?.removeAllRanges();
|
||||
setCopy(true);
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,10 @@ function generateHostPage(
|
||||
defaultValueExpression:
|
||||
index == 0
|
||||
? 'marketing'
|
||||
: `{${f('legitimate_interest_description', previous_cluster)}}`,
|
||||
: `{${f(
|
||||
'legitimate_interest_description',
|
||||
previous_cluster || undefined
|
||||
)}}`,
|
||||
},
|
||||
{
|
||||
type: 'radiogroup',
|
||||
|
@ -16,7 +16,9 @@ function handleNewFile(
|
||||
);
|
||||
setFiltered(new Blob([JSON.stringify(content)], { type: 'application/json' }));
|
||||
});
|
||||
reader.readAsText(element.files[0]);
|
||||
const file = element?.files?.[0];
|
||||
if (!file) throw new Error('file empty?');
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
function generateFakeHAR(entries: StolenDataEntry[]) {
|
||||
@ -80,8 +82,11 @@ export default function HARConverter({ entries }: { entries: StolenDataEntry[] }
|
||||
type="file"
|
||||
accept=".har"
|
||||
onChange={(e) => {
|
||||
setFilename(e.target.files[0].name);
|
||||
handleNewFile(e.target, entries, setFiltered);
|
||||
const file = e.target?.files?.[0];
|
||||
if (file) {
|
||||
setFilename(file.name);
|
||||
handleNewFile(e.target, entries, setFiltered);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{(filtered && (
|
||||
|
@ -1,36 +0,0 @@
|
||||
import { RequestCluster } from '../../../request-cluster';
|
||||
import { ExplainerKey } from '../explainers';
|
||||
import { ParsedAnswers, ParsedHostAnswers } from '../parse-answers';
|
||||
import { v } from '../verbs';
|
||||
import { Problem } from './problem';
|
||||
|
||||
type UnlawfulDataClassification = 'no_purpose';
|
||||
|
||||
export function classifyUnlawfulData(
|
||||
hostAnswers: ParsedHostAnswers,
|
||||
cluster: RequestCluster
|
||||
): UnlawfulDataClassification {
|
||||
if (hostAnswers.present == 'not_mentioned' && hostAnswers.was_processing_necessary == 'no') {
|
||||
return 'no_purpose';
|
||||
}
|
||||
}
|
||||
|
||||
export class UnlawfulData extends Problem {
|
||||
static qualifies(answers: ParsedAnswers, clusters: RequestCluster[]): boolean {}
|
||||
getEmailContent() {
|
||||
const _ = (key: string) => v(key, this.answers.zaimek);
|
||||
return (
|
||||
<>
|
||||
<h2>Przetwarzanie danych osobowych bez ważnej podsawy prawnej</h2>
|
||||
<p>
|
||||
{_('Moje')} dane osobowe zostały ujawnione podmiotom, które są właścicielami
|
||||
domen:
|
||||
</p>
|
||||
{this.getRangeDescription()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
getNecessaryExplainers() {
|
||||
return [] as ExplainerKey[];
|
||||
}
|
||||
}
|
@ -22,15 +22,17 @@ function Report() {
|
||||
const url = new URL(document.location.toString());
|
||||
const origin = url.searchParams.get('origin');
|
||||
const [counter] = useEmitter(getMemory());
|
||||
const rawAnswers = url.searchParams.get('answers');
|
||||
const [answers, setAnswers] = React.useState<ParsedAnswers>(
|
||||
url.searchParams.get('answers') ? JSON.parse(url.searchParams.get('answers')) : null
|
||||
rawAnswers ? JSON.parse(rawAnswers) : null
|
||||
);
|
||||
const [mode, setMode] = React.useState(url.searchParams.get('mode') || 'survey');
|
||||
const [scrRequestPath, setScrRequestPath] = React.useState('');
|
||||
|
||||
const clusters = getMemory().getClustersForOrigin(origin);
|
||||
const clusters = getMemory().getClustersForOrigin(origin || '');
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!origin) return;
|
||||
const url = new URL(document.location.toString());
|
||||
url.searchParams.set('origin', origin);
|
||||
url.searchParams.set('answers', JSON.stringify(answers));
|
||||
@ -38,8 +40,12 @@ function Report() {
|
||||
history.pushState({}, 'Rentgen', url.toString());
|
||||
}, [mode, answers, origin]);
|
||||
const visited_url = Object.values(clusters)
|
||||
.find((cluster) => cluster.getMarkedRequests().length > 0)
|
||||
?.getMarkedRequests()[0].originalURL;
|
||||
.sort((clusterA, clusterB) => (clusterA.lastModified > clusterB.lastModified ? -1 : 1))
|
||||
.find((cluster) => !!cluster.lastFullUrl)?.lastFullUrl;
|
||||
|
||||
if (!visited_url) {
|
||||
return <div>Wczytywanie...</div>;
|
||||
}
|
||||
|
||||
const result = (
|
||||
<div {...{ 'data-version': counter }}>
|
||||
|
@ -66,7 +66,7 @@ export default function ScreenshotGenerator({
|
||||
}) {
|
||||
const [mode, setMode] = React.useState<string>('idle');
|
||||
const [images, setImages] = React.useState<Screenshot[]>([]);
|
||||
const [taskId, setTaskId] = React.useState<string>(null);
|
||||
const [taskId, setTaskId] = React.useState<string | null>(null);
|
||||
const [output, setOutput] = React.useState<any>({});
|
||||
|
||||
async function subscribeTask(path: string): Promise<screenshotTask> {
|
||||
@ -75,8 +75,8 @@ export default function ScreenshotGenerator({
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
response = await (await pollTask(path)).json();
|
||||
setImages((response as screenshotTask)?.images);
|
||||
document.querySelector('.images').scrollTo({
|
||||
top: document.querySelector('.images').scrollHeight,
|
||||
document.querySelector('.images')?.scrollTo({
|
||||
top: document.querySelector('.images')?.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
|
@ -8,8 +8,8 @@ import verbs, { v } from './verbs';
|
||||
export default function useSurvey(
|
||||
clusters: RequestCluster[],
|
||||
{ onComplete }: { onComplete: (sender: { data: RawAnswers }) => void }
|
||||
): Survey.ReactSurveyModel {
|
||||
const [survey, setSurvey] = React.useState<Survey.Model>(null);
|
||||
): Survey.ReactSurveyModel | null {
|
||||
const [survey, setSurvey] = React.useState<Survey.Model | null>(null);
|
||||
React.useEffect(() => {
|
||||
const model = generateSurveyQuestions(clusters);
|
||||
const survey = new Survey.Model(model);
|
||||
|
@ -1,20 +1,11 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Options from '../../options';
|
||||
import { StolenData } from './stolen-data';
|
||||
import { getshorthost, useEmitter } from '../../util';
|
||||
import { getMemory } from '../../memory';
|
||||
|
||||
async function getCurrentTab() {
|
||||
const [tab] = await browser.tabs.query({
|
||||
active: true,
|
||||
windowId: browser.windows.WINDOW_ID_CURRENT,
|
||||
});
|
||||
return tab;
|
||||
}
|
||||
|
||||
import Options from '../../options';
|
||||
import { useEmitter } from '../../util';
|
||||
import './../../styles/global.scss';
|
||||
import './sidebar.scss';
|
||||
import { StolenData } from './stolen-data';
|
||||
|
||||
const Sidebar = () => {
|
||||
const url = new URL(document.location.toString());
|
||||
@ -28,8 +19,8 @@ const Sidebar = () => {
|
||||
const [cookiesOnly, setCookiesOnly] = React.useState<boolean>(false);
|
||||
const [stolenDataView, setStolenDataView] = React.useState<boolean>(true);
|
||||
const [cookiesOrOriginOnly, setCookiesOrOriginOnly] = React.useState<boolean>(false);
|
||||
const [eventCounts, setEventCounts] = useEmitter(getMemory());
|
||||
const [marksOccurrence, setMarksOccurrence] = React.useState<boolean>(false);
|
||||
const [eventCounts] = useEmitter(getMemory());
|
||||
const [_, setMarksOccurrence] = React.useState<boolean>(false);
|
||||
const [infoDataDialogAck, setInfoDataDialogAck] = React.useState<boolean>(
|
||||
localStorage.getItem('infoDataDialogAck') === null
|
||||
? true
|
||||
@ -53,6 +44,7 @@ const Sidebar = () => {
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!origin) return;
|
||||
for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) {
|
||||
if (cluster.hasMarks()) {
|
||||
return setMarksOccurrence(true);
|
||||
@ -62,6 +54,7 @@ const Sidebar = () => {
|
||||
return setMarksOccurrence(false);
|
||||
}, [eventCounts['*']]);
|
||||
|
||||
if (!origin) return <div>Błąd: Brak parametru "origin"</div>;
|
||||
return (
|
||||
<div className="sidebar">
|
||||
<header className="header">
|
||||
@ -204,7 +197,7 @@ const Sidebar = () => {
|
||||
<StolenData
|
||||
origin={origin}
|
||||
eventCounts={eventCounts}
|
||||
minValueLength={minValueLength}
|
||||
minValueLength={minValueLength === null ? 7 : minValueLength}
|
||||
cookiesOnly={cookiesOnly}
|
||||
cookiesOrOriginOnly={cookiesOrOriginOnly}
|
||||
detailsVisibility={detailsVisibility}
|
||||
@ -212,7 +205,7 @@ const Sidebar = () => {
|
||||
</>
|
||||
) : (
|
||||
<Options
|
||||
minValueLength={minValueLength}
|
||||
minValueLength={minValueLength === null ? 7 : minValueLength}
|
||||
setMinValueLength={setMinValueLength}
|
||||
cookiesOnly={cookiesOnly}
|
||||
setCookiesOnly={setCookiesOnly}
|
||||
|
@ -14,7 +14,7 @@ export function StolenData({
|
||||
detailsVisibility,
|
||||
}: {
|
||||
origin: string;
|
||||
eventCounts: Record<string, number>;
|
||||
eventCounts: Record<string, number | undefined>;
|
||||
minValueLength: number;
|
||||
cookiesOnly: boolean;
|
||||
cookiesOrOriginOnly: boolean;
|
||||
@ -43,7 +43,7 @@ export function StolenData({
|
||||
origin={origin}
|
||||
shorthost={cluster.id}
|
||||
key={cluster.id + origin}
|
||||
refreshToken={eventCounts[cluster.id]}
|
||||
refreshToken={eventCounts[cluster.id] || 0}
|
||||
minValueLength={minValueLength}
|
||||
cookiesOnly={cookiesOnly}
|
||||
cookiesOrOriginOnly={cookiesOrOriginOnly}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Tab } from '../../util';
|
||||
|
||||
export default function TabDropdown({
|
||||
setPickedTab,
|
||||
@ -7,7 +8,7 @@ export default function TabDropdown({
|
||||
setPickedTab: (tab_id: number) => void;
|
||||
pickedTab: number;
|
||||
}) {
|
||||
const [tabs, setTabs] = React.useState([]);
|
||||
const [tabs, setTabs] = React.useState<Tab[]>([]);
|
||||
React.useEffect(() => {
|
||||
browser.tabs.query({ currentWindow: true }).then(setTabs);
|
||||
}, []);
|
||||
|
@ -16,10 +16,9 @@ import './toolbar.scss';
|
||||
|
||||
const Toolbar = () => {
|
||||
const [origin, setOrigin] = React.useState<string | null>(null);
|
||||
const [stolenDataView, setStolenDataView] = React.useState<boolean>(true);
|
||||
const [eventCounts, setEventCounts] = useEmitter(getMemory());
|
||||
const [eventCounts] = useEmitter(getMemory());
|
||||
const [cookieDomainCopy, setCookieDomainCopy] = React.useState<string | null>(null);
|
||||
const [marksOccurrence, setMarksOccurrence] = React.useState<boolean>(false);
|
||||
const [_, setMarksOccurrence] = React.useState<boolean>(false);
|
||||
const [exposedOriginDomainCopy, setExposedOriginDomainCopy] = React.useState<string | null>(
|
||||
null
|
||||
);
|
||||
@ -33,6 +32,7 @@ const Toolbar = () => {
|
||||
const tab = await getCurrentTab();
|
||||
|
||||
if (tab !== undefined) {
|
||||
if (!tab.url) return;
|
||||
const url = new URL(tab.url);
|
||||
if (url.origin.startsWith('moz-extension')) {
|
||||
return;
|
||||
@ -58,23 +58,27 @@ const Toolbar = () => {
|
||||
|
||||
switch (exposedOriginDomains.length) {
|
||||
case 0:
|
||||
return null;
|
||||
break;
|
||||
case 1:
|
||||
return setExposedOriginDomainCopy(`${exposedOriginDomains[0]}.`);
|
||||
setExposedOriginDomainCopy(`${exposedOriginDomains[0]}.`);
|
||||
break;
|
||||
case 2:
|
||||
return setExposedOriginDomainCopy(
|
||||
setExposedOriginDomainCopy(
|
||||
`${exposedOriginDomains[0]} oraz ${exposedOriginDomains[1]}.`
|
||||
);
|
||||
break;
|
||||
case 3:
|
||||
return setExposedOriginDomainCopy(
|
||||
setExposedOriginDomainCopy(
|
||||
`${exposedOriginDomains[0]}, ${exposedOriginDomains[1]} oraz ${exposedOriginDomains[2]}.`
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return setExposedOriginDomainCopy(
|
||||
setExposedOriginDomainCopy(
|
||||
`${exposedOriginDomains[0]}, ${exposedOriginDomains[1]} (i ${
|
||||
exposedOriginDomains.length - 2 < 2 ? 2 : exposedOriginDomains.length - 2
|
||||
} innych).`
|
||||
);
|
||||
break;
|
||||
}
|
||||
}, [eventCounts['*'], origin]);
|
||||
|
||||
@ -86,25 +90,30 @@ const Toolbar = () => {
|
||||
|
||||
switch (cookieDomains.length) {
|
||||
case 0:
|
||||
return null;
|
||||
null;
|
||||
case 1:
|
||||
return setCookieDomainCopy(`${cookieDomains[0]}.`);
|
||||
setCookieDomainCopy(`${cookieDomains[0]}.`);
|
||||
break;
|
||||
case 2:
|
||||
return setCookieDomainCopy(`${cookieDomains[0]} oraz ${cookieDomains[1]}.`);
|
||||
setCookieDomainCopy(`${cookieDomains[0]} oraz ${cookieDomains[1]}.`);
|
||||
break;
|
||||
case 3:
|
||||
return setCookieDomainCopy(
|
||||
setCookieDomainCopy(
|
||||
`${cookieDomains[0]}, ${cookieDomains[1]} oraz ${cookieDomains[2]}.`
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return setCookieDomainCopy(
|
||||
setCookieDomainCopy(
|
||||
`${cookieDomains[0]}, ${cookieDomains[1]} (i ${
|
||||
cookieDomains.length - 2 < 2 ? 2 : cookieDomains.length - 2
|
||||
} innych).`
|
||||
);
|
||||
break;
|
||||
}
|
||||
}, [eventCounts['*'], origin]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!origin) return;
|
||||
for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) {
|
||||
if (cluster.hasMarks()) {
|
||||
return setMarksOccurrence(true);
|
||||
@ -115,12 +124,17 @@ const Toolbar = () => {
|
||||
}, [eventCounts['*']]);
|
||||
|
||||
function autoMark() {
|
||||
if (!origin) return;
|
||||
for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) {
|
||||
cluster.autoMark();
|
||||
}
|
||||
return setMarksOccurrence(true);
|
||||
}
|
||||
|
||||
if (!origin) {
|
||||
return <div>Wczytywanie...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="toolbar">
|
||||
<header className="header">
|
||||
|
@ -75,12 +75,12 @@ export default class ExtendedRequest {
|
||||
public tabId: number;
|
||||
public url: string;
|
||||
public shorthost: string;
|
||||
public requestHeaders: Request['requestHeaders'] = [];
|
||||
public originalURL: string;
|
||||
public requestHeaders: { name: string; value?: string; binaryValue?: number[] }[] = [];
|
||||
public originalURL: string | null = null;
|
||||
public origin: string;
|
||||
public initialized = false;
|
||||
public stolenData: StolenDataEntry[];
|
||||
public originalPathname: string;
|
||||
public stolenData: StolenDataEntry[] = [];
|
||||
public originalPathname: string | null = null;
|
||||
public requestBody: RequestBody;
|
||||
|
||||
static by_id = {} as Record<string, ExtendedRequest>;
|
||||
@ -97,20 +97,21 @@ export default class ExtendedRequest {
|
||||
(this.data as any).frameAncestors = [
|
||||
...(data as any).frameAncestors.map((e: any) => ({ url: e.url })),
|
||||
];
|
||||
this.origin = this.cacheOrigin();
|
||||
}
|
||||
|
||||
addHeaders(headers: Request['requestHeaders']) {
|
||||
this.requestHeaders = headers;
|
||||
this.requestHeaders = headers || [];
|
||||
return this;
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.cacheOrigin();
|
||||
init() {
|
||||
this.cacheOrigin();
|
||||
this.initialized = true;
|
||||
this.stolenData = this.getAllStolenData();
|
||||
}
|
||||
|
||||
async cacheOrigin(): Promise<void> {
|
||||
cacheOrigin(): string {
|
||||
let url: string;
|
||||
if (this.data.type === 'main_frame') {
|
||||
url = this.data.url;
|
||||
@ -135,6 +136,7 @@ export default class ExtendedRequest {
|
||||
this.originalURL = url;
|
||||
this.origin = new URL(url).origin;
|
||||
this.originalPathname = new URL(url).pathname;
|
||||
return this.origin;
|
||||
}
|
||||
|
||||
isThirdParty() {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import ExtendedRequest from './extended-request';
|
||||
import { SaferEmitter } from './safer-emitter';
|
||||
import { Sources, StolenDataEntry } from './stolen-data-entry';
|
||||
@ -11,12 +10,18 @@ export class RequestCluster extends SaferEmitter {
|
||||
public requests: ExtendedRequest[] = [];
|
||||
public representativeStolenData: StolenDataEntry[] = [];
|
||||
public expanded: boolean;
|
||||
public lastModified: number = 0;
|
||||
public lastFullUrl: string | null = null;
|
||||
constructor(public id: string) {
|
||||
super();
|
||||
}
|
||||
add(request: ExtendedRequest) {
|
||||
this.requests.push(request);
|
||||
this.emit('change');
|
||||
this.lastModified = Date.now();
|
||||
if (request.originalURL) {
|
||||
this.lastFullUrl = request.originalURL;
|
||||
}
|
||||
}
|
||||
|
||||
toggleExpanded(state: boolean) {
|
||||
|
@ -8,6 +8,8 @@
|
||||
"typeRoots": ["node_modules/@types", "node_modules/web-ext-types"],
|
||||
"target": "es2019",
|
||||
"outDir": "lib",
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
|
23
util.ts
23
util.ts
@ -36,7 +36,11 @@ export function getshorthost(host: string) {
|
||||
.replace(/^.*:\/\//, '')
|
||||
.replace(/\/.*$/, '')
|
||||
.split('.');
|
||||
let lookback = !['co', 'com'].includes(parts.at(-2)) ? -2 : -3;
|
||||
const second_last = parts.at(-2);
|
||||
if (!second_last) {
|
||||
throw new Error('url too short?');
|
||||
}
|
||||
let lookback = !['co', 'com'].includes(second_last) ? -2 : -3;
|
||||
if (parts.at(-2) == 'doubleclick' || parts.at(-2) == 'google') {
|
||||
lookback = -4; // to distinguish between google ads and stats
|
||||
} else if (parts.at(-2) == 'google') {
|
||||
@ -89,7 +93,7 @@ export async function getTabByID(id: number) {
|
||||
}
|
||||
|
||||
export function parseToObject(str: unknown): Record<string | symbol, unknown> {
|
||||
let result: Record<string | symbol, unknown>;
|
||||
let result: Record<string | symbol, unknown> = {};
|
||||
let original_string: string;
|
||||
if (typeof str === 'string') {
|
||||
original_string = str;
|
||||
@ -97,6 +101,8 @@ export function parseToObject(str: unknown): Record<string | symbol, unknown> {
|
||||
} else if (typeof str == 'object') {
|
||||
result = str as Record<string | symbol, unknown>;
|
||||
original_string = (result[Symbol.for('originalString')] as string) || JSON.stringify(str);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
result[Symbol.for('originalString')] = original_string;
|
||||
return result;
|
||||
@ -149,10 +155,11 @@ export function getDate() {
|
||||
}
|
||||
|
||||
export function toBase64(file: File): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const FR = new FileReader();
|
||||
FR.addEventListener('load', (e) => {
|
||||
resolve(e.target.result as string);
|
||||
const target = e.target;
|
||||
target ? resolve(target.result as string) : reject('empty file?');
|
||||
});
|
||||
FR.readAsDataURL(file);
|
||||
});
|
||||
@ -199,7 +206,8 @@ export function isBase64JSON(s: unknown): s is string {
|
||||
|
||||
export function flattenObject(
|
||||
obj: unknown,
|
||||
parser: (to_parse: unknown) => string | Record<string, unknown> = (id) => id.toString(),
|
||||
parser: (to_parse: { toString: () => string }) => string | Record<string, unknown> = (id) =>
|
||||
id.toString(),
|
||||
key = '',
|
||||
ret = [] as [string, string][],
|
||||
parsed = false
|
||||
@ -220,7 +228,7 @@ export function flattenObject(
|
||||
flattenObject(value, parser, prefix + subkey, ret);
|
||||
}
|
||||
} else if (!parsed) {
|
||||
flattenObject(parser(obj), parser, key, ret, true);
|
||||
flattenObject(parser(obj as { toString: () => string }), parser, key, ret, true);
|
||||
} else if (typeof obj === 'string') {
|
||||
ret.push([key, obj]);
|
||||
} else {
|
||||
@ -231,7 +239,8 @@ export function flattenObject(
|
||||
|
||||
export function flattenObjectEntries(
|
||||
entries: [string, unknown][],
|
||||
parser: (to_parse: unknown) => string | Record<string, unknown> = (id) => id.toString()
|
||||
parser: (to_parse: { toString: () => string }) => string | Record<string, unknown> = (id) =>
|
||||
id.toString()
|
||||
): [string, string][] {
|
||||
return flattenObject(Object.fromEntries(entries), parser);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user