Files
mattermostest/src/main/views/webContentEvents.ts
Devin Binnie 686b4ac9f1 [MM-50485] Migrate app to ServerManager, remove view names and replace with IDs (#2672)
* Migrate app to ServerManager, remove view names and replace with IDs

* Fixed a test

* Fixed a bug when adding the initial server

* Merge'd

* Bug fixes and PR feedback
2023-04-12 12:52:34 -04:00

331 lines
13 KiB
TypeScript

// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BrowserWindow, session, shell, WebContents} from 'electron';
import Config from 'common/config';
import {Logger} from 'common/log';
import urlUtils from 'common/utils/url';
import {flushCookiesStore} from 'main/app/utils';
import ContextMenu from 'main/contextMenu';
import ServerManager from 'common/servers/serverManager';
import MainWindow from 'main/windows/mainWindow';
import WindowManager from 'main/windows/windowManager';
import ViewManager from 'main/views/viewManager';
import CallsWidgetWindow from 'main/windows/callsWidgetWindow';
import {protocols} from '../../../electron-builder.json';
import allowProtocolDialog from '../allowProtocolDialog';
import {composeUserAgent} from '../utils';
type CustomLogin = {
inProgress: boolean;
}
const log = new Logger('WebContentsEventManager');
const scheme = protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0];
export class WebContentsEventManager {
customLogins: Record<number, CustomLogin>;
listeners: Record<number, () => void>;
popupWindow?: {win: BrowserWindow; serverURL?: URL};
constructor() {
this.customLogins = {};
this.listeners = {};
}
private log = (webContentsId?: number) => {
if (!webContentsId) {
return log;
}
const view = ViewManager.getViewByWebContentsId(webContentsId);
if (!view) {
return log;
}
return ServerManager.getViewLog(view.id, 'WebContentsEventManager');
}
private isTrustedPopupWindow = (webContentsId: number) => {
if (!this.popupWindow) {
return false;
}
return webContentsId === this.popupWindow.win.webContents.id;
}
private getServerURLFromWebContentsId = (webContentsId: number) => {
if (this.popupWindow && webContentsId === this.popupWindow.win.webContents.id) {
return this.popupWindow.serverURL;
}
return WindowManager.getServerURLFromWebContentsId(webContentsId);
}
private generateWillNavigate = (webContentsId: number) => {
return (event: Event, url: string) => {
this.log(webContentsId).debug('will-navigate', url);
const parsedURL = urlUtils.parseURL(url)!;
const serverURL = this.getServerURLFromWebContentsId(webContentsId);
if (serverURL && (urlUtils.isTeamUrl(serverURL, parsedURL) || urlUtils.isAdminUrl(serverURL, parsedURL) || this.isTrustedPopupWindow(webContentsId))) {
return;
}
if (serverURL && urlUtils.isChannelExportUrl(serverURL, parsedURL)) {
return;
}
if (serverURL && urlUtils.isCustomLoginURL(parsedURL, serverURL)) {
return;
}
if (parsedURL.protocol === 'mailto:') {
return;
}
if (this.customLogins[webContentsId]?.inProgress) {
flushCookiesStore(session.defaultSession);
return;
}
const callID = CallsWidgetWindow.callID;
if (serverURL && callID && urlUtils.isCallsPopOutURL(serverURL, parsedURL, callID)) {
return;
}
this.log(webContentsId).info(`Prevented desktop from navigating to: ${url}`);
event.preventDefault();
};
};
private generateDidStartNavigation = (webContentsId: number) => {
return (event: Event, url: string) => {
this.log(webContentsId).debug('did-start-navigation', url);
const parsedURL = urlUtils.parseURL(url)!;
const serverURL = this.getServerURLFromWebContentsId(webContentsId);
if (!serverURL || !urlUtils.isTrustedURL(parsedURL, serverURL)) {
return;
}
if (serverURL && urlUtils.isCustomLoginURL(parsedURL, serverURL)) {
this.customLogins[webContentsId].inProgress = true;
} else if (serverURL && this.customLogins[webContentsId].inProgress && urlUtils.isInternalURL(serverURL || new URL(''), parsedURL)) {
this.customLogins[webContentsId].inProgress = false;
}
};
};
private denyNewWindow = (details: Electron.HandlerDetails): {action: 'deny' | 'allow'} => {
this.log().warn(`Prevented popup window to open a new window to ${details.url}.`);
return {action: 'deny'};
};
private generateNewWindowListener = (webContentsId: number, spellcheck?: boolean) => {
return (details: Electron.HandlerDetails): {action: 'deny' | 'allow'} => {
this.log(webContentsId).debug('new-window', details.url);
const parsedURL = urlUtils.parseURL(details.url);
if (!parsedURL) {
this.log(webContentsId).warn(`Ignoring non-url ${details.url}`);
return {action: 'deny'};
}
// Dev tools case
if (parsedURL.protocol === 'devtools:') {
return {action: 'allow'};
}
// Check for custom protocol
if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:' && parsedURL.protocol !== `${scheme}:`) {
allowProtocolDialog.handleDialogEvent(parsedURL.protocol, details.url);
return {action: 'deny'};
}
// Check for valid URL
// Let the browser handle invalid URIs
if (!urlUtils.isValidURI(details.url)) {
shell.openExternal(details.url);
return {action: 'deny'};
}
const serverURL = this.getServerURLFromWebContentsId(webContentsId);
if (!serverURL) {
shell.openExternal(details.url);
return {action: 'deny'};
}
// Public download links case
// TODO: We might be handling different types differently in the future, for now
// we are going to mimic the browser and just pop a new browser window for public links
if (parsedURL.pathname.match(/^(\/api\/v[3-4]\/public)*\/files\//)) {
shell.openExternal(details.url);
return {action: 'deny'};
}
// Image proxy case
if (parsedURL.pathname.match(/^\/api\/v[3-4]\/image/)) {
shell.openExternal(details.url);
return {action: 'deny'};
}
if (parsedURL.pathname.match(/^\/help\//)) {
// Help links case
// continue to open special case internal urls in default browser
shell.openExternal(details.url);
return {action: 'deny'};
}
if (urlUtils.isTeamUrl(serverURL, parsedURL, true)) {
WindowManager.showMainWindow(parsedURL);
return {action: 'deny'};
}
if (urlUtils.isAdminUrl(serverURL, parsedURL)) {
this.log(webContentsId).info(`${details.url} is an admin console page, preventing to open a new window`);
return {action: 'deny'};
}
if (this.popupWindow && this.popupWindow.win.webContents.getURL() === details.url) {
this.log(webContentsId).info(`Popup window already open at provided url: ${details.url}`);
return {action: 'deny'};
}
// TODO: move popups to its own and have more than one.
if (urlUtils.isPluginUrl(serverURL, parsedURL) || urlUtils.isManagedResource(serverURL, parsedURL)) {
let popup: BrowserWindow;
if (this.popupWindow) {
this.popupWindow.win.once('ready-to-show', () => {
this.popupWindow?.win.show();
});
popup = this.popupWindow.win;
} else {
this.popupWindow = {
win: new BrowserWindow({
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
parent: MainWindow.get(),
show: false,
center: true,
webPreferences: {
spellcheck: (typeof spellcheck === 'undefined' ? true : spellcheck),
},
}),
serverURL,
};
this.customLogins[this.popupWindow.win.webContents.id] = {
inProgress: false,
};
popup = this.popupWindow.win;
popup.webContents.on('will-redirect', (event, url) => {
const parsedURL = urlUtils.parseURL(url);
if (!parsedURL) {
event.preventDefault();
return;
}
if (urlUtils.isInternalURL(serverURL, parsedURL) && !urlUtils.isPluginUrl(serverURL, parsedURL) && !urlUtils.isManagedResource(serverURL, parsedURL)) {
event.preventDefault();
}
});
popup.webContents.on('will-navigate', this.generateWillNavigate(popup.webContents.id));
popup.webContents.on('did-start-navigation', this.generateDidStartNavigation(popup.webContents.id));
popup.webContents.setWindowOpenHandler(this.denyNewWindow);
popup.once('closed', () => {
this.popupWindow = undefined;
});
const contextMenu = new ContextMenu({}, popup);
contextMenu.reload();
}
popup.once('ready-to-show', () => popup.show());
if (urlUtils.isManagedResource(serverURL, parsedURL)) {
popup.loadURL(details.url);
} else {
// currently changing the userAgent for popup windows to allow plugins to go through google's oAuth
// should be removed once a proper oAuth2 implementation is setup.
popup.loadURL(details.url, {
userAgent: composeUserAgent(),
});
}
return {action: 'deny'};
}
const otherServerURL = ServerManager.lookupTabByURL(parsedURL);
if (otherServerURL && urlUtils.isTeamUrl(otherServerURL.server.url, parsedURL, true)) {
WindowManager.showMainWindow(parsedURL);
return {action: 'deny'};
}
// If all else fails, just open externally
shell.openExternal(details.url);
return {action: 'deny'};
};
};
removeWebContentsListeners = (id: number) => {
if (this.listeners[id]) {
this.listeners[id]();
}
};
addWebContentsEventListeners = (
contents: WebContents,
addListeners?: (contents: WebContents) => void,
removeListeners?: (contents: WebContents) => void,
) => {
// initialize custom login tracking
this.customLogins[contents.id] = {
inProgress: false,
};
if (this.listeners[contents.id]) {
this.removeWebContentsListeners(contents.id);
}
const willNavigate = this.generateWillNavigate(contents.id);
contents.on('will-navigate', willNavigate);
// handle custom login requests (oath, saml):
// 1. are we navigating to a supported local custom login path from the `/login` page?
// - indicate custom login is in progress
// 2. are we finished with the custom login process?
// - indicate custom login is NOT in progress
const didStartNavigation = this.generateDidStartNavigation(contents.id);
contents.on('did-start-navigation', didStartNavigation);
const spellcheck = Config.useSpellChecker;
const newWindow = this.generateNewWindowListener(contents.id, spellcheck);
contents.setWindowOpenHandler(newWindow);
addListeners?.(contents);
const removeWebContentsListeners = () => {
try {
contents.removeListener('will-navigate', willNavigate);
contents.removeListener('did-start-navigation', didStartNavigation);
removeListeners?.(contents);
} catch (e) {
this.log(contents.id).error(`Error while trying to detach listeners, this might be ok if the process crashed: ${e}`);
}
};
this.listeners[contents.id] = removeWebContentsListeners;
contents.once('render-process-gone', (event, details) => {
if (details.reason !== 'clean-exit') {
this.log(contents.id).error('Renderer process for a webcontent is no longer available:', details.reason);
}
removeWebContentsListeners();
});
};
}
const webContentsEventManager = new WebContentsEventManager();
export default webContentsEventManager;