From 7112caaedd80cd9bbb1134f5979bb755d0d92d43 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 11 Jun 2025 19:07:45 -0300 Subject: [PATCH 1/5] Experiments: Fix toolbar help menu dev icon patch --- src/plugins/experiments/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/experiments/index.tsx b/src/plugins/experiments/index.tsx index 2482d051..967197ad 100644 --- a/src/plugins/experiments/index.tsx +++ b/src/plugins/experiments/index.tsx @@ -81,7 +81,7 @@ export default definePlugin({ }, // Change top right chat toolbar button from the help one to the dev one { - find: ".CONTEXTLESS,isActivityPanelMode:", + find: '"M9 3v18"', replacement: { match: /hasBugReporterAccess:(\i)/, replace: "_hasBugReporterAccess:$1=true" From 7f2c4a35660002cbf31df72ad43bff67b7026fc0 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 11 Jun 2025 19:10:14 -0300 Subject: [PATCH 2/5] Delete Banger plugin ~ Ban modal no longer contains a gif Discord removed the gifs displayed in the ban modal, so this plugin serves no purpose anymore. --- src/plugins/banger/index.ts | 49 ------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 src/plugins/banger/index.ts diff --git a/src/plugins/banger/index.ts b/src/plugins/banger/index.ts deleted file mode 100644 index eed0e1b4..00000000 --- a/src/plugins/banger/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; - -const settings = definePluginSettings({ - source: { - description: "Source to replace ban GIF with (Video or Gif)", - type: OptionType.STRING, - default: "https://i.imgur.com/wp5q52C.mp4", - restartNeeded: true, - } -}); - -export default definePlugin({ - name: "BANger", - description: "Replaces the GIF in the ban dialogue with a custom one.", - authors: [Devs.Xinto, Devs.Glitch], - settings, - patches: [ - { - find: "#{intl::jeKpoq::raw}", // BAN_CONFIRM_TITLE - replacement: { - match: /src:\i\("?\d+"?\)/g, - replace: "src:$self.source" - } - } - ], - get source() { - return settings.store.source; - } -}); From ed5ed4b80a7b3a50858eefef083b385449147486 Mon Sep 17 00:00:00 2001 From: Vending Machine Date: Thu, 12 Jun 2025 02:19:45 +0200 Subject: [PATCH 3/5] Allow users to manually whitelist Domains for use in themes (#3476) --- browser/VencordNativeStub.ts | 1 + src/VencordNative.ts | 13 ++ src/components/VencordSettings/CloudTab.tsx | 4 +- src/components/VencordSettings/ThemesTab.tsx | 69 ++++++++-- .../VencordSettings/themesStyles.css | 23 ++++ src/main/{csp.ts => csp/index.ts} | 64 +++++---- src/main/csp/manager.ts | 125 ++++++++++++++++++ src/main/ipcMain.ts | 3 + src/main/settings.ts | 6 +- src/shared/IpcEvents.ts | 4 + src/utils/cloud.tsx | 39 +++++- src/utils/settingsSync.ts | 8 +- 12 files changed, 313 insertions(+), 46 deletions(-) rename src/main/{csp.ts => csp/index.ts} (69%) create mode 100644 src/main/csp/manager.ts diff --git a/browser/VencordNativeStub.ts b/browser/VencordNativeStub.ts index 9080f644..8cb3ff29 100644 --- a/browser/VencordNativeStub.ts +++ b/browser/VencordNativeStub.ts @@ -115,4 +115,5 @@ window.VencordNative = { }, pluginHelpers: {} as any, + csp: {} as any, }; diff --git a/src/VencordNative.ts b/src/VencordNative.ts index 3bed5a59..faf9b052 100644 --- a/src/VencordNative.ts +++ b/src/VencordNative.ts @@ -5,6 +5,7 @@ */ import type { Settings } from "@api/Settings"; +import { CspRequestResult } from "@main/csp/manager"; import { PluginIpcMappings } from "@main/ipcPlugins"; import type { UserThemeHeader } from "@main/themes"; import { IpcEvents } from "@shared/IpcEvents"; @@ -73,5 +74,17 @@ export default { openExternal: (url: string) => invoke(IpcEvents.OPEN_EXTERNAL, url) }, + csp: { + /** + * Note: Only supports full explicit matches, not wildcards. + * + * If `*.example.com` is allowed, `isDomainAllowed("https://sub.example.com")` will return false. + */ + isDomainAllowed: (url: string, directives: string[]) => invoke(IpcEvents.CSP_IS_DOMAIN_ALLOWED, url, directives), + removeOverride: (url: string) => invoke(IpcEvents.CSP_REMOVE_OVERRIDE, url), + requestAddOverride: (url: string, directives: string[], callerName: string) => + invoke(IpcEvents.CSP_REQUEST_ADD_OVERRIDE, url, directives, callerName), + }, + pluginHelpers: PluginHelpers }; diff --git a/src/components/VencordSettings/CloudTab.tsx b/src/components/VencordSettings/CloudTab.tsx index a13c3f6c..809d4062 100644 --- a/src/components/VencordSettings/CloudTab.tsx +++ b/src/components/VencordSettings/CloudTab.tsx @@ -21,7 +21,7 @@ import { Settings, useSettings } from "@api/Settings"; import { CheckedTextInput } from "@components/CheckedTextInput"; import { Grid } from "@components/Grid"; import { Link } from "@components/Link"; -import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud"; +import { authorizeCloud, checkCloudUrlCsp, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud"; import { Margins } from "@utils/margins"; import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync"; import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common"; @@ -38,6 +38,8 @@ function validateUrl(url: string) { } async function eraseAllData() { + if (!await checkCloudUrlCsp()) return; + const res = await fetch(new URL("/v1/", getCloudUrl()), { method: "DELETE", headers: { Authorization: await getCloudAuth() } diff --git a/src/components/VencordSettings/ThemesTab.tsx b/src/components/VencordSettings/ThemesTab.tsx index 0c66ec5d..ceb59898 100644 --- a/src/components/VencordSettings/ThemesTab.tsx +++ b/src/components/VencordSettings/ThemesTab.tsx @@ -24,15 +24,15 @@ import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIc import { Link } from "@components/Link"; import { openPluginModal } from "@components/PluginSettings/PluginModal"; import type { UserThemeHeader } from "@main/themes"; -import { useCspErrors } from "@utils/cspViolations"; +import { CspBlockedUrls, useCspErrors } from "@utils/cspViolations"; import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; -import { showItemInFolder } from "@utils/native"; -import { useAwaiter } from "@utils/react"; +import { relaunch, showItemInFolder } from "@utils/native"; +import { useAwaiter, useForceUpdater } from "@utils/react"; import { getStylusWebStoreUrl } from "@utils/web"; import { findLazy } from "@webpack"; -import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common"; +import { Alerts, Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common"; import type { ComponentType, Ref, SyntheticEvent } from "react"; import Plugins from "~plugins"; @@ -365,22 +365,73 @@ function ThemesTab() { } export function CspErrorCard() { + if (IS_WEB) return null; + const errors = useCspErrors(); + const forceUpdate = useForceUpdater(); if (!errors.length) return null; + const isImgurHtmlDomain = (url: string) => url.startsWith("https://imgur.com/"); + + const allowUrl = async (url: string) => { + const { origin: baseUrl, hostname } = new URL(url); + + const result = await VencordNative.csp.requestAddOverride(baseUrl, ["connect-src", "img-src", "style-src", "font-src"], "Vencord Themes"); + if (result !== "ok") return; + + CspBlockedUrls.forEach(url => { + if (new URL(url).hostname === hostname) { + CspBlockedUrls.delete(url); + } + }); + + forceUpdate(); + + Alerts.show({ + title: "Restart Required", + body: "A restart is required to apply this change", + confirmText: "Restart now", + cancelText: "Later!", + onConfirm: relaunch + }); + }; + + const hasImgurHtmlDomain = errors.some(isImgurHtmlDomain); + return ( Blocked Resources Some images, styles, or fonts were blocked because they come from disallowed domains. - Make sure that your themes and custom css only load resources from whitelisted websites, such as GitHub, Imgur and Google Fonts. + It is highly recommended to move them to GitHub or Imgur. But you may also allow domains if you fully trust them. + + After allowing a domain, you have to fully close (from tray / task manager) and restart {IS_DISCORD_DESKTOP ? "Discord" : "Vesktop"} to apply the change. + Blocked URLs - - {errors.map(url => ( - {url} + + {errors.map((url, i) => ( + + {i !== 0 && } + + {url} + allowUrl(url)} disabled={isImgurHtmlDomain(url)}> + Allow + + + ))} - + + + {hasImgurHtmlDomain && ( + <> + + + Imgur links should be direct links in the form of https://i.imgur.com/... + + To obtain a direct link, right-click the image and select "Copy image address". + > + )} ); } diff --git a/src/components/VencordSettings/themesStyles.css b/src/components/VencordSettings/themesStyles.css index 6038274f..b10daff3 100644 --- a/src/components/VencordSettings/themesStyles.css +++ b/src/components/VencordSettings/themesStyles.css @@ -27,3 +27,26 @@ .vc-settings-theme-author::before { content: "by "; } + +.vc-settings-csp-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.vc-settings-csp-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + + + & a { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + line-height: 1.2em; + } + + --custom-button-button-md-height: 26px; +} diff --git a/src/main/csp.ts b/src/main/csp/index.ts similarity index 69% rename from src/main/csp.ts rename to src/main/csp/index.ts index 2faee606..fefbc774 100644 --- a/src/main/csp.ts +++ b/src/main/csp/index.ts @@ -4,59 +4,63 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { NativeSettings } from "@main/settings"; import { session } from "electron"; type PolicyMap = Record; export const ConnectSrc = ["connect-src"]; -export const MediaSrc = [...ConnectSrc, "img-src", "media-src"]; +export const ImageSrc = [...ConnectSrc, "img-src"]; export const CssSrc = ["style-src", "font-src"]; -export const MediaAndCssSrc = [...MediaSrc, ...CssSrc]; -export const MediaScriptsAndCssSrc = [...MediaAndCssSrc, "script-src", "worker-src"]; +export const ImageAndCssSrc = [...ImageSrc, ...CssSrc]; +export const ImageScriptsAndCssSrc = [...ImageAndCssSrc, "script-src", "worker-src"]; // Plugins can whitelist their own domains by importing this object in their native.ts // script and just adding to it. But generally, you should just edit this file instead export const CspPolicies: PolicyMap = { - "*.github.io": MediaAndCssSrc, // GitHub pages, used by most themes - "github.com": MediaAndCssSrc, // GitHub content (stuff uploaded to markdown forms), used by most themes - "raw.githubusercontent.com": MediaAndCssSrc, // GitHub raw, used by some themes - "*.gitlab.io": MediaAndCssSrc, // GitLab pages, used by some themes - "gitlab.com": MediaAndCssSrc, // GitLab raw, used by some themes - "*.codeberg.page": MediaAndCssSrc, // Codeberg pages, used by some themes - "codeberg.org": MediaAndCssSrc, // Codeberg raw, used by some themes + "localhost": ImageAndCssSrc, + "127.0.0.1": ImageAndCssSrc, - "*.githack.com": MediaAndCssSrc, // githack (namely raw.githack.com), used by some themes - "jsdelivr.net": MediaAndCssSrc, // jsDelivr, used by very few themes + "*.github.io": ImageAndCssSrc, // GitHub pages, used by most themes + "github.com": ImageAndCssSrc, // GitHub content (stuff uploaded to markdown forms), used by most themes + "raw.githubusercontent.com": ImageAndCssSrc, // GitHub raw, used by some themes + "*.gitlab.io": ImageAndCssSrc, // GitLab pages, used by some themes + "gitlab.com": ImageAndCssSrc, // GitLab raw, used by some themes + "*.codeberg.page": ImageAndCssSrc, // Codeberg pages, used by some themes + "codeberg.org": ImageAndCssSrc, // Codeberg raw, used by some themes + + "*.githack.com": ImageAndCssSrc, // githack (namely raw.githack.com), used by some themes + "jsdelivr.net": ImageAndCssSrc, // jsDelivr, used by very few themes "fonts.googleapis.com": CssSrc, // Google Fonts, used by many themes - "i.imgur.com": MediaSrc, // Imgur, used by some themes - "i.ibb.co": MediaSrc, // ImgBB, used by some themes - "i.pinimg.com": MediaSrc, // Pinterest, used by some themes - "*.tenor.com": MediaSrc, // Tenor, used by some themes - "files.catbox.moe": MediaSrc, // Catbox, used by some themes + "i.imgur.com": ImageSrc, // Imgur, used by some themes + "i.ibb.co": ImageSrc, // ImgBB, used by some themes + "i.pinimg.com": ImageSrc, // Pinterest, used by some themes + "*.tenor.com": ImageSrc, // Tenor, used by some themes + "files.catbox.moe": ImageAndCssSrc, // Catbox, used by some themes - "cdn.discordapp.com": MediaAndCssSrc, // Discord CDN, used by Vencord and some themes to load media - "media.discordapp.net": MediaSrc, // Discord media CDN, possible alternative to Discord CDN + "cdn.discordapp.com": ImageAndCssSrc, // Discord CDN, used by Vencord and some themes to load media + "media.discordapp.net": ImageSrc, // Discord media CDN, possible alternative to Discord CDN // CDNs used for some things by Vencord. // FIXME: we really should not be using CDNs anymore - "cdnjs.cloudflare.com": MediaScriptsAndCssSrc, - "cdn.jsdelivr.net": MediaScriptsAndCssSrc, + "cdnjs.cloudflare.com": ImageScriptsAndCssSrc, + "cdn.jsdelivr.net": ImageScriptsAndCssSrc, // Function Specific "api.github.com": ConnectSrc, // used for updating Vencord itself "ws.audioscrobbler.com": ConnectSrc, // Last.fm API "translate-pa.googleapis.com": ConnectSrc, // Google Translate API - "*.vencord.dev": MediaSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev) - "manti.vendicated.dev": MediaSrc, // ReviewDB API + "*.vencord.dev": ImageSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev) + "manti.vendicated.dev": ImageSrc, // ReviewDB API "decor.fieryflames.dev": ConnectSrc, // Decor API - "ugc.decor.fieryflames.dev": MediaSrc, // Decor CDN + "ugc.decor.fieryflames.dev": ImageSrc, // Decor CDN "sponsor.ajay.app": ConnectSrc, // Dearrow API - "dearrow-thumb.ajay.app": MediaSrc, // Dearrow Thumbnail CDN - "usrbg.is-hardly.online": MediaSrc, // USRBG API - "icons.duckduckgo.com": MediaSrc, // DuckDuckGo Favicon API (Reverse Image Search) + "dearrow-thumb.ajay.app": ImageSrc, // Dearrow Thumbnail CDN + "usrbg.is-hardly.online": ImageSrc, // USRBG API + "icons.duckduckgo.com": ImageSrc, // DuckDuckGo Favicon API (Reverse Image Search) }; const findHeader = (headers: PolicyMap, headerName: Lowercase) => { @@ -107,6 +111,12 @@ const patchCsp = (headers: PolicyMap) => { pushDirective(directive, "blob:", "data:", "vencord:"); } + for (const [host, directives] of Object.entries(NativeSettings.store.customCspRules)) { + for (const directive of directives) { + pushDirective(directive, host); + } + } + for (const [host, directives] of Object.entries(CspPolicies)) { for (const directive of directives) { pushDirective(directive, host); diff --git a/src/main/csp/manager.ts b/src/main/csp/manager.ts new file mode 100644 index 00000000..b8fbbea3 --- /dev/null +++ b/src/main/csp/manager.ts @@ -0,0 +1,125 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { NativeSettings } from "@main/settings"; +import { IpcEvents } from "@shared/IpcEvents"; +import { dialog, ipcMain, IpcMainInvokeEvent } from "electron"; + +import { CspPolicies, ImageAndCssSrc } from "."; + +export type CspRequestResult = "invalid" | "cancelled" | "unchecked" | "ok" | "conflict"; + +export function registerCspIpcHandlers() { + ipcMain.handle(IpcEvents.CSP_REMOVE_OVERRIDE, removeCspRule); + ipcMain.handle(IpcEvents.CSP_REQUEST_ADD_OVERRIDE, addCspRule); + ipcMain.handle(IpcEvents.CSP_IS_DOMAIN_ALLOWED, isDomainAllowed); +} + +function validate(url: string, directives: string[]) { + try { + const { hostname } = new URL(url); + + if (/[;'"\\]/.test(hostname)) return false; + } catch { + return false; + } + + if (directives.length === 0) return false; + if (directives.some(d => !ImageAndCssSrc.includes(d))) return false; + + return true; +} + +function getMessage(url: string, directives: string[], callerName: string) { + const domain = new URL(url).hostname; + + const message = `${callerName} wants to allow connections to ${domain}`; + + let detail = + `Unless you recognise and fully trust ${domain}, you should cancel this request!\n\n` + + `You will have to fully close and restart ${IS_DISCORD_DESKTOP ? "Discord" : "Vesktop"} for the changes to take effect.`; + + if (directives.length === 1 && directives[0] === "connect-src") { + return { message, detail }; + } + + const contentTypes = directives + .filter(type => type !== "connect-src") + .map(type => { + switch (type) { + case "img-src": + return "Images"; + case "style-src": + return "CSS & Themes"; + case "font-src": + return "Fonts"; + default: + throw new Error(`Illegal CSP directive: ${type}`); + } + }) + .sort() + .join(", "); + + detail = `The following types of content will be allowed to load from ${domain}:\n${contentTypes}\n\n${detail}`; + + return { message, detail }; +} + +async function addCspRule(_: IpcMainInvokeEvent, url: string, directives: string[], callerName: string): Promise { + if (!validate(url, directives)) { + return "invalid"; + } + + const domain = new URL(url).hostname; + + if (domain in NativeSettings.store.customCspRules) { + return "conflict"; + } + + const { checkboxChecked, response } = await dialog.showMessageBox({ + ...getMessage(url, directives, callerName), + type: callerName ? "info" : "warning", + title: "Vencord Host Permissions", + buttons: ["Cancel", "Allow"], + defaultId: 0, + cancelId: 0, + checkboxLabel: `I fully trust ${domain} and understand the risks of allowing connections to it.`, + checkboxChecked: false, + }); + + if (response !== 1) { + return "cancelled"; + } + + if (!checkboxChecked) { + return "unchecked"; + } + + NativeSettings.store.customCspRules[domain] = directives; + return "ok"; +} + +function removeCspRule(_: IpcMainInvokeEvent, domain: string) { + if (domain in NativeSettings.store.customCspRules) { + delete NativeSettings.store.customCspRules[domain]; + return true; + } + + return false; +} + +function isDomainAllowed(_: IpcMainInvokeEvent, url: string, directives: string[]) { + try { + const domain = new URL(url).hostname; + + const ruleForDomain = CspPolicies[domain] ?? NativeSettings.store.customCspRules[domain]; + if (!ruleForDomain) return false; + + return directives.every(d => ruleForDomain.includes(d)); + } catch (e) { + return false; + } +} diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index 3979a1bc..b9e4099b 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -28,12 +28,15 @@ import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs"; import { open, readdir, readFile } from "fs/promises"; import { join, normalize } from "path"; +import { registerCspIpcHandlers } from "./csp/manager"; import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes"; import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants"; import { makeLinksOpenExternally } from "./utils/externalLinks"; mkdirSync(THEMES_DIR, { recursive: true }); +registerCspIpcHandlers(); + export function ensureSafePath(basePath: string, path: string) { const normalizedBasePath = normalize(basePath + "/"); const newPath = join(basePath, path); diff --git a/src/main/settings.ts b/src/main/settings.ts index 3d367a94..ed2f4850 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -49,16 +49,18 @@ export interface NativeSettings { [setting: string]: any; }; }; + customCspRules: Record; } const DefaultNativeSettings: NativeSettings = { - plugins: {} + plugins: {}, + customCspRules: {} }; const nativeSettings = readSettings("native", NATIVE_SETTINGS_FILE); mergeDefaults(nativeSettings, DefaultNativeSettings); -export const NativeSettings = new SettingsStore(nativeSettings); +export const NativeSettings = new SettingsStore(nativeSettings as NativeSettings); NativeSettings.addGlobalChangeListener(() => { try { diff --git a/src/shared/IpcEvents.ts b/src/shared/IpcEvents.ts index 2027df9c..914a0a9f 100644 --- a/src/shared/IpcEvents.ts +++ b/src/shared/IpcEvents.ts @@ -42,4 +42,8 @@ export const enum IpcEvents { OPEN_IN_APP__RESOLVE_REDIRECT = "VencordOIAResolveRedirect", VOICE_MESSAGES_READ_RECORDING = "VencordVMReadRecording", + + CSP_IS_DOMAIN_ALLOWED = "VencordCspIsDomainAllowed", + CSP_REMOVE_OVERRIDE = "VencordCspRemoveOverride", + CSP_REQUEST_ADD_OVERRIDE = "VencordCspRequestAddOverride", } diff --git a/src/utils/cloud.tsx b/src/utils/cloud.tsx index 508b1c7e..3b482d4d 100644 --- a/src/utils/cloud.tsx +++ b/src/utils/cloud.tsx @@ -19,15 +19,40 @@ import * as DataStore from "@api/DataStore"; import { showNotification } from "@api/Notifications"; import { Settings } from "@api/Settings"; -import { OAuth2AuthorizeModal, UserStore } from "@webpack/common"; +import { Alerts, OAuth2AuthorizeModal, UserStore } from "@webpack/common"; import { Logger } from "./Logger"; import { openModal } from "./modal"; +import { relaunch } from "./native"; export const cloudLogger = new Logger("Cloud", "#39b7e0"); -export const getCloudUrl = () => new URL(Settings.cloud.url); -const cloudUrlOrigin = () => getCloudUrl().origin; +export const getCloudUrl = () => new URL(Settings.cloud.url); +const getCloudUrlOrigin = () => getCloudUrl().origin; + +export async function checkCloudUrlCsp() { + if (IS_WEB) return true; + + const { host } = getCloudUrl(); + if (host === "api.vencord.dev") return true; + + if (await VencordNative.csp.isDomainAllowed(Settings.cloud.url, ["connect-src"])) { + return true; + } + + const res = await VencordNative.csp.requestAddOverride(Settings.cloud.url, ["connect-src"], "Cloud Sync"); + if (res === "ok") { + Alerts.show({ + title: "Cloud Integration enabled", + body: `${host} has been added to the whitelist. Please restart the app for the changes to take effect.`, + confirmText: "Restart now", + cancelText: "Later!", + onConfirm: relaunch + }); + } + return false; +} + const getUserId = () => { const id = UserStore.getCurrentUser()?.id; if (!id) throw new Error("User not yet logged in"); @@ -37,7 +62,7 @@ const getUserId = () => { export async function getAuthorization() { const secrets = await DataStore.get>("Vencord_cloudSecret") ?? {}; - const origin = cloudUrlOrigin(); + const origin = getCloudUrlOrigin(); // we need to migrate from the old format here if (secrets[origin]) { @@ -59,7 +84,7 @@ export async function getAuthorization() { async function setAuthorization(secret: string) { await DataStore.update>("Vencord_cloudSecret", secrets => { secrets ??= {}; - secrets[`${cloudUrlOrigin()}:${getUserId()}`] = secret; + secrets[`${getCloudUrlOrigin()}:${getUserId()}`] = secret; return secrets; }); } @@ -67,7 +92,7 @@ async function setAuthorization(secret: string) { export async function deauthorizeCloud() { await DataStore.update>("Vencord_cloudSecret", secrets => { secrets ??= {}; - delete secrets[`${cloudUrlOrigin()}:${getUserId()}`]; + delete secrets[`${getCloudUrlOrigin()}:${getUserId()}`]; return secrets; }); } @@ -78,6 +103,8 @@ export async function authorizeCloud() { return; } + if (!await checkCloudUrlCsp()) return; + try { const oauthConfiguration = await fetch(new URL("/v1/oauth/settings", getCloudUrl())); var { clientId, redirectUri } = await oauthConfiguration.json(); diff --git a/src/utils/settingsSync.ts b/src/utils/settingsSync.ts index 6ec3e527..c711aacd 100644 --- a/src/utils/settingsSync.ts +++ b/src/utils/settingsSync.ts @@ -21,7 +21,7 @@ import { PlainSettings, Settings } from "@api/Settings"; import { moment, Toasts } from "@webpack/common"; import { deflateSync, inflateSync } from "fflate"; -import { getCloudAuth, getCloudUrl } from "./cloud"; +import { checkCloudUrlCsp, getCloudAuth, getCloudUrl } from "./cloud"; import { Logger } from "./Logger"; import { relaunch } from "./native"; import { chooseFile, saveFile } from "./web"; @@ -115,6 +115,8 @@ const cloudSettingsLogger = new Logger("Cloud:Settings", "#39b7e0"); export async function putCloudSettings(manual?: boolean) { const settings = await exportSettings({ minify: true }); + if (!await checkCloudUrlCsp()) return; + try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "PUT", @@ -159,6 +161,8 @@ export async function putCloudSettings(manual?: boolean) { } export async function getCloudSettings(shouldNotify = true, force = false) { + if (!await checkCloudUrlCsp()) return; + try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "GET", @@ -248,6 +252,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) { } export async function deleteCloudSettings() { + if (!await checkCloudUrlCsp()) return; + try { const res = await fetch(new URL("/v1/settings", getCloudUrl()), { method: "DELETE", From a366693e96bb67ef76d705f737458b3e58d624cd Mon Sep 17 00:00:00 2001 From: Randomuser8219 <168323856+Randomuser8219@users.noreply.github.com> Date: Wed, 11 Jun 2025 17:24:24 -0700 Subject: [PATCH 4/5] ServerInfo: rename "Nitro Boosts" -> "Server Boosts" (#3364) --- src/plugins/serverInfo/GuildInfoModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/serverInfo/GuildInfoModal.tsx b/src/plugins/serverInfo/GuildInfoModal.tsx index 0f08af59..60401b71 100644 --- a/src/plugins/serverInfo/GuildInfoModal.tsx +++ b/src/plugins/serverInfo/GuildInfoModal.tsx @@ -198,7 +198,7 @@ function ServerInfoTab({ guild }: GuildProps) { "Vanity Link": guild.vanityURLCode ? ({`discord.gg/${guild.vanityURLCode}`}) : "-", // Making the anchor href valid would cause Discord to reload "Preferred Locale": guild.preferredLocale || "-", "Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?", - "Nitro Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`, + "Server Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`, "Channels": GuildChannelStore.getChannels(guild.id)?.count - 1 || "?", // - null category "Roles": Object.keys(GuildStore.getRoles(guild.id)).length - 1, // - @everyone }; From b35b72c066710943bc4569d4e5791fb758559b11 Mon Sep 17 00:00:00 2001 From: T1ckbase <146760065+T1ckbase@users.noreply.github.com> Date: Thu, 12 Jun 2025 08:29:50 +0800 Subject: [PATCH 5/5] Translate: Make translation more readable (#3252) --- src/plugins/translate/TranslationAccessory.tsx | 2 +- src/plugins/translate/styles.css | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/translate/TranslationAccessory.tsx b/src/plugins/translate/TranslationAccessory.tsx index 9b6393cc..db9461d8 100644 --- a/src/plugins/translate/TranslationAccessory.tsx +++ b/src/plugins/translate/TranslationAccessory.tsx @@ -57,7 +57,7 @@ export function TranslationAccessory({ message }: { message: Message; }) { {Parser.parse(translation.text)} - {" "} + (translated from {translation.sourceLanguage} - setTranslation(undefined)} />) ); diff --git a/src/plugins/translate/styles.css b/src/plugins/translate/styles.css index c07c9e36..33a1fc85 100644 --- a/src/plugins/translate/styles.css +++ b/src/plugins/translate/styles.css @@ -15,6 +15,8 @@ margin-top: 0.5em; font-style: italic; font-weight: 400; + line-height: 1.2rem; + white-space: break-spaces; } .vc-trans-accessory-icon {
https://i.imgur.com/...