Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
thororen1234 2025-06-06 15:30:58 -04:00
commit 8ae44e2ec3
No known key found for this signature in database
20 changed files with 209 additions and 91 deletions

View file

@ -48,7 +48,7 @@ export function _modifyAccessories(
) { ) {
for (const [key, accessory] of accessories.entries()) { for (const [key, accessory] of accessories.entries()) {
const res = ( const res = (
<ErrorBoundary message={`Failed to render ${key} Message Accessory`} key={key}> <ErrorBoundary noop message={`Failed to render ${key} Message Accessory`} key={key}>
<accessory.render {...props} /> <accessory.render {...props} />
</ErrorBoundary> </ErrorBoundary>
); );

View file

@ -75,10 +75,15 @@ const ErrorBoundary = LazyComponent(() => {
logger.error(`${this.props.message || "A component threw an Error"}\n`, error, errorInfo.componentStack); logger.error(`${this.props.message || "A component threw an Error"}\n`, error, errorInfo.componentStack);
} }
get isNoop() {
if (IS_DEV) return false;
return this.props.noop;
}
render() { render() {
if (this.state.error === NO_ERROR) return this.props.children; if (this.state.error === NO_ERROR) return this.props.children;
if (this.props.noop) return null; if (this.isNoop) return null;
if (this.props.fallback) if (this.props.fallback)
return ( return (

View file

@ -4,4 +4,8 @@
border: 1px solid #e78284; border: 1px solid #e78284;
border-radius: 5px; border-radius: 5px;
color: var(--text-normal, white); color: var(--text-normal, white);
& a:hover {
text-decoration: underline;
}
} }

View file

@ -28,6 +28,9 @@ export function Link(props: React.PropsWithChildren<Props>) {
props.style.pointerEvents = "none"; props.style.pointerEvents = "none";
props["aria-disabled"] = true; props["aria-disabled"] = true;
} }
props.rel ??= "noreferrer";
return ( return (
<a role="link" target="_blank" {...props}> <a role="link" target="_blank" {...props}>
{props.children} {props.children}

View file

@ -270,7 +270,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
{!!plugin.settingsAboutComponent && ( {!!plugin.settingsAboutComponent && (
<div className={classes(Margins.bottom8, "vc-text-selectable")}> <div className={classes(Margins.bottom8, "vc-text-selectable")}>
<Forms.FormSection> <Forms.FormSection>
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent"> <ErrorBoundary message="An error occurred while rendering this plugin's custom Info Component">
<plugin.settingsAboutComponent tempSettings={tempSettings} /> <plugin.settingsAboutComponent tempSettings={tempSettings} />
</ErrorBoundary> </ErrorBoundary>
</Forms.FormSection> </Forms.FormSection>

View file

@ -147,7 +147,7 @@ function VencordPopoutButton() {
function ToolboxFragmentWrapper({ children }: { children: ReactNode[]; }) { function ToolboxFragmentWrapper({ children }: { children: ReactNode[]; }) {
children.splice( children.splice(
children.length - 1, 0, children.length - 1, 0,
<ErrorBoundary noop={true}> <ErrorBoundary noop>
<VencordPopoutButton /> <VencordPopoutButton />
</ErrorBoundary> </ErrorBoundary>
); );

138
src/main/csp.ts Normal file
View file

@ -0,0 +1,138 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { session } from "electron";
type PolicyMap = Record<string, string[]>;
export const ConnectSrc = ["connect-src"];
export const MediaSrc = [...ConnectSrc, "img-src", "media-src"];
export const CssSrc = ["style-src", "font-src"];
export const MediaAndCssSrc = [...MediaSrc, ...CssSrc];
export const MediaScriptsAndCssSrc = [...MediaAndCssSrc, "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
"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
"*.githack.com": MediaAndCssSrc, // githack (namely raw.githack.com), used by some themes
"jsdelivr.net": MediaAndCssSrc, // 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
"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
// CDNs used for some things by Vencord.
// FIXME: we really should not be using CDNs anymore
"cdnjs.cloudflare.com": MediaScriptsAndCssSrc,
"cdn.jsdelivr.net": MediaScriptsAndCssSrc,
// 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
"decor.fieryflames.dev": ConnectSrc, // Decor API
"ugc.decor.fieryflames.dev": MediaSrc, // 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)
};
const findHeader = (headers: PolicyMap, headerName: Lowercase<string>) => {
return Object.keys(headers).find(h => h.toLowerCase() === headerName);
};
const parsePolicy = (policy: string): PolicyMap => {
const result: PolicyMap = {};
policy.split(";").forEach(directive => {
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
result[directiveKey] = directiveValue;
}
});
return result;
};
const stringifyPolicy = (policy: PolicyMap): string =>
Object.entries(policy)
.filter(([, values]) => values?.length)
.map(directive => directive.flat().join(" "))
.join("; ");
const patchCsp = (headers: PolicyMap) => {
const reportOnlyHeader = findHeader(headers, "content-security-policy-report-only");
if (reportOnlyHeader)
delete headers[reportOnlyHeader];
const header = findHeader(headers, "content-security-policy");
if (header) {
const csp = parsePolicy(headers[header][0]);
const pushDirective = (directive: string, ...values: string[]) => {
csp[directive] ??= [...(csp["default-src"] ?? [])];
csp[directive].push(...values);
};
pushDirective("style-src", "'unsafe-inline'");
// we could make unsafe-inline safe by using strict-dynamic with a random nonce on our Vencord loader script https://content-security-policy.com/strict-dynamic/
// HOWEVER, at the time of writing (24 Jan 2025), Discord is INSANE and also uses unsafe-inline
// Once they stop using it, we also should
pushDirective("script-src", "'unsafe-inline'", "'unsafe-eval'");
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
pushDirective(directive, "blob:", "data:", "vencord:");
}
for (const [host, directives] of Object.entries(CspPolicies)) {
for (const directive of directives) {
pushDirective(directive, host);
}
}
headers[header] = [stringifyPolicy(csp)];
}
};
export function initCsp() {
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders);
// Fix hosts that don't properly set the css content type, such as
// raw.githubusercontent.com
if (resourceType === "stylesheet") {
const header = findHeader(responseHeaders, "content-type");
if (header)
responseHeaders[header] = ["text/css"];
}
}
cb({ cancel: false, responseHeaders });
});
// assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
// For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
// impossible to load css from github raw despite our fix above
session.defaultSession.webRequest.onHeadersReceived = () => { };
}

View file

@ -16,9 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { app, protocol, session } from "electron"; import { app, protocol } from "electron";
import { join } from "path"; import { join } from "path";
import { initCsp } from "./csp";
import { ensureSafePath } from "./ipcMain"; import { ensureSafePath } from "./ipcMain";
import { RendererSettings } from "./settings"; import { RendererSettings } from "./settings";
import { IS_VANILLA, THEMES_DIR } from "./utils/constants"; import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
@ -86,70 +87,7 @@ if (!IS_VANILLA && !IS_EXTENSION) {
} catch { } } catch { }
const findHeader = (headers: Record<string, string[]>, headerName: Lowercase<string>) => { initCsp();
return Object.keys(headers).find(h => h.toLowerCase() === headerName);
};
// Remove CSP
type PolicyResult = Record<string, string[]>;
const parsePolicy = (policy: string): PolicyResult => {
const result: PolicyResult = {};
policy.split(";").forEach(directive => {
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
result[directiveKey] = directiveValue;
}
});
return result;
};
const stringifyPolicy = (policy: PolicyResult): string =>
Object.entries(policy)
.filter(([, values]) => values?.length)
.map(directive => directive.flat().join(" "))
.join("; ");
const patchCsp = (headers: Record<string, string[]>) => {
const header = findHeader(headers, "content-security-policy");
if (header) {
const csp = parsePolicy(headers[header][0]);
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
csp[directive] ??= [];
csp[directive].push("*", "blob:", "data:", "vencord:", "'unsafe-inline'");
}
// TODO: Restrict this to only imported packages with fixed version.
// Perhaps auto generate with esbuild
csp["script-src"] ??= [];
csp["script-src"].push("'unsafe-eval'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com");
headers[header] = [stringifyPolicy(csp)];
}
};
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders);
// Fix hosts that don't properly set the css content type, such as
// raw.githubusercontent.com
if (resourceType === "stylesheet") {
const header = findHeader(responseHeaders, "content-type");
if (header)
responseHeaders[header] = ["text/css"];
}
}
cb({ cancel: false, responseHeaders });
});
// assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
// For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
// impossible to load css from github raw despite our fix above
session.defaultSession.webRequest.onHeadersReceived = () => { };
}); });
} }

View file

@ -31,7 +31,7 @@ import { User } from "discord-types/general";
import { EquicordDonorModal, VencordDonorModal } from "./modals"; import { EquicordDonorModal, VencordDonorModal } from "./modals";
const CONTRIBUTOR_BADGE = "https://vencord.dev/assets/favicon.png"; const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/emojis/1092089799109775453.png?size=64";
const EQUICORD_CONTRIBUTOR_BADGE = "https://i.imgur.com/57ATLZu.png"; const EQUICORD_CONTRIBUTOR_BADGE = "https://i.imgur.com/57ATLZu.png";
const EQUICORD_DONOR_BADGE = "https://cdn.nest.rip/uploads/78cb1e77-b7a6-4242-9089-e91f866159bf.png"; const EQUICORD_DONOR_BADGE = "https://cdn.nest.rip/uploads/78cb1e77-b7a6-4242-9089-e91f866159bf.png";

View file

@ -139,5 +139,5 @@ export default definePlugin({
} }
}, },
DecorSection: ErrorBoundary.wrap(DecorSection) DecorSection: ErrorBoundary.wrap(DecorSection, { noop: true })
}); });

View file

@ -118,7 +118,7 @@ export default definePlugin({
renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) { renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) {
this.instance = instance; this.instance = instance;
return ( return (
<ErrorBoundary noop={true}> <ErrorBoundary noop>
<SearchBar instance={instance} SearchBarComponent={SearchBarComponent} /> <SearchBar instance={instance} SearchBarComponent={SearchBarComponent} />
</ErrorBoundary> </ErrorBoundary>
); );

View file

@ -98,7 +98,7 @@ export default definePlugin({
src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${roleId}/${role.icon}.webp?size=24&quality=lossless`} src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${roleId}/${role.icon}.webp?size=24&quality=lossless`}
/> />
); );
}), }, { noop: true }),
}); });
function getUsernameString(username: string) { function getUsernameString(username: string) {

View file

@ -20,7 +20,6 @@ import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccesso
import { updateMessage } from "@api/MessageUpdater"; import { updateMessage } from "@api/MessageUpdater";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings"; import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants.js"; import { Devs } from "@utils/constants.js";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { Queue } from "@utils/Queue"; import { Queue } from "@utils/Queue";
@ -373,7 +372,7 @@ export default definePlugin({
settings, settings,
start() { start() {
addMessageAccessory("messageLinkEmbed", props => { addMessageAccessory("MessageLinkEmbeds", props => {
if (!messageLinkRegex.test(props.message.content)) if (!messageLinkRegex.test(props.message.content))
return null; return null;
@ -381,15 +380,13 @@ export default definePlugin({
messageLinkRegex.lastIndex = 0; messageLinkRegex.lastIndex = 0;
return ( return (
<ErrorBoundary> <MessageEmbedAccessory
<MessageEmbedAccessory message={props.message}
message={props.message} />
/>
</ErrorBoundary>
); );
}, 4 /* just above rich embeds */); }, 4 /* just above rich embeds */);
}, },
stop() { stop() {
removeMessageAccessory("messageLinkEmbed"); removeMessageAccessory("MessageLinkEmbeds");
} }
}); });

View file

@ -204,5 +204,5 @@ export default definePlugin({
/> />
</> </>
); );
}) }, { noop: true })
}); });

View file

@ -75,5 +75,5 @@ export default definePlugin({
}}> Pause Indefinitely.</a>} }}> Pause Indefinitely.</a>}
</div> </div>
); );
}) }, { noop: true })
}); });

View file

@ -53,14 +53,12 @@ function makeSearchItem(src: string) {
<Flex style={{ alignItems: "center", gap: "0.5em" }}> <Flex style={{ alignItems: "center", gap: "0.5em" }}>
<img <img
style={{ style={{
borderRadius: i >= 3 // Do not round Google, Yandex & SauceNAO borderRadius: "50%",
? "50%"
: void 0
}} }}
aria-hidden="true" aria-hidden="true"
height={16} height={16}
width={16} width={16}
src={new URL("/favicon.ico", Engines[engine]).toString().replace("lens.", "")} src={`https://icons.duckduckgo.com/ip3/${new URL(Engines[engine]).host}.ico`}
/> />
{engine} {engine}
</Flex> </Flex>

View file

@ -86,5 +86,5 @@ export default definePlugin({
</TooltipContainer> </TooltipContainer>
)} )}
</div>; </div>;
}) }, { noop: true })
}); });

View file

@ -57,7 +57,7 @@ export interface Dev {
*/ */
export const Devs = /* #__PURE__*/ Object.freeze({ export const Devs = /* #__PURE__*/ Object.freeze({
Ven: { Ven: {
name: "Vee", name: "V",
id: 343383572805058560n id: 343383572805058560n
}, },
Arjix: { Arjix: {
@ -211,7 +211,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
}, },
axyie: { axyie: {
name: "'ax", name: "'ax",
id: 273562710745284628n id: 929877747151548487n,
}, },
pointy: { pointy: {
name: "pointy", name: "pointy",
@ -604,7 +604,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
}, },
samsam: { samsam: {
name: "samsam", name: "samsam",
id: 836452332387565589n, id: 400482410279469056n,
}, },
Cootshk: { Cootshk: {
name: "Cootshk", name: "Cootshk",

View file

@ -0,0 +1,34 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { useLayoutEffect } from "@webpack/common";
import { useForceUpdater } from "./react";
const cssRelevantDirectives = ["style-src", "img-src", "font-src"] as const;
export const CspBlockedUrls = new Set<string>();
const CspErrorListeners = new Set<() => void>();
document.addEventListener("securitypolicyviolation", ({ effectiveDirective, blockedURI }) => {
if (!blockedURI || !cssRelevantDirectives.includes(effectiveDirective as any)) return;
CspBlockedUrls.add(blockedURI);
CspErrorListeners.forEach(listener => listener());
});
export function useCspErrors() {
const forceUpdate = useForceUpdater();
useLayoutEffect(() => {
CspErrorListeners.add(forceUpdate);
return () => void CspErrorListeners.delete(forceUpdate);
}, [forceUpdate]);
return [...CspBlockedUrls] as const;
}

View file

@ -21,6 +21,7 @@ export * from "../shared/onceDefined";
export * from "./ChangeList"; export * from "./ChangeList";
export * from "./clipboard"; export * from "./clipboard";
export * from "./constants"; export * from "./constants";
export * from "./cspViolations";
export * from "./discord"; export * from "./discord";
export * from "./guards"; export * from "./guards";
export * from "./intlHash"; export * from "./intlHash";