[MM-52587] Clean up URL utils, use isInternalURL when possible (#2702)

This commit is contained in:
Devin Binnie
2023-05-03 08:48:41 -04:00
committed by GitHub
parent f3a4417464
commit e227c6bf1d
30 changed files with 481 additions and 634 deletions

View File

@@ -7,7 +7,6 @@ import {BrowserViewConstructorOptions, Event, Input} from 'electron/main';
import {EventEmitter} from 'events';
import {RELOAD_INTERVAL, MAX_SERVER_RETRIES, SECOND, MAX_LOADING_SCREEN_SECONDS} from 'common/utils/constants';
import urlUtils from 'common/utils/url';
import AppState from 'common/appState';
import {
LOAD_RETRY,
@@ -23,6 +22,7 @@ import {
} from 'common/communication';
import ServerManager from 'common/servers/serverManager';
import {Logger} from 'common/log';
import {isInternalURL, parseURL} from 'common/utils/url';
import {TabView} from 'common/tabs/TabView';
import MainWindow from 'main/windows/mainWindow';
@@ -114,7 +114,7 @@ export class MattermostView extends EventEmitter {
return this.loggedIn;
}
get currentURL() {
return this.view.webContents.getURL();
return parseURL(this.view.webContents.getURL());
}
get webContentsId() {
return this.view.webContents.id;
@@ -129,8 +129,8 @@ export class MattermostView extends EventEmitter {
// If we're logging in from a different tab, force a reload
if (loggedIn &&
this.currentURL !== this.tab.url.toString() &&
!this.currentURL.startsWith(this.tab.url.toString())
this.currentURL?.toString() !== this.tab.url.toString() &&
!this.currentURL?.toString().startsWith(this.tab.url.toString())
) {
this.reload();
}
@@ -149,7 +149,7 @@ export class MattermostView extends EventEmitter {
}
updateHistoryButton = () => {
if (urlUtils.parseURL(this.currentURL)?.toString() === this.tab.url.toString()) {
if (this.currentURL?.toString() === this.tab.url.toString()) {
this.view.webContents.clearHistory();
this.atRoot = true;
} else {
@@ -165,7 +165,7 @@ export class MattermostView extends EventEmitter {
let loadURL: string;
if (someURL) {
const parsedURL = urlUtils.parseURL(someURL);
const parsedURL = parseURL(someURL);
if (parsedURL) {
loadURL = parsedURL.toString();
} else {
@@ -198,6 +198,9 @@ export class MattermostView extends EventEmitter {
if (!mainWindow) {
return;
}
if (!this.currentURL) {
return;
}
if (this.isVisible) {
return;
}
@@ -435,7 +438,7 @@ export class MattermostView extends EventEmitter {
this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true);
this.emit(LOAD_SUCCESS, this.id, loadURL);
const mainWindow = MainWindow.get();
if (mainWindow) {
if (mainWindow && this.currentURL) {
this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.tab.url || '', this.currentURL)));
}
};
@@ -472,8 +475,12 @@ export class MattermostView extends EventEmitter {
if (!mainWindow) {
return;
}
const parsedURL = parseURL(url);
if (!parsedURL) {
return;
}
if (shouldHaveBackBar(this.tab.url || '', url)) {
if (shouldHaveBackBar(this.tab.url || '', parsedURL)) {
this.setBounds(getWindowBoundaries(mainWindow, true));
MainWindow.sendToRenderer(TOGGLE_BACK_BUTTON, true);
this.log.debug('show back button');
@@ -486,10 +493,11 @@ export class MattermostView extends EventEmitter {
private handleUpdateTarget = (e: Event, url: string) => {
this.log.silly('handleUpdateTarget', url);
if (url && !urlUtils.isInternalURL(urlUtils.parseURL(url), this.tab.server.url)) {
this.emit(UPDATE_TARGET_URL, url);
} else {
const parsedURL = parseURL(url);
if (parsedURL && isInternalURL(parsedURL, this.tab.server.url)) {
this.emit(UPDATE_TARGET_URL);
} else {
this.emit(UPDATE_TARGET_URL, url);
}
}

View File

@@ -29,11 +29,11 @@ import {
} from 'common/communication';
import Config from 'common/config';
import {Logger} from 'common/log';
import urlUtils from 'common/utils/url';
import Utils from 'common/utils/util';
import {MattermostServer} from 'common/servers/MattermostServer';
import ServerManager from 'common/servers/serverManager';
import {TabView, TAB_MESSAGING} from 'common/tabs/TabView';
import {parseURL} from 'common/utils/url';
import {localizeMessage} from 'main/i18nManager';
import MainWindow from 'main/windows/mainWindow';
@@ -174,7 +174,7 @@ export class ViewManager {
handleDeepLink = (url: string | URL) => {
if (url) {
const parsedURL = urlUtils.parseURL(url)!;
const parsedURL = parseURL(url)!;
const tabView = ServerManager.lookupTabByURL(parsedURL, true);
if (tabView) {
const urlWithSchema = `${tabView.url.origin}${parsedURL.pathname}${parsedURL.search}`;
@@ -471,11 +471,17 @@ export class ViewManager {
log.debug('handleBrowserHistoryPush', {viewId, pathName});
const currentView = this.getView(viewId);
const cleanedPathName = urlUtils.cleanPathName(currentView?.tab.server.url.pathname || '', pathName);
const redirectedviewId = ServerManager.lookupTabByURL(`${currentView?.tab.server.url.toString().replace(/\/$/, '')}${cleanedPathName}`)?.id || viewId;
if (!currentView) {
return;
}
let cleanedPathName = pathName;
if (currentView.tab.server.url.pathname !== '/' && pathName.startsWith(currentView.tab.server.url.pathname)) {
cleanedPathName = pathName.replace(currentView.tab.server.url.pathname, '');
}
const redirectedviewId = ServerManager.lookupTabByURL(`${currentView.tab.server.url.toString().replace(/\/$/, '')}${cleanedPathName}`)?.id || viewId;
if (this.isViewClosed(redirectedviewId)) {
// If it's a closed view, just open it and stop
this.openClosedTab(redirectedviewId, `${currentView?.tab.server.url}${cleanedPathName}`);
this.openClosedTab(redirectedviewId, `${currentView.tab.server.url}${cleanedPathName}`);
return;
}
let redirectedView = this.getView(redirectedviewId) || currentView;
@@ -540,7 +546,7 @@ export class ViewManager {
log.debug('handleSetCurrentViewBounds', newBounds);
const currentView = this.getCurrentView();
if (currentView) {
if (currentView && currentView.currentURL) {
const adjustedBounds = getAdjustedWindowBoundaries(newBounds.width, newBounds.height, shouldHaveBackBar(currentView.tab.url, currentView.currentURL));
currentView.setBounds(adjustedBounds);
}

View File

@@ -5,8 +5,6 @@
import {shell, BrowserWindow} from 'electron';
import urlUtils from 'common/utils/url';
import ContextMenu from 'main/contextMenu';
import ViewManager from 'main/views/viewManager';
@@ -40,27 +38,6 @@ jest.mock('common/config', () => ({
spellcheck: true,
}));
jest.mock('common/utils/url', () => ({
parseURL: (url) => {
try {
return new URL(url);
} catch (e) {
return null;
}
},
getView: jest.fn(),
isTeamUrl: jest.fn(),
isAdminUrl: jest.fn(),
isTrustedPopupWindow: jest.fn(),
isTrustedURL: jest.fn(),
isCustomLoginURL: jest.fn(),
isInternalURL: jest.fn(),
isValidURI: jest.fn(),
isPluginUrl: jest.fn(),
isManagedResource: jest.fn(),
isChannelExportUrl: jest.fn(),
}));
jest.mock('main/app/utils', () => ({
flushCookiesStore: jest.fn(),
}));
@@ -97,13 +74,11 @@ describe('main/views/webContentsEvents', () => {
});
it('should allow navigation when url isTeamURL', () => {
urlUtils.isTeamUrl.mockImplementation((serverURL, parsedURL) => parsedURL.toString().startsWith(serverURL));
willNavigate(event, 'http://server-1.com/subpath');
expect(event.preventDefault).not.toBeCalled();
});
it('should allow navigation when url isAdminURL', () => {
urlUtils.isAdminUrl.mockImplementation((serverURL, parsedURL) => parsedURL.toString().startsWith(`${serverURL}admin_console`));
willNavigate(event, 'http://server-1.com/admin_console/subpath');
expect(event.preventDefault).not.toBeCalled();
});
@@ -116,11 +91,15 @@ describe('main/views/webContentsEvents', () => {
});
it('should allow navigation when isCustomLoginURL', () => {
urlUtils.isCustomLoginURL.mockImplementation((parsedURL) => parsedURL.toString().startsWith('http://loginurl.com/login'));
willNavigate(event, 'http://loginurl.com/login/oauth');
willNavigate(event, 'http://server-1.com/oauth/authorize');
expect(event.preventDefault).not.toBeCalled();
});
it('should not allow navigation when isCustomLoginURL is external', () => {
willNavigate(event, 'http://loginurl.com/oauth/authorize');
expect(event.preventDefault).toBeCalled();
});
it('should allow navigation when protocol is mailto', () => {
willNavigate(event, 'mailto:test@mattermost.com');
expect(event.preventDefault).not.toBeCalled();
@@ -133,7 +112,6 @@ describe('main/views/webContentsEvents', () => {
});
it('should allow navigation when it isChannelExportUrl', () => {
urlUtils.isChannelExportUrl.mockImplementation((serverURL, parsedURL) => parsedURL.toString().includes('/plugins/com.mattermost.plugin-channel-export/api/v1/export'));
willNavigate(event, 'http://server-1.com/plugins/com.mattermost.plugin-channel-export/api/v1/export');
expect(event.preventDefault).not.toBeCalled();
});
@@ -150,9 +128,6 @@ describe('main/views/webContentsEvents', () => {
beforeEach(() => {
webContentsEventManager.getServerURLFromWebContentsId = jest.fn().mockImplementation(() => new URL('http://server-1.com'));
urlUtils.isTrustedURL.mockReturnValue(true);
urlUtils.isInternalURL.mockImplementation((serverURL, parsedURL) => parsedURL.toString().startsWith(serverURL));
urlUtils.isCustomLoginURL.mockImplementation((parsedURL) => parsedURL.toString().startsWith('http://loginurl.com/login'));
});
afterEach(() => {
@@ -162,7 +137,7 @@ describe('main/views/webContentsEvents', () => {
it('should add custom login entry on custom login URL', () => {
webContentsEventManager.customLogins[1] = {inProgress: false};
didStartNavigation(event, 'http://loginurl.com/login/oauth');
didStartNavigation(event, 'http://server-1.com/oauth/authorize');
expect(webContentsEventManager.customLogins[1]).toStrictEqual({inProgress: true});
});
@@ -178,12 +153,7 @@ describe('main/views/webContentsEvents', () => {
const newWindow = webContentsEventManager.generateNewWindowListener(1, true);
beforeEach(() => {
urlUtils.isValidURI.mockReturnValue(true);
webContentsEventManager.getServerURLFromWebContentsId = jest.fn().mockImplementation(() => new URL('http://server-1.com'));
urlUtils.isTeamUrl.mockImplementation((serverURL, parsedURL) => parsedURL.toString().startsWith(`${serverURL}myteam`));
urlUtils.isAdminUrl.mockImplementation((serverURL, parsedURL) => parsedURL.toString().startsWith(`${serverURL}admin_console`));
urlUtils.isPluginUrl.mockImplementation((serverURL, parsedURL) => parsedURL.toString().startsWith(`${serverURL}myplugin`));
urlUtils.isManagedResource.mockImplementation((serverURL, parsedURL) => parsedURL.toString().startsWith(`${serverURL}trusted`));
BrowserWindow.mockImplementation(() => ({
once: jest.fn(),
@@ -212,7 +182,6 @@ describe('main/views/webContentsEvents', () => {
});
it('should open invalid URIs in browser', () => {
urlUtils.isValidURI.mockReturnValue(false);
expect(newWindow({url: 'https://google.com/?^'})).toStrictEqual({action: 'deny'});
expect(shell.openExternal).toBeCalledWith('https://google.com/?^');
});
@@ -258,7 +227,7 @@ describe('main/views/webContentsEvents', () => {
});
it('should open popup window for plugins', () => {
expect(newWindow({url: 'http://server-1.com/myplugin/login'})).toStrictEqual({action: 'deny'});
expect(newWindow({url: 'http://server-1.com/plugins/myplugin/login'})).toStrictEqual({action: 'deny'});
expect(webContentsEventManager.popupWindow).toBeTruthy();
});
@@ -268,7 +237,6 @@ describe('main/views/webContentsEvents', () => {
});
it('should open external URIs in browser', () => {
urlUtils.isValidURI.mockReturnValue(false);
expect(newWindow({url: 'https://google.com'})).toStrictEqual({action: 'deny'});
expect(shell.openExternal).toBeCalledWith('https://google.com');
});

View File

@@ -5,11 +5,26 @@ import {BrowserWindow, session, shell, WebContents} from 'electron';
import Config from 'common/config';
import {Logger} from 'common/log';
import urlUtils from 'common/utils/url';
import ServerManager from 'common/servers/serverManager';
import {
isAdminUrl,
isCallsPopOutURL,
isChannelExportUrl,
isCustomLoginURL,
isHelpUrl,
isImageProxyUrl,
isInternalURL,
isManagedResource,
isPluginUrl,
isPublicFilesUrl,
isTeamUrl,
isTrustedURL,
isValidURI,
parseURL,
} 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 ViewManager from 'main/views/viewManager';
@@ -73,18 +88,18 @@ export class WebContentsEventManager {
return (event: Event, url: string) => {
this.log(webContentsId).debug('will-navigate', url);
const parsedURL = urlUtils.parseURL(url)!;
const parsedURL = parseURL(url)!;
const serverURL = this.getServerURLFromWebContentsId(webContentsId);
if (serverURL && (urlUtils.isTeamUrl(serverURL, parsedURL) || urlUtils.isAdminUrl(serverURL, parsedURL) || this.isTrustedPopupWindow(webContentsId))) {
if (serverURL && (isTeamUrl(serverURL, parsedURL) || isAdminUrl(serverURL, parsedURL) || this.isTrustedPopupWindow(webContentsId))) {
return;
}
if (serverURL && urlUtils.isChannelExportUrl(serverURL, parsedURL)) {
if (serverURL && isChannelExportUrl(serverURL, parsedURL)) {
return;
}
if (serverURL && urlUtils.isCustomLoginURL(parsedURL, serverURL)) {
if (serverURL && isCustomLoginURL(parsedURL, serverURL)) {
return;
}
if (parsedURL.protocol === 'mailto:') {
@@ -96,7 +111,7 @@ export class WebContentsEventManager {
}
const callID = CallsWidgetWindow.callID;
if (serverURL && callID && urlUtils.isCallsPopOutURL(serverURL, parsedURL, callID)) {
if (serverURL && callID && isCallsPopOutURL(serverURL, parsedURL, callID)) {
return;
}
@@ -109,16 +124,16 @@ export class WebContentsEventManager {
return (event: Event, url: string) => {
this.log(webContentsId).debug('did-start-navigation', url);
const parsedURL = urlUtils.parseURL(url)!;
const parsedURL = parseURL(url)!;
const serverURL = this.getServerURLFromWebContentsId(webContentsId);
if (!serverURL || !urlUtils.isTrustedURL(parsedURL, serverURL)) {
if (!serverURL || !isTrustedURL(parsedURL, serverURL)) {
return;
}
if (serverURL && urlUtils.isCustomLoginURL(parsedURL, serverURL)) {
if (serverURL && isCustomLoginURL(parsedURL, serverURL)) {
this.customLogins[webContentsId].inProgress = true;
} else if (serverURL && this.customLogins[webContentsId].inProgress && urlUtils.isInternalURL(serverURL || new URL(''), parsedURL)) {
} else if (serverURL && this.customLogins[webContentsId].inProgress && isInternalURL(serverURL || new URL(''), parsedURL)) {
this.customLogins[webContentsId].inProgress = false;
}
};
@@ -133,7 +148,7 @@ export class WebContentsEventManager {
return (details: Electron.HandlerDetails): {action: 'deny' | 'allow'} => {
this.log(webContentsId).debug('new-window', details.url);
const parsedURL = urlUtils.parseURL(details.url);
const parsedURL = parseURL(details.url);
if (!parsedURL) {
this.log(webContentsId).warn(`Ignoring non-url ${details.url}`);
return {action: 'deny'};
@@ -152,7 +167,7 @@ export class WebContentsEventManager {
// Check for valid URL
// Let the browser handle invalid URIs
if (!urlUtils.isValidURI(details.url)) {
if (!isValidURI(details.url)) {
shell.openExternal(details.url);
return {action: 'deny'};
}
@@ -164,31 +179,30 @@ export class WebContentsEventManager {
}
// 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\//)) {
if (isPublicFilesUrl(serverURL, parsedURL)) {
shell.openExternal(details.url);
return {action: 'deny'};
}
// Image proxy case
if (parsedURL.pathname.match(/^\/api\/v[3-4]\/image/)) {
if (isImageProxyUrl(serverURL, parsedURL)) {
shell.openExternal(details.url);
return {action: 'deny'};
}
if (parsedURL.pathname.match(/^\/help\//)) {
if (isHelpUrl(serverURL, parsedURL)) {
// 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)) {
if (isTeamUrl(serverURL, parsedURL, true)) {
ViewManager.handleDeepLink(parsedURL);
return {action: 'deny'};
}
if (urlUtils.isAdminUrl(serverURL, parsedURL)) {
if (isAdminUrl(serverURL, parsedURL)) {
this.log(webContentsId).info(`${details.url} is an admin console page, preventing to open a new window`);
return {action: 'deny'};
}
@@ -198,7 +212,7 @@ export class WebContentsEventManager {
}
// TODO: move popups to its own and have more than one.
if (urlUtils.isPluginUrl(serverURL, parsedURL) || urlUtils.isManagedResource(serverURL, parsedURL)) {
if (isPluginUrl(serverURL, parsedURL) || isManagedResource(serverURL, parsedURL)) {
let popup: BrowserWindow;
if (this.popupWindow) {
this.popupWindow.win.once('ready-to-show', () => {
@@ -224,13 +238,13 @@ export class WebContentsEventManager {
popup = this.popupWindow.win;
popup.webContents.on('will-redirect', (event, url) => {
const parsedURL = urlUtils.parseURL(url);
const parsedURL = parseURL(url);
if (!parsedURL) {
event.preventDefault();
return;
}
if (urlUtils.isInternalURL(serverURL, parsedURL) && !urlUtils.isPluginUrl(serverURL, parsedURL) && !urlUtils.isManagedResource(serverURL, parsedURL)) {
if (isInternalURL(serverURL, parsedURL) && !isPluginUrl(serverURL, parsedURL) && !isManagedResource(serverURL, parsedURL)) {
event.preventDefault();
}
});
@@ -247,7 +261,7 @@ export class WebContentsEventManager {
popup.once('ready-to-show', () => popup.show());
if (urlUtils.isManagedResource(serverURL, parsedURL)) {
if (isManagedResource(serverURL, parsedURL)) {
popup.loadURL(details.url);
} else {
// currently changing the userAgent for popup windows to allow plugins to go through google's oAuth
@@ -261,7 +275,7 @@ export class WebContentsEventManager {
}
const otherServerURL = ServerManager.lookupTabByURL(parsedURL);
if (otherServerURL && urlUtils.isTeamUrl(otherServerURL.server.url, parsedURL, true)) {
if (otherServerURL && isTeamUrl(otherServerURL.server.url, parsedURL, true)) {
ViewManager.handleDeepLink(parsedURL);
return {action: 'deny'};
}