mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-07 13:43:03 -04:00
Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
8ae44e2ec3
20 changed files with 209 additions and 91 deletions
|
@ -48,7 +48,7 @@ export function _modifyAccessories(
|
|||
) {
|
||||
for (const [key, accessory] of accessories.entries()) {
|
||||
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} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
|
@ -75,10 +75,15 @@ const ErrorBoundary = LazyComponent(() => {
|
|||
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() {
|
||||
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)
|
||||
return (
|
||||
|
|
|
@ -4,4 +4,8 @@
|
|||
border: 1px solid #e78284;
|
||||
border-radius: 5px;
|
||||
color: var(--text-normal, white);
|
||||
|
||||
& a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,9 @@ export function Link(props: React.PropsWithChildren<Props>) {
|
|||
props.style.pointerEvents = "none";
|
||||
props["aria-disabled"] = true;
|
||||
}
|
||||
|
||||
props.rel ??= "noreferrer";
|
||||
|
||||
return (
|
||||
<a role="link" target="_blank" {...props}>
|
||||
{props.children}
|
||||
|
|
|
@ -270,7 +270,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
{!!plugin.settingsAboutComponent && (
|
||||
<div className={classes(Margins.bottom8, "vc-text-selectable")}>
|
||||
<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} />
|
||||
</ErrorBoundary>
|
||||
</Forms.FormSection>
|
||||
|
|
|
@ -147,7 +147,7 @@ function VencordPopoutButton() {
|
|||
function ToolboxFragmentWrapper({ children }: { children: ReactNode[]; }) {
|
||||
children.splice(
|
||||
children.length - 1, 0,
|
||||
<ErrorBoundary noop={true}>
|
||||
<ErrorBoundary noop>
|
||||
<VencordPopoutButton />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
138
src/main/csp.ts
Normal file
138
src/main/csp.ts
Normal 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 = () => { };
|
||||
}
|
|
@ -16,9 +16,10 @@
|
|||
* 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 { initCsp } from "./csp";
|
||||
import { ensureSafePath } from "./ipcMain";
|
||||
import { RendererSettings } from "./settings";
|
||||
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
||||
|
@ -86,70 +87,7 @@ if (!IS_VANILLA && !IS_EXTENSION) {
|
|||
} catch { }
|
||||
|
||||
|
||||
const findHeader = (headers: Record<string, string[]>, headerName: Lowercase<string>) => {
|
||||
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 = () => { };
|
||||
initCsp();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ import { User } from "discord-types/general";
|
|||
|
||||
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_DONOR_BADGE = "https://cdn.nest.rip/uploads/78cb1e77-b7a6-4242-9089-e91f866159bf.png";
|
||||
|
||||
|
|
|
@ -139,5 +139,5 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
|
||||
DecorSection: ErrorBoundary.wrap(DecorSection)
|
||||
DecorSection: ErrorBoundary.wrap(DecorSection, { noop: true })
|
||||
});
|
||||
|
|
|
@ -118,7 +118,7 @@ export default definePlugin({
|
|||
renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) {
|
||||
this.instance = instance;
|
||||
return (
|
||||
<ErrorBoundary noop={true}>
|
||||
<ErrorBoundary noop>
|
||||
<SearchBar instance={instance} SearchBarComponent={SearchBarComponent} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
|
@ -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`}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
}, { noop: true }),
|
||||
});
|
||||
|
||||
function getUsernameString(username: string) {
|
||||
|
|
|
@ -20,7 +20,6 @@ import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccesso
|
|||
import { updateMessage } from "@api/MessageUpdater";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { getUserSettingLazy } from "@api/UserSettings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants.js";
|
||||
import { classes } from "@utils/misc";
|
||||
import { Queue } from "@utils/Queue";
|
||||
|
@ -373,7 +372,7 @@ export default definePlugin({
|
|||
settings,
|
||||
|
||||
start() {
|
||||
addMessageAccessory("messageLinkEmbed", props => {
|
||||
addMessageAccessory("MessageLinkEmbeds", props => {
|
||||
if (!messageLinkRegex.test(props.message.content))
|
||||
return null;
|
||||
|
||||
|
@ -381,15 +380,13 @@ export default definePlugin({
|
|||
messageLinkRegex.lastIndex = 0;
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<MessageEmbedAccessory
|
||||
message={props.message}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}, 4 /* just above rich embeds */);
|
||||
},
|
||||
stop() {
|
||||
removeMessageAccessory("messageLinkEmbed");
|
||||
removeMessageAccessory("MessageLinkEmbeds");
|
||||
}
|
||||
});
|
||||
|
|
|
@ -204,5 +204,5 @@ export default definePlugin({
|
|||
/>
|
||||
</>
|
||||
);
|
||||
})
|
||||
}, { noop: true })
|
||||
});
|
||||
|
|
|
@ -75,5 +75,5 @@ export default definePlugin({
|
|||
}}> Pause Indefinitely.</a>}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}, { noop: true })
|
||||
});
|
||||
|
|
|
@ -53,14 +53,12 @@ function makeSearchItem(src: string) {
|
|||
<Flex style={{ alignItems: "center", gap: "0.5em" }}>
|
||||
<img
|
||||
style={{
|
||||
borderRadius: i >= 3 // Do not round Google, Yandex & SauceNAO
|
||||
? "50%"
|
||||
: void 0
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
height={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}
|
||||
</Flex>
|
||||
|
|
|
@ -86,5 +86,5 @@ export default definePlugin({
|
|||
</TooltipContainer>
|
||||
)}
|
||||
</div>;
|
||||
})
|
||||
}, { noop: true })
|
||||
});
|
||||
|
|
|
@ -57,7 +57,7 @@ export interface Dev {
|
|||
*/
|
||||
export const Devs = /* #__PURE__*/ Object.freeze({
|
||||
Ven: {
|
||||
name: "Vee",
|
||||
name: "V",
|
||||
id: 343383572805058560n
|
||||
},
|
||||
Arjix: {
|
||||
|
@ -211,7 +211,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
},
|
||||
axyie: {
|
||||
name: "'ax",
|
||||
id: 273562710745284628n
|
||||
id: 929877747151548487n,
|
||||
},
|
||||
pointy: {
|
||||
name: "pointy",
|
||||
|
@ -604,7 +604,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
},
|
||||
samsam: {
|
||||
name: "samsam",
|
||||
id: 836452332387565589n,
|
||||
id: 400482410279469056n,
|
||||
},
|
||||
Cootshk: {
|
||||
name: "Cootshk",
|
||||
|
|
34
src/utils/cspViolations.ts
Normal file
34
src/utils/cspViolations.ts
Normal 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;
|
||||
}
|
|
@ -21,6 +21,7 @@ export * from "../shared/onceDefined";
|
|||
export * from "./ChangeList";
|
||||
export * from "./clipboard";
|
||||
export * from "./constants";
|
||||
export * from "./cspViolations";
|
||||
export * from "./discord";
|
||||
export * from "./guards";
|
||||
export * from "./intlHash";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue