2021-06-25 15:35:58 +02:00
|
|
|
/*
|
|
|
|
Copyright 2021 New Vector Ltd
|
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import {
|
|
|
|
clipboard,
|
|
|
|
nativeImage,
|
|
|
|
Menu,
|
|
|
|
MenuItem,
|
|
|
|
shell,
|
|
|
|
dialog,
|
|
|
|
ipcMain,
|
|
|
|
NativeImage,
|
|
|
|
WebContents,
|
|
|
|
ContextMenuParams,
|
|
|
|
DownloadItem,
|
|
|
|
MenuItemConstructorOptions,
|
|
|
|
IpcMainEvent,
|
2023-06-09 13:11:00 +02:00
|
|
|
Event,
|
2022-12-15 12:00:58 +01:00
|
|
|
} from "electron";
|
|
|
|
import url from "url";
|
|
|
|
import fs from "fs";
|
|
|
|
import fetch from "node-fetch";
|
|
|
|
import { pipeline } from "stream";
|
|
|
|
import path from "path";
|
2022-01-10 13:57:33 +01:00
|
|
|
|
2022-12-15 12:00:58 +01:00
|
|
|
import { _t } from "./language-helper";
|
2019-12-06 19:17:34 +01:00
|
|
|
|
|
|
|
const MAILTO_PREFIX = "mailto:";
|
|
|
|
|
2022-12-15 12:00:58 +01:00
|
|
|
const PERMITTED_URL_SCHEMES: string[] = ["http:", "https:", MAILTO_PREFIX];
|
2019-12-06 19:17:34 +01:00
|
|
|
|
2021-06-25 15:35:58 +02:00
|
|
|
function safeOpenURL(target: string): void {
|
2019-12-06 19:17:34 +01:00
|
|
|
// openExternal passes the target to open/start/xdg-open,
|
|
|
|
// so put fairly stringent limits on what can be opened
|
|
|
|
// (for instance, open /bin/sh does indeed open a terminal
|
|
|
|
// with a shell, albeit with no arguments)
|
|
|
|
const parsedUrl = url.parse(target);
|
2022-10-13 13:42:33 +02:00
|
|
|
if (PERMITTED_URL_SCHEMES.includes(parsedUrl.protocol!)) {
|
2019-12-06 19:17:34 +01:00
|
|
|
// explicitly use the URL re-assembled by the url library,
|
|
|
|
// so we know the url parser has understood all the parts
|
|
|
|
// of the input string
|
|
|
|
const newTarget = url.format(parsedUrl);
|
|
|
|
shell.openExternal(newTarget);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-25 15:35:58 +02:00
|
|
|
function onWindowOrNavigate(ev: Event, target: string): void {
|
2019-12-06 19:17:34 +01:00
|
|
|
// always prevent the default: if something goes wrong,
|
|
|
|
// we don't want to end up opening it in the electron
|
|
|
|
// app, as we could end up opening any sort of random
|
|
|
|
// url in a window that has node scripting access.
|
|
|
|
ev.preventDefault();
|
|
|
|
safeOpenURL(target);
|
|
|
|
}
|
|
|
|
|
2021-06-25 15:35:58 +02:00
|
|
|
function writeNativeImage(filePath: string, img: NativeImage): Promise<void> {
|
2022-12-15 12:00:58 +01:00
|
|
|
switch (filePath.split(".").pop()?.toLowerCase()) {
|
2020-05-31 13:23:23 +02:00
|
|
|
case "jpg":
|
|
|
|
case "jpeg":
|
|
|
|
return fs.promises.writeFile(filePath, img.toJPEG(100));
|
|
|
|
case "bmp":
|
|
|
|
return fs.promises.writeFile(filePath, img.toBitmap());
|
|
|
|
case "png":
|
|
|
|
default:
|
|
|
|
return fs.promises.writeFile(filePath, img.toPNG());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-25 15:35:58 +02:00
|
|
|
function onLinkContextMenu(ev: Event, params: ContextMenuParams, webContents: WebContents): void {
|
2019-12-06 19:17:34 +01:00
|
|
|
let url = params.linkURL || params.srcURL;
|
|
|
|
|
2022-12-15 12:00:58 +01:00
|
|
|
if (url.startsWith("vector://vector/webapp")) {
|
2020-05-31 13:23:23 +02:00
|
|
|
// Avoid showing a context menu for app icons
|
|
|
|
if (params.hasImageContents) return;
|
|
|
|
// Rewrite URL so that it can be used outside of the app
|
2020-07-01 16:30:53 +02:00
|
|
|
url = "https://app.element.io/" + url.substring(23);
|
2019-12-06 19:17:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const popupMenu = new Menu();
|
|
|
|
// No point trying to open blob: URLs in an external browser: it ain't gonna work.
|
2022-12-15 12:00:58 +01:00
|
|
|
if (!url.startsWith("blob:")) {
|
|
|
|
popupMenu.append(
|
|
|
|
new MenuItem({
|
|
|
|
label: url,
|
|
|
|
click(): void {
|
|
|
|
safeOpenURL(url);
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
2019-12-06 19:17:34 +01:00
|
|
|
}
|
|
|
|
|
2020-05-31 13:23:23 +02:00
|
|
|
if (params.hasImageContents) {
|
2022-12-15 12:00:58 +01:00
|
|
|
popupMenu.append(
|
|
|
|
new MenuItem({
|
|
|
|
label: _t("Copy image"),
|
|
|
|
accelerator: "c",
|
|
|
|
click(): void {
|
|
|
|
webContents.copyImageAt(params.x, params.y);
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
2019-12-06 19:17:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// No point offering to copy a blob: URL either
|
2022-12-15 12:00:58 +01:00
|
|
|
if (!url.startsWith("blob:")) {
|
2019-12-06 19:17:34 +01:00
|
|
|
// Special-case e-mail URLs to strip the `mailto:` like modern browsers do
|
|
|
|
if (url.startsWith(MAILTO_PREFIX)) {
|
2022-12-15 12:00:58 +01:00
|
|
|
popupMenu.append(
|
|
|
|
new MenuItem({
|
|
|
|
label: _t("Copy email address"),
|
|
|
|
accelerator: "a",
|
|
|
|
click(): void {
|
|
|
|
clipboard.writeText(url.substr(MAILTO_PREFIX.length));
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
2019-12-06 19:17:34 +01:00
|
|
|
} else {
|
2022-12-15 12:00:58 +01:00
|
|
|
popupMenu.append(
|
|
|
|
new MenuItem({
|
|
|
|
label: params.hasImageContents ? _t("Copy image address") : _t("Copy link address"),
|
|
|
|
accelerator: "a",
|
|
|
|
click(): void {
|
|
|
|
clipboard.writeText(url);
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
2019-12-06 19:17:34 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-31 13:28:15 +02:00
|
|
|
// XXX: We cannot easily save a blob from the main process as
|
|
|
|
// only the renderer can resolve them so don't give the user an option to.
|
2022-12-15 12:00:58 +01:00
|
|
|
if (params.hasImageContents && !url.startsWith("blob:")) {
|
|
|
|
popupMenu.append(
|
|
|
|
new MenuItem({
|
|
|
|
label: _t("Save image as..."),
|
|
|
|
accelerator: "s",
|
|
|
|
async click(): Promise<void> {
|
|
|
|
const targetFileName = params.suggestedFilename || params.altText || "image.png";
|
|
|
|
const { filePath } = await dialog.showSaveDialog({
|
|
|
|
defaultPath: targetFileName,
|
|
|
|
});
|
2019-12-06 19:17:34 +01:00
|
|
|
|
2022-12-15 12:00:58 +01:00
|
|
|
if (!filePath) return; // user cancelled dialog
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (url.startsWith("data:")) {
|
|
|
|
await writeNativeImage(filePath, nativeImage.createFromDataURL(url));
|
|
|
|
} else {
|
|
|
|
const resp = await fetch(url);
|
|
|
|
if (!resp.ok) throw new Error(`unexpected response ${resp.statusText}`);
|
|
|
|
if (!resp.body) throw new Error(`unexpected response has no body ${resp.statusText}`);
|
|
|
|
pipeline(resp.body, fs.createWriteStream(filePath));
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
console.error(err);
|
|
|
|
dialog.showMessageBox({
|
|
|
|
type: "error",
|
|
|
|
title: _t("Failed to save image"),
|
|
|
|
message: _t("The image failed to save"),
|
|
|
|
});
|
2019-12-06 19:17:34 +01:00
|
|
|
}
|
2022-12-15 12:00:58 +01:00
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
2019-12-06 19:17:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// popup() requires an options object even for no options
|
|
|
|
popupMenu.popup({});
|
|
|
|
ev.preventDefault();
|
|
|
|
}
|
|
|
|
|
2021-07-01 10:22:57 +02:00
|
|
|
function cutCopyPasteSelectContextMenus(params: ContextMenuParams): MenuItemConstructorOptions[] {
|
2021-06-25 15:35:58 +02:00
|
|
|
const options: MenuItemConstructorOptions[] = [];
|
2020-02-24 18:16:35 +01:00
|
|
|
|
|
|
|
if (params.misspelledWord) {
|
2022-12-15 12:00:58 +01:00
|
|
|
params.dictionarySuggestions.forEach((word) => {
|
2020-02-24 18:16:35 +01:00
|
|
|
options.push({
|
|
|
|
label: word,
|
|
|
|
click: (menuItem, browserWindow) => {
|
2022-10-13 13:42:33 +02:00
|
|
|
browserWindow?.webContents.replaceMisspelling(word);
|
2020-02-24 18:16:35 +01:00
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
2022-12-15 12:00:58 +01:00
|
|
|
options.push(
|
|
|
|
{
|
|
|
|
type: "separator",
|
2020-02-24 18:16:35 +01:00
|
|
|
},
|
2022-12-15 12:00:58 +01:00
|
|
|
{
|
|
|
|
label: _t("Add to dictionary"),
|
|
|
|
click: (menuItem, browserWindow) => {
|
|
|
|
browserWindow?.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: "separator",
|
|
|
|
},
|
|
|
|
);
|
2020-02-24 18:16:35 +01:00
|
|
|
}
|
|
|
|
|
2022-12-15 12:00:58 +01:00
|
|
|
options.push(
|
|
|
|
{
|
|
|
|
role: "cut",
|
|
|
|
label: _t("Cut"),
|
|
|
|
accelerator: "t",
|
|
|
|
enabled: params.editFlags.canCut,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
role: "copy",
|
|
|
|
label: _t("Copy"),
|
|
|
|
accelerator: "c",
|
|
|
|
enabled: params.editFlags.canCopy,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
role: "paste",
|
|
|
|
label: _t("Paste"),
|
|
|
|
accelerator: "p",
|
|
|
|
enabled: params.editFlags.canPaste,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
role: "pasteAndMatchStyle",
|
|
|
|
enabled: params.editFlags.canPaste,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
role: "selectAll",
|
|
|
|
label: _t("Select All"),
|
|
|
|
accelerator: "a",
|
|
|
|
enabled: params.editFlags.canSelectAll,
|
|
|
|
},
|
|
|
|
);
|
2020-02-24 18:16:35 +01:00
|
|
|
return options;
|
2019-12-06 19:17:34 +01:00
|
|
|
}
|
|
|
|
|
2022-12-01 08:20:55 +01:00
|
|
|
function onSelectedContextMenu(ev: Event, params: ContextMenuParams): void {
|
2021-07-01 10:22:57 +02:00
|
|
|
const items = cutCopyPasteSelectContextMenus(params);
|
2019-12-06 19:17:34 +01:00
|
|
|
const popupMenu = Menu.buildFromTemplate(items);
|
|
|
|
|
|
|
|
// popup() requires an options object even for no options
|
|
|
|
popupMenu.popup({});
|
|
|
|
ev.preventDefault();
|
|
|
|
}
|
|
|
|
|
2022-12-01 08:20:55 +01:00
|
|
|
function onEditableContextMenu(ev: Event, params: ContextMenuParams): void {
|
2021-06-25 15:35:58 +02:00
|
|
|
const items: MenuItemConstructorOptions[] = [
|
2022-12-15 12:00:58 +01:00
|
|
|
{ role: "undo" },
|
|
|
|
{ role: "redo", enabled: params.editFlags.canRedo },
|
|
|
|
{ type: "separator" },
|
2021-07-01 10:22:57 +02:00
|
|
|
...cutCopyPasteSelectContextMenus(params),
|
2021-06-25 15:35:58 +02:00
|
|
|
];
|
2019-12-06 19:17:34 +01:00
|
|
|
|
|
|
|
const popupMenu = Menu.buildFromTemplate(items);
|
|
|
|
|
|
|
|
// popup() requires an options object even for no options
|
|
|
|
popupMenu.popup({});
|
|
|
|
ev.preventDefault();
|
|
|
|
}
|
|
|
|
|
2021-12-21 16:34:59 +01:00
|
|
|
let userDownloadIndex = 0;
|
|
|
|
const userDownloadMap = new Map<number, string>(); // Map from id to path
|
2022-12-15 12:00:58 +01:00
|
|
|
ipcMain.on("userDownloadAction", function (ev: IpcMainEvent, { id, open = false }) {
|
2022-10-13 13:42:33 +02:00
|
|
|
const path = userDownloadMap.get(id);
|
|
|
|
if (open && path) {
|
|
|
|
shell.openPath(path);
|
2021-12-21 16:34:59 +01:00
|
|
|
}
|
|
|
|
userDownloadMap.delete(id);
|
2020-06-29 17:08:39 +02:00
|
|
|
});
|
2019-12-06 19:17:34 +01:00
|
|
|
|
2021-06-25 15:35:58 +02:00
|
|
|
export default (webContents: WebContents): void => {
|
2022-12-07 01:04:53 +01:00
|
|
|
webContents.setWindowOpenHandler((details) => {
|
|
|
|
safeOpenURL(details.url);
|
|
|
|
return { action: "deny" };
|
|
|
|
});
|
|
|
|
|
2022-12-15 12:00:58 +01:00
|
|
|
webContents.on("will-navigate", (ev: Event, target: string): void => {
|
2020-03-02 16:04:51 +01:00
|
|
|
if (target.startsWith("vector://")) return;
|
|
|
|
return onWindowOrNavigate(ev, target);
|
|
|
|
});
|
2019-12-06 19:17:34 +01:00
|
|
|
|
2022-12-15 12:00:58 +01:00
|
|
|
webContents.on("context-menu", function (ev: Event, params: ContextMenuParams): void {
|
2019-12-06 19:17:34 +01:00
|
|
|
if (params.linkURL || params.srcURL) {
|
2021-06-25 15:35:58 +02:00
|
|
|
onLinkContextMenu(ev, params, webContents);
|
2019-12-06 19:17:34 +01:00
|
|
|
} else if (params.selectionText) {
|
|
|
|
onSelectedContextMenu(ev, params);
|
|
|
|
} else if (params.isEditable) {
|
|
|
|
onEditableContextMenu(ev, params);
|
|
|
|
}
|
|
|
|
});
|
2020-06-29 17:08:39 +02:00
|
|
|
|
2022-12-15 12:00:58 +01:00
|
|
|
webContents.session.on("will-download", (event: Event, item: DownloadItem): void => {
|
|
|
|
item.once("done", (event, state) => {
|
|
|
|
if (state === "completed") {
|
2020-06-29 17:08:39 +02:00
|
|
|
const savePath = item.getSavePath();
|
2021-12-21 16:34:59 +01:00
|
|
|
const id = userDownloadIndex++;
|
|
|
|
userDownloadMap.set(id, savePath);
|
2022-12-15 12:00:58 +01:00
|
|
|
webContents.send("userDownloadCompleted", {
|
2021-12-21 16:34:59 +01:00
|
|
|
id,
|
2020-06-29 17:08:39 +02:00
|
|
|
name: path.basename(savePath),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
2019-12-06 19:17:34 +01:00
|
|
|
};
|