2020-06-29 17:08:39 +02:00
|
|
|
const {clipboard, nativeImage, Menu, MenuItem, shell, dialog, ipcMain} = require('electron');
|
2019-12-06 19:17:34 +01:00
|
|
|
const url = require('url');
|
|
|
|
const fs = require('fs');
|
|
|
|
const request = require('request');
|
2020-06-29 17:08:39 +02:00
|
|
|
const path = require('path');
|
2021-04-26 15:50:18 +02:00
|
|
|
const { _t } = require('./language-helper');
|
2019-12-06 19:17:34 +01:00
|
|
|
|
|
|
|
const MAILTO_PREFIX = "mailto:";
|
|
|
|
|
|
|
|
const PERMITTED_URL_SCHEMES = [
|
|
|
|
'http:',
|
|
|
|
'https:',
|
|
|
|
MAILTO_PREFIX,
|
|
|
|
];
|
|
|
|
|
|
|
|
function safeOpenURL(target) {
|
|
|
|
// 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);
|
|
|
|
if (PERMITTED_URL_SCHEMES.indexOf(parsedUrl.protocol) > -1) {
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function onWindowOrNavigate(ev, target) {
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
2020-05-31 13:23:23 +02:00
|
|
|
function writeNativeImage(filePath, img) {
|
|
|
|
switch (filePath.split('.').pop().toLowerCase()) {
|
|
|
|
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());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-12-06 19:17:34 +01:00
|
|
|
function onLinkContextMenu(ev, params) {
|
|
|
|
let url = params.linkURL || params.srcURL;
|
|
|
|
|
|
|
|
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.
|
|
|
|
if (!url.startsWith('blob:')) {
|
|
|
|
popupMenu.append(new MenuItem({
|
|
|
|
label: url,
|
|
|
|
click() {
|
|
|
|
safeOpenURL(url);
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2020-05-31 13:23:23 +02:00
|
|
|
if (params.hasImageContents) {
|
2019-12-06 19:17:34 +01:00
|
|
|
popupMenu.append(new MenuItem({
|
2021-04-26 15:50:18 +02:00
|
|
|
label: _t('Copy image'),
|
2021-04-23 17:56:17 +02:00
|
|
|
accelerator: 'c',
|
2019-12-06 19:17:34 +01:00
|
|
|
click() {
|
2020-05-31 13:23:23 +02:00
|
|
|
ev.sender.copyImageAt(params.x, params.y);
|
2019-12-06 19:17:34 +01:00
|
|
|
},
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
// No point offering to copy a blob: URL either
|
|
|
|
if (!url.startsWith('blob:')) {
|
|
|
|
// Special-case e-mail URLs to strip the `mailto:` like modern browsers do
|
|
|
|
if (url.startsWith(MAILTO_PREFIX)) {
|
|
|
|
popupMenu.append(new MenuItem({
|
2021-04-26 15:50:18 +02:00
|
|
|
label: _t('Copy email address'),
|
2021-04-23 17:56:17 +02:00
|
|
|
accelerator: 'a',
|
2019-12-06 19:17:34 +01:00
|
|
|
click() {
|
|
|
|
clipboard.writeText(url.substr(MAILTO_PREFIX.length));
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
} else {
|
|
|
|
popupMenu.append(new MenuItem({
|
2021-04-26 15:50:18 +02:00
|
|
|
label: _t('Copy link address'),
|
2021-04-23 17:56:17 +02:00
|
|
|
accelerator: 'a',
|
2019-12-06 19:17:34 +01:00
|
|
|
click() {
|
|
|
|
clipboard.writeText(url);
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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.
|
2020-05-31 13:23:23 +02:00
|
|
|
if (params.hasImageContents && !url.startsWith('blob:')) {
|
2019-12-06 19:17:34 +01:00
|
|
|
popupMenu.append(new MenuItem({
|
2021-04-26 15:50:18 +02:00
|
|
|
label: _t('Save image as...'),
|
2021-04-23 17:56:17 +02:00
|
|
|
accelerator: 'a',
|
2020-05-31 13:23:23 +02:00
|
|
|
async click() {
|
2019-12-06 19:17:34 +01:00
|
|
|
const targetFileName = params.titleText || "image.png";
|
2020-05-31 13:23:23 +02:00
|
|
|
const {filePath} = await dialog.showSaveDialog({
|
2019-12-06 19:17:34 +01:00
|
|
|
defaultPath: targetFileName,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!filePath) return; // user cancelled dialog
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (url.startsWith("data:")) {
|
2020-05-31 13:23:23 +02:00
|
|
|
await writeNativeImage(filePath, nativeImage.createFromDataURL(url));
|
2019-12-06 19:17:34 +01:00
|
|
|
} else {
|
|
|
|
request.get(url).pipe(fs.createWriteStream(filePath));
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
console.error(err);
|
|
|
|
dialog.showMessageBox({
|
|
|
|
type: "error",
|
2021-04-26 15:50:18 +02:00
|
|
|
title: _t("Failed to save image"),
|
|
|
|
message: _t("The image failed to save"),
|
2019-12-06 19:17:34 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
// popup() requires an options object even for no options
|
|
|
|
popupMenu.popup({});
|
|
|
|
ev.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
function _CutCopyPasteSelectContextMenus(params) {
|
2020-02-24 18:16:35 +01:00
|
|
|
const options = [];
|
|
|
|
|
|
|
|
if (params.misspelledWord) {
|
|
|
|
params.dictionarySuggestions.forEach(word => {
|
|
|
|
options.push({
|
|
|
|
label: word,
|
|
|
|
click: (menuItem, browserWindow) => {
|
|
|
|
browserWindow.webContents.replaceMisspelling(word);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
options.push({
|
|
|
|
type: 'separator',
|
|
|
|
}, {
|
2021-04-26 15:50:18 +02:00
|
|
|
label: _t('Add to dictionary'),
|
2020-02-24 18:16:35 +01:00
|
|
|
click: (menuItem, browserWindow) => {
|
|
|
|
browserWindow.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord);
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
type: 'separator',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
options.push({
|
2019-12-06 19:17:34 +01:00
|
|
|
role: 'cut',
|
2021-04-26 15:50:18 +02:00
|
|
|
label: _t('Cut'),
|
2021-04-23 17:56:17 +02:00
|
|
|
accelerator: 't',
|
2019-12-06 19:17:34 +01:00
|
|
|
enabled: params.editFlags.canCut,
|
|
|
|
}, {
|
|
|
|
role: 'copy',
|
2021-04-26 15:50:18 +02:00
|
|
|
label: _t('Copy'),
|
2021-04-23 17:56:17 +02:00
|
|
|
accelerator: 'c',
|
2019-12-06 19:17:34 +01:00
|
|
|
enabled: params.editFlags.canCopy,
|
|
|
|
}, {
|
|
|
|
role: 'paste',
|
2021-04-26 15:50:18 +02:00
|
|
|
label: _t('Paste'),
|
2021-04-23 17:56:17 +02:00
|
|
|
accelerator: 'p',
|
2019-12-06 19:17:34 +01:00
|
|
|
enabled: params.editFlags.canPaste,
|
|
|
|
}, {
|
|
|
|
role: 'pasteandmatchstyle',
|
|
|
|
enabled: params.editFlags.canPaste,
|
|
|
|
}, {
|
|
|
|
role: 'selectall',
|
2021-04-26 15:50:18 +02:00
|
|
|
label: _t("Select All"),
|
2021-04-23 17:56:17 +02:00
|
|
|
accelerator: 'a',
|
2019-12-06 19:17:34 +01:00
|
|
|
enabled: params.editFlags.canSelectAll,
|
2020-02-24 18:16:35 +01:00
|
|
|
});
|
|
|
|
return options;
|
2019-12-06 19:17:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
function onSelectedContextMenu(ev, params) {
|
|
|
|
const items = _CutCopyPasteSelectContextMenus(params);
|
|
|
|
const popupMenu = Menu.buildFromTemplate(items);
|
|
|
|
|
|
|
|
// popup() requires an options object even for no options
|
|
|
|
popupMenu.popup({});
|
|
|
|
ev.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
function onEditableContextMenu(ev, params) {
|
|
|
|
const items = [
|
|
|
|
{ role: 'undo' },
|
|
|
|
{ role: 'redo', enabled: params.editFlags.canRedo },
|
|
|
|
{ type: 'separator' },
|
|
|
|
].concat(_CutCopyPasteSelectContextMenus(params));
|
|
|
|
|
|
|
|
const popupMenu = Menu.buildFromTemplate(items);
|
|
|
|
|
|
|
|
// popup() requires an options object even for no options
|
|
|
|
popupMenu.popup({});
|
|
|
|
ev.preventDefault();
|
|
|
|
}
|
|
|
|
|
2020-06-29 17:08:39 +02:00
|
|
|
ipcMain.on('userDownloadOpen', function(ev, {path}) {
|
|
|
|
shell.openPath(path);
|
|
|
|
});
|
2019-12-06 19:17:34 +01:00
|
|
|
|
|
|
|
module.exports = (webContents) => {
|
|
|
|
webContents.on('new-window', onWindowOrNavigate);
|
2020-03-02 16:04:51 +01:00
|
|
|
webContents.on('will-navigate', (ev, target) => {
|
|
|
|
if (target.startsWith("vector://")) return;
|
|
|
|
return onWindowOrNavigate(ev, target);
|
|
|
|
});
|
2019-12-06 19:17:34 +01:00
|
|
|
|
|
|
|
webContents.on('context-menu', function(ev, params) {
|
|
|
|
if (params.linkURL || params.srcURL) {
|
|
|
|
onLinkContextMenu(ev, params);
|
|
|
|
} else if (params.selectionText) {
|
|
|
|
onSelectedContextMenu(ev, params);
|
|
|
|
} else if (params.isEditable) {
|
|
|
|
onEditableContextMenu(ev, params);
|
|
|
|
}
|
|
|
|
});
|
2020-06-29 17:08:39 +02:00
|
|
|
|
|
|
|
webContents.session.on('will-download', (event, item) => {
|
|
|
|
item.once('done', (event, state) => {
|
|
|
|
if (state === 'completed') {
|
|
|
|
const savePath = item.getSavePath();
|
|
|
|
webContents.send('userDownloadCompleted', {
|
|
|
|
path: savePath,
|
|
|
|
name: path.basename(savePath),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
2019-12-06 19:17:34 +01:00
|
|
|
};
|