gotosocial/web/source/settings/lib/query/admin/domain-permissions/process.ts

185 lines
5.0 KiB
TypeScript

/*
GoToSocial
Copyright (C) GoToSocial Authors admin@gotosocial.org
SPDX-License-Identifier: AGPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
ParseConfig as CSVParseConfig,
parse as csvParse
} from "papaparse";
import { nanoid } from "nanoid";
import { isValidDomainPermission, hasBetterScope } from "../../../util/domain-permission";
import { gtsApi } from "../../gts-api";
import {
validateDomainPerms,
type DomainPerm,
} from "../../../types/domain-permission";
/**
* Parse the given string of domain permissions and return it as an array.
* Accepts input as a JSON array string, a CSV, or newline-separated domain names.
* Will throw an error if input is invalid.
* @param list
* @returns
* @throws
*/
function parseDomainList(list: string): DomainPerm[] {
if (list.startsWith("[")) {
// Assume JSON array.
const data = JSON.parse(list);
const validateRes = validateDomainPerms(data);
if (!validateRes.success) {
throw `parsed JSON was not array of DomainPermission: ${JSON.stringify(validateRes.errors)}`;
}
return data;
} else if (list.startsWith("#domain") || list.startsWith("domain,severity")) {
// Assume Mastodon-style CSV.
const csvParseCfg: CSVParseConfig = {
// Key by header.
header: true,
// Remove leading '#' from headers if present.
transformHeader: (header) => header.startsWith("#") ? header.slice(1) : header,
// Massage weird boolean values.
transform: (value, _field) => {
if (value == "False" || value == "True") {
return value.toLowerCase();
} else {
return value;
}
},
skipEmptyLines: true,
// Only dynamic type boolean values,
// leave the rest as strings.
dynamicTyping: {
"domain": false,
"severity": false,
"reject_media": true,
"reject_reports": true,
"public_comment": false,
"obfuscate": true,
}
};
const { data, errors } = csvParse(list, csvParseCfg);
if (errors.length > 0) {
let error = "";
errors.forEach((err) => {
error += `${err.message} (line ${err.row})`;
});
throw error;
}
const validateRes = validateDomainPerms(data);
if (!validateRes.success) {
throw `parsed CSV was not array of DomainPermission: ${JSON.stringify(validateRes.errors)}`;
}
return data;
} else {
// Fallback: assume newline-separated
// list of simple domain strings.
const data: DomainPerm[] = [];
list.split("\n").forEach((line) => {
let domain = line.trim();
let valid = true;
if (domain.startsWith("http")) {
try {
domain = new URL(domain).hostname;
} catch (e) {
valid = false;
}
}
if (domain.length > 0) {
data.push({ domain, valid });
}
});
return data;
}
}
function deduplicateDomainList(list: DomainPerm[]): DomainPerm[] {
let domains = new Set();
return list.filter((entry) => {
if (domains.has(entry.domain)) {
return false;
} else {
domains.add(entry.domain);
return true;
}
});
}
function validateDomainList(list: DomainPerm[]) {
list.forEach((entry) => {
if (entry.domain.startsWith("*.")) {
// A domain permission always includes
// all subdomains, wildcard is meaningless here
entry.domain = entry.domain.slice(2);
}
entry.valid = (entry.valid !== false) && isValidDomainPermission(entry.domain);
if (entry.valid) {
entry.suggest = hasBetterScope(entry.domain);
}
entry.checked = entry.valid;
});
return list;
}
const extended = gtsApi.injectEndpoints({
endpoints: (build) => ({
processDomainPermissions: build.mutation<DomainPerm[], any>({
async queryFn(formData, _api, _extraOpts, _fetchWithBQ) {
if (formData.domains == undefined || formData.domains.length == 0) {
throw "No domains entered";
}
// Parse + tidy up the form data.
const permissions = parseDomainList(formData.domains);
const deduped = deduplicateDomainList(permissions);
const validated = validateDomainList(deduped);
validated.forEach((entry) => {
// Set unique key that stays stable
// even if domain gets modified by user.
entry.key = nanoid();
});
return { data: validated };
}
})
})
});
/**
* useProcessDomainPermissionsMutation uses the RTK Query API without actually
* hitting the GtS API, it's purely an internal function for our own convenience.
*
* It returns the validated and deduplicated domain permission list.
*/
const useProcessDomainPermissionsMutation = extended.useProcessDomainPermissionsMutation;
export { useProcessDomainPermissionsMutation };