diff options
Diffstat (limited to 'src/main')
-rw-r--r-- | src/main/index.ts | 16 | ||||
-rw-r--r-- | src/main/ipcMain.ts | 45 | ||||
-rw-r--r-- | src/main/themes/LICENSE | 177 | ||||
-rw-r--r-- | src/main/themes/index.ts | 81 | ||||
-rw-r--r-- | src/main/utils/constants.ts | 1 |
5 files changed, 314 insertions, 6 deletions
diff --git a/src/main/index.ts b/src/main/index.ts index cb723bb..a8b9429 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -19,8 +19,8 @@ import { app, protocol, session } from "electron"; import { join } from "path"; -import { getSettings } from "./ipcMain"; -import { IS_VANILLA } from "./utils/constants"; +import { ensureSafePath, getSettings } from "./ipcMain"; +import { IS_VANILLA, THEMES_DIR } from "./utils/constants"; import { installExt } from "./utils/extensions"; if (IS_VENCORD_DESKTOP || !IS_VANILLA) { @@ -30,6 +30,16 @@ if (IS_VENCORD_DESKTOP || !IS_VANILLA) { protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => { let url = unsafeUrl.slice("vencord://".length); if (url.endsWith("/")) url = url.slice(0, -1); + if (url.startsWith("/themes/")) { + const theme = url.slice("/themes/".length); + const safeUrl = ensureSafePath(THEMES_DIR, theme); + if (!safeUrl) { + cb({ statusCode: 403 }); + return; + } + cb(safeUrl.replace(/\?v=\d+$/, "")); + return; + } switch (url) { case "renderer.js.map": case "vencordDesktopRenderer.js.map": @@ -75,7 +85,7 @@ if (IS_VENCORD_DESKTOP || !IS_VANILLA) { const csp = parsePolicy(headers[header][0]); for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) { - csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"]; + csp[directive] = ["*", "blob:", "data:", "vencord:", "'unsafe-inline'"]; } // TODO: Restrict this to only imported packages with fixed version. // Perhaps auto generate with esbuild diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index d62888c..1dcb17c 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -24,19 +24,50 @@ import { IpcEvents } from "@utils/IpcEvents"; import { Queue } from "@utils/Queue"; import { BrowserWindow, ipcMain, shell } from "electron"; import { mkdirSync, readFileSync, watch } from "fs"; -import { open, readFile, writeFile } from "fs/promises"; -import { join } from "path"; +import { open, readdir, readFile, writeFile } from "fs/promises"; +import { join, normalize } from "path"; import monacoHtml from "~fileContent/../components/monacoWin.html;base64"; -import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./utils/constants"; +import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes"; +import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE, THEMES_DIR } from "./utils/constants"; mkdirSync(SETTINGS_DIR, { recursive: true }); +mkdirSync(THEMES_DIR, { recursive: true }); + +export function ensureSafePath(basePath: string, path: string) { + const normalizedBasePath = normalize(basePath); + const newPath = join(basePath, path); + const normalizedPath = normalize(newPath); + return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null; +} function readCss() { return readFile(QUICKCSS_PATH, "utf-8").catch(() => ""); } +async function listThemes(): Promise<UserThemeHeader[]> { + const files = await readdir(THEMES_DIR).catch(() => []); + + const themeInfo: UserThemeHeader[] = []; + + for (const fileName of files) { + const data = await getThemeData(fileName).then(stripBOM).catch(() => null); + if (!data) continue; + const parsed = getThemeInfo(data, fileName); + themeInfo.push(parsed); + } + + return themeInfo; +} + +function getThemeData(fileName: string) { + fileName = fileName.replace(/\?v=\d+$/, ""); + const safePath = ensureSafePath(THEMES_DIR, fileName); + if (!safePath) return Promise.reject(`Unsafe path ${fileName}`); + return readFile(safePath, "utf-8"); +} + export function readSettings() { try { return readFileSync(SETTINGS_FILE, "utf-8"); @@ -75,6 +106,10 @@ ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) => cssWriteQueue.push(() => writeFile(QUICKCSS_PATH, css)) ); +ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR); +ipcMain.handle(IpcEvents.GET_THEMES_LIST, () => listThemes()); +ipcMain.handle(IpcEvents.GET_THEME_DATA, (_, fileName) => getThemeData(fileName)); + ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR); ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings()); @@ -90,6 +125,10 @@ export function initIpc(mainWindow: BrowserWindow) { mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss()); }, 50)); }); + + watch(THEMES_DIR, { persistent: false }, debounce(() => { + mainWindow.webContents.postMessage(IpcEvents.THEME_UPDATE, void 0); + })); } ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { diff --git a/src/main/themes/LICENSE b/src/main/themes/LICENSE new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/src/main/themes/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/src/main/themes/index.ts b/src/main/themes/index.ts new file mode 100644 index 0000000..f660e50 --- /dev/null +++ b/src/main/themes/index.ts @@ -0,0 +1,81 @@ +/* eslint-disable header/header */ + +/*! + * BetterDiscord addon meta parser + * Copyright 2023 BetterDiscord contributors + * Copyright 2023 Vendicated and Vencord contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const splitRegex = /[^\S\r\n]*?\r?(?:\r\n|\n)[^\S\r\n]*?\*[^\S\r\n]?/; +const escapedAtRegex = /^\\@/; + +export interface UserThemeHeader { + fileName: string; + name: string; + author: string; + description: string; + version?: string; + license?: string; + source?: string; + website?: string; + invite?: string; +} + +function makeHeader(fileName: string, opts: Partial<UserThemeHeader> = {}): UserThemeHeader { + return { + fileName, + name: opts.name ?? fileName.replace(/\.css$/i, ""), + author: opts.author ?? "Unknown Author", + description: opts.description ?? "A Discord Theme.", + version: opts.version, + license: opts.license, + source: opts.source, + website: opts.website, + invite: opts.invite + }; +} + +export function stripBOM(fileContent: string) { + if (fileContent.charCodeAt(0) === 0xFEFF) { + fileContent = fileContent.slice(1); + } + return fileContent; +} + +export function getThemeInfo(css: string, fileName: string): UserThemeHeader { + if (!css) return makeHeader(fileName); + + const block = css.split("/**", 2)?.[1]?.split("*/", 1)?.[0]; + if (!block) return makeHeader(fileName); + + const header: Partial<UserThemeHeader> = {}; + let field = ""; + let accum = ""; + for (const line of block.split(splitRegex)) { + if (line.length === 0) continue; + if (line.charAt(0) === "@" && line.charAt(1) !== " ") { + header[field] = accum.trim(); + const l = line.indexOf(" "); + field = line.substring(1, l); + accum = line.substring(l + 1); + } + else { + accum += " " + line.replace("\\n", "\n").replace(escapedAtRegex, "@"); + } + } + header[field] = accum.trim(); + delete header[""]; + return makeHeader(fileName, header); +} diff --git a/src/main/utils/constants.ts b/src/main/utils/constants.ts index 8ebf7f4..cd6e509 100644 --- a/src/main/utils/constants.ts +++ b/src/main/utils/constants.ts @@ -25,6 +25,7 @@ export const DATA_DIR = process.env.VENCORD_USER_DATA_DIR ?? ( : join(app.getPath("userData"), "..", "Vencord") ); export const SETTINGS_DIR = join(DATA_DIR, "settings"); +export const THEMES_DIR = join(DATA_DIR, "themes"); export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css"); export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json"); export const ALLOWED_PROTOCOLS = [ |