Remove use of getView() and replace with getViewByURL() where necessary. (#2544)

* WIP

* Replace getView with getViewByURL
This commit is contained in:
Devin Binnie
2023-02-07 08:59:35 -05:00
committed by GitHub
parent 8babd52b81
commit 07e41ec678
14 changed files with 254 additions and 350 deletions

View File

@@ -1,35 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MattermostServer} from 'common/servers/MattermostServer';
import * as TabView from 'common/tabs/TabView';
describe('common/tabs/TabView', () => {
describe('getServerView', () => {
it('should return correct URL on messaging tab', () => {
const server = new MattermostServer('server-1', 'http://server-1.com');
const tab = {name: TabView.TAB_MESSAGING};
expect(TabView.getServerView(server, tab).url).toBe(server.url);
});
it('should return correct URL on playbooks tab', () => {
const server = new MattermostServer('server-1', 'http://server-1.com');
const tab = {name: TabView.TAB_PLAYBOOKS};
expect(TabView.getServerView(server, tab).url.toString()).toBe(`${server.url}playbooks`);
});
it('should return correct URL on boards tab', () => {
const server = new MattermostServer('server-1', 'http://server-1.com');
const tab = {name: TabView.TAB_FOCALBOARD};
expect(TabView.getServerView(server, tab).url.toString()).toBe(`${server.url}boards`);
});
it('should throw error on bad tab name', () => {
const server = new MattermostServer('server-1', 'http://server-1.com');
const tab = {name: 'not a real tab name'};
expect(() => {
TabView.getServerView(server, tab);
}).toThrow(Error);
});
});
});

View File

@@ -1,14 +1,10 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Tab, Team} from 'types/config';
import {Team} from 'types/config';
import {MattermostServer} from 'common/servers/MattermostServer';
import MessagingTabView from './MessagingTabView';
import FocalboardTabView from './FocalboardTabView';
import PlaybooksTabView from './PlaybooksTabView';
export const TAB_MESSAGING = 'TAB_MESSAGING';
export const TAB_FOCALBOARD = 'TAB_FOCALBOARD';
export const TAB_PLAYBOOKS = 'TAB_PLAYBOOKS';
@@ -46,19 +42,6 @@ export function getDefaultTeamWithTabsFromTeam(team: Team) {
};
}
export function getServerView(srv: MattermostServer, tab: Tab) {
switch (tab.name) {
case TAB_MESSAGING:
return new MessagingTabView(srv);
case TAB_FOCALBOARD:
return new FocalboardTabView(srv);
case TAB_PLAYBOOKS:
return new PlaybooksTabView(srv);
default:
throw new Error('Not implemeneted');
}
}
export function getTabDisplayName(tabType: TabType) {
switch (tabType) {
case TAB_MESSAGING:

View File

@@ -172,86 +172,6 @@ describe('common/utils/url', () => {
});
});
describe('getView', () => {
const servers = [
{
name: 'server-1',
url: 'http://server-1.com',
tabs: [
{
name: 'tab',
},
{
name: 'tab-type1',
},
{
name: 'tab-type2',
},
],
},
{
name: 'server-2',
url: 'http://server-2.com/subpath',
tabs: [
{
name: 'tab-type1',
},
{
name: 'tab-type2',
},
{
name: 'tab',
},
],
},
];
it('should match the correct server - base URL', () => {
const inputURL = new URL('http://server-1.com');
expect(urlUtils.getView(inputURL, servers)).toStrictEqual({name: 'server-1_tab', url: 'http://server-1.com/'});
});
it('should match the correct server - base tab', () => {
const inputURL = new URL('http://server-1.com/team');
expect(urlUtils.getView(inputURL, servers)).toStrictEqual({name: 'server-1_tab', url: 'http://server-1.com/'});
});
it('should match the correct server - different tab', () => {
const inputURL = new URL('http://server-1.com/type1/app');
expect(urlUtils.getView(inputURL, servers)).toStrictEqual({name: 'server-1_tab-type1', url: 'http://server-1.com/type1'});
});
it('should return undefined for server with subpath and URL without', () => {
const inputURL = new URL('http://server-2.com');
expect(urlUtils.getView(inputURL, servers)).toBe(undefined);
});
it('should return undefined for server with subpath and URL with wrong subpath', () => {
const inputURL = new URL('http://server-2.com/different/subpath');
expect(urlUtils.getView(inputURL, servers)).toBe(undefined);
});
it('should match the correct server with a subpath - base URL', () => {
const inputURL = new URL('http://server-2.com/subpath');
expect(urlUtils.getView(inputURL, servers)).toStrictEqual({name: 'server-2_tab', url: 'http://server-2.com/subpath/'});
});
it('should match the correct server with a subpath - base tab', () => {
const inputURL = new URL('http://server-2.com/subpath/team');
expect(urlUtils.getView(inputURL, servers)).toStrictEqual({name: 'server-2_tab', url: 'http://server-2.com/subpath/'});
});
it('should match the correct server with a subpath - different tab', () => {
const inputURL = new URL('http://server-2.com/subpath/type2/team');
expect(urlUtils.getView(inputURL, servers)).toStrictEqual({name: 'server-2_tab-type2', url: 'http://server-2.com/subpath/type2'});
});
it('should return undefined for wrong server', () => {
const inputURL = new URL('http://server-3.com');
expect(urlUtils.getView(inputURL, servers)).toBe(undefined);
});
});
describe('equalUrls', () => {
it('base urls', () => {
const url1 = new URL('http://server-1.com');
@@ -321,110 +241,38 @@ describe('common/utils/url', () => {
it('should match correct URL', () => {
expect(urlUtils.isCustomLoginURL(
'http://server.com/oauth/authorize',
{
url: 'http://server.com',
},
[
{
name: 'a',
url: 'http://server.com',
tabs: [
{
name: 'tab',
},
],
},
])).toBe(true);
'http://server.com',
)).toBe(true);
});
it('should not match incorrect URL', () => {
expect(urlUtils.isCustomLoginURL(
'http://server.com/oauth/notauthorize',
{
url: 'http://server.com',
},
[
{
name: 'a',
url: 'http://server.com',
tabs: [
{
name: 'tab',
},
],
},
])).toBe(false);
'http://server.com',
)).toBe(false);
});
it('should not match base URL', () => {
expect(urlUtils.isCustomLoginURL(
'http://server.com/',
{
url: 'http://server.com',
},
[
{
name: 'a',
url: 'http://server.com',
tabs: [
{
name: 'tab',
},
],
},
])).toBe(false);
'http://server.com',
)).toBe(false);
});
it('should match with subpath', () => {
expect(urlUtils.isCustomLoginURL(
'http://server.com/subpath/oauth/authorize',
{
url: 'http://server.com/subpath',
},
[
{
name: 'a',
url: 'http://server.com/subpath',
tabs: [
{
name: 'tab',
},
],
},
])).toBe(true);
'http://server.com/subpath',
)).toBe(true);
});
it('should not match with different subpath', () => {
expect(urlUtils.isCustomLoginURL(
'http://server.com/subpath/oauth/authorize',
{
url: 'http://server.com/different/subpath',
},
[
{
name: 'a',
url: 'http://server.com/different/subpath',
tabs: [
{
name: 'tab',
},
],
},
])).toBe(false);
'http://server.com/different/subpath',
)).toBe(false);
});
it('should not match with oauth subpath', () => {
expect(urlUtils.isCustomLoginURL(
'http://server.com/oauth/authorize',
{
url: 'http://server.com/oauth/authorize',
},
[
{
name: 'a',
url: 'http://server.com/oauth/authorize',
tabs: [
{
name: 'tab',
},
],
},
])).toBe(false);
'http://server.com/oauth/authorize',
)).toBe(false);
});
});
});

View File

@@ -3,12 +3,7 @@
import {isHttpsUri, isHttpUri, isUri} from 'valid-url';
import {TeamWithTabs} from 'types/config';
import {ServerFromURL} from 'types/utils';
import buildConfig from 'common/config/buildConfig';
import {MattermostServer} from 'common/servers/MattermostServer';
import {getServerView} from 'common/tabs/TabView';
import {customLoginRegexPaths, nonTeamUrlPaths} from 'common/utils/constants';
function isValidURL(testURL: string) {
@@ -126,43 +121,6 @@ function isManagedResource(serverUrl: URL | string, inputURL: URL | string) {
return paths.some((testPath) => isUrlType(testPath, serverUrl, inputURL));
}
function getView(inputURL: URL | string, teams: TeamWithTabs[], ignoreScheme = false): ServerFromURL | undefined {
const parsedURL = parseURL(inputURL);
if (!parsedURL) {
return undefined;
}
let firstOption;
let secondOption;
teams.forEach((team) => {
const srv = new MattermostServer(team.name, team.url);
// sort by length so that we match the highest specificity last
const filteredTabs = team.tabs.map((tab) => {
const tabView = getServerView(srv, tab);
const parsedServerUrl = parseURL(tabView.url);
return {tabView, parsedServerUrl};
});
filteredTabs.sort((a, b) => a.tabView.url.toString().length - b.tabView.url.toString().length);
filteredTabs.forEach((tab) => {
if (tab.parsedServerUrl) {
// check server and subpath matches (without subpath pathname is \ so it always matches)
if (getFormattedPathName(tab.parsedServerUrl.pathname) !== '/' && equalUrlsWithSubpath(tab.parsedServerUrl, parsedURL, ignoreScheme)) {
firstOption = {name: tab.tabView.name, url: tab.parsedServerUrl.toString()};
}
if (getFormattedPathName(tab.parsedServerUrl.pathname) === '/' && equalUrlsIgnoringSubpath(tab.parsedServerUrl, parsedURL, ignoreScheme)) {
// in case the user added something on the path that doesn't really belong to the server
// there might be more than one that matches, but we can't differentiate, so last one
// is as good as any other in case there is no better match (e.g.: two subpath servers with the same origin)
// e.g.: https://community.mattermost.com/core
secondOption = {name: tab.tabView.name, url: tab.parsedServerUrl.toString()};
}
}
});
});
return firstOption || secondOption;
}
// next two functions are defined to clarify intent
export function equalUrlsWithSubpath(url1: URL, url2: URL, ignoreScheme?: boolean) {
if (ignoreScheme) {
@@ -178,24 +136,26 @@ export function equalUrlsIgnoringSubpath(url1: URL, url2: URL, ignoreScheme?: bo
return url1.origin.toLowerCase() === url2.origin.toLowerCase();
}
function isTrustedURL(url: URL | string, teams: TeamWithTabs[]) {
function isTrustedURL(url: URL | string, rootURL: URL | string) {
const parsedURL = parseURL(url);
if (!parsedURL) {
const rootParsedURL = parseURL(rootURL);
if (!parsedURL || !rootParsedURL) {
return false;
}
return getView(parsedURL, teams) !== null;
return (getFormattedPathName(rootParsedURL.pathname) !== '/' && equalUrlsWithSubpath(rootParsedURL, parsedURL)) ||
(getFormattedPathName(rootParsedURL.pathname) === '/' && equalUrlsIgnoringSubpath(rootParsedURL, parsedURL));
}
function isCustomLoginURL(url: URL | string, server: ServerFromURL, teams: TeamWithTabs[]): boolean {
const serverURL = parseURL(server.url);
const subpath = server && serverURL ? serverURL.pathname : '';
function isCustomLoginURL(url: URL | string, serverURL: URL | string): boolean {
const parsedServerURL = parseURL(serverURL);
const parsedURL = parseURL(url);
if (!parsedURL) {
if (!parsedURL || !parsedServerURL) {
return false;
}
if (!isTrustedURL(parsedURL, teams)) {
if (!isTrustedURL(parsedURL, parsedServerURL)) {
return false;
}
const subpath = parsedServerURL.pathname;
const urlPath = parsedURL.pathname;
const replacement = subpath.endsWith('/') ? '/' : '';
const replacedPath = urlPath.replace(subpath, replacement);
@@ -229,7 +189,6 @@ export default {
isValidURI,
isInternalURL,
parseURL,
getView,
getServerInfo,
isAdminUrl,
isTeamUrl,

View File

@@ -153,6 +153,7 @@ jest.mock('main/windows/windowManager', () => ({
sendToMattermostViews: jest.fn(),
sendToRenderer: jest.fn(),
getServerNameByWebContentsId: jest.fn(),
getServerURLFromWebContentsId: jest.fn(),
}));
describe('main/app/initialize', () => {
beforeEach(() => {
@@ -237,6 +238,7 @@ describe('main/app/initialize', () => {
});
it('should allow permission requests for supported types from trusted URLs', async () => {
WindowManager.getServerURLFromWebContentsId.mockReturnValue(new URL('http://server-1.com'));
let callback = jest.fn();
session.defaultSession.setPermissionRequestHandler.mockImplementation((cb) => {
cb({id: 1, getURL: () => 'http://server-1.com'}, 'bad-permission', callback);

View File

@@ -399,9 +399,15 @@ function initializeAfterAppReady() {
}
const requestingURL = webContents.getURL();
const serverURL = WindowManager.getServerURLFromWebContentsId(webContents.id);
if (!serverURL) {
callback(false);
return;
}
// is the requesting url trusted?
callback(urlUtils.isTrustedURL(requestingURL, Config.teams));
callback(urlUtils.isTrustedURL(requestingURL, serverURL));
});
// only check for non-Windows, as with Windows we have to wait for GPO teams

View File

@@ -58,12 +58,6 @@ jest.mock('common/utils/url', () => {
const actualUrl = jest.requireActual('common/utils/url');
return {
...actualUrl.default,
getView: (url) => {
if (url.toString() === 'http://badurl.com/') {
return null;
}
return {name: 'test', url};
},
isTrustedURL: (url) => {
return url.toString() === 'http://trustedurl.com/';
},
@@ -92,6 +86,7 @@ jest.mock('main/trustedOrigins', () => ({
jest.mock('main/windows/windowManager', () => ({
getMainWindow: jest.fn().mockImplementation(() => ({})),
getServerURLFromWebContentsId: jest.fn(),
}));
jest.mock('main/views/modalManager', () => ({
@@ -109,45 +104,45 @@ describe('main/authManager', () => {
authManager.popLoginModal = jest.fn();
authManager.popPermissionModal = jest.fn();
it('should not pop any modal on null url', () => {
authManager.handleAppLogin({preventDefault: jest.fn()}, null, {url: null}, null, jest.fn());
expect(authManager.popLoginModal).not.toBeCalled();
expect(authManager.popPermissionModal).not.toBeCalled();
});
it('should not pop any modal on null server', () => {
authManager.handleAppLogin({preventDefault: jest.fn()}, null, {url: 'http://badurl.com/'}, null, jest.fn());
it('should not pop any modal on a missing server', () => {
WindowManager.getServerURLFromWebContentsId.mockReturnValue(undefined);
authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 0}, {url: 'http://badurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).not.toBeCalled();
expect(authManager.popPermissionModal).not.toBeCalled();
});
it('should popLoginModal when isTrustedURL', () => {
authManager.handleAppLogin({preventDefault: jest.fn()}, null, {url: 'http://trustedurl.com/'}, null, jest.fn());
WindowManager.getServerURLFromWebContentsId.mockReturnValue(new URL('http://trustedurl.com/'));
authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://trustedurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).toBeCalled();
expect(authManager.popPermissionModal).not.toBeCalled();
});
it('should popLoginModal when isCustomLoginURL', () => {
authManager.handleAppLogin({preventDefault: jest.fn()}, null, {url: 'http://customloginurl.com/'}, null, jest.fn());
WindowManager.getServerURLFromWebContentsId.mockReturnValue(new URL('http://customloginurl.com/'));
authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://customloginurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).toBeCalled();
expect(authManager.popPermissionModal).not.toBeCalled();
});
it('should popLoginModal when has permission', () => {
authManager.handleAppLogin({preventDefault: jest.fn()}, null, {url: 'http://haspermissionurl.com/'}, null, jest.fn());
WindowManager.getServerURLFromWebContentsId.mockReturnValue(new URL('http://haspermissionurl.com/'));
authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://haspermissionurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).toBeCalled();
expect(authManager.popPermissionModal).not.toBeCalled();
});
it('should popPermissionModal when anything else is true', () => {
authManager.handleAppLogin({preventDefault: jest.fn()}, null, {url: 'http://someotherurl.com/'}, null, jest.fn());
WindowManager.getServerURLFromWebContentsId.mockReturnValue(new URL('http://someotherurl.com/'));
authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://someotherurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).not.toBeCalled();
expect(authManager.popPermissionModal).toBeCalled();
});
it('should set login callback when logging in', () => {
WindowManager.getServerURLFromWebContentsId.mockReturnValue(new URL('http://someotherurl.com/'));
const callback = jest.fn();
authManager.handleAppLogin({preventDefault: jest.fn()}, null, {url: 'http://someotherurl.com/'}, null, callback);
authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://someotherurl.com/'}, null, callback);
expect(authManager.loginCallbackMap.get('http://someotherurl.com/')).toEqual(callback);
});
});

View File

@@ -6,7 +6,6 @@ import log from 'electron-log';
import {PermissionType} from 'types/trustedOrigin';
import {LoginModalData} from 'types/auth';
import Config from 'common/config';
import {BASIC_AUTH_PERMISSION} from 'common/permissions';
import urlUtils from 'common/utils/url';
@@ -39,13 +38,13 @@ export class AuthManager {
if (!parsedURL) {
return;
}
const server = urlUtils.getView(parsedURL, Config.teams);
if (!server) {
const serverURL = WindowManager.getServerURLFromWebContentsId(webContents.id);
if (!serverURL) {
return;
}
this.loginCallbackMap.set(request.url, callback); // if callback is undefined set it to null instead so we know we have set it up with no value
if (urlUtils.isTrustedURL(request.url, Config.teams) || urlUtils.isCustomLoginURL(parsedURL, server, Config.teams) || TrustedOriginsStore.checkPermission(request.url, BASIC_AUTH_PERMISSION)) {
if (urlUtils.isTrustedURL(request.url, serverURL) || urlUtils.isCustomLoginURL(parsedURL, serverURL) || TrustedOriginsStore.checkPermission(request.url, BASIC_AUTH_PERMISSION)) {
this.popLoginModal(request, authInfo);
} else {
this.popPermissionModal(request, authInfo, BASIC_AUTH_PERMISSION);

View File

@@ -9,8 +9,8 @@ import {Tuple as tuple} from '@bloomberg/record-tuple-polyfill';
import {BROWSER_HISTORY_PUSH, LOAD_SUCCESS, MAIN_WINDOW_SHOWN} from 'common/communication';
import {MattermostServer} from 'common/servers/MattermostServer';
import {getServerView, getTabViewName} from 'common/tabs/TabView';
import urlUtils from 'common/utils/url';
import {getTabViewName} from 'common/tabs/TabView';
import {equalUrlsIgnoringSubpath} from 'common/utils/url';
import {MattermostView} from './MattermostView';
import {ViewManager} from './viewManager';
@@ -29,8 +29,8 @@ jest.mock('electron', () => ({
}));
jest.mock('common/tabs/TabView', () => ({
getServerView: jest.fn(),
getTabViewName: jest.fn((a, b) => `${a}-${b}`),
TAB_MESSAGING: 'tab',
}));
jest.mock('common/servers/MattermostServer', () => ({
@@ -45,7 +45,7 @@ jest.mock('common/utils/url', () => ({
return null;
}
},
getView: jest.fn(),
equalUrlsIgnoringSubpath: jest.fn(),
}));
jest.mock('main/i18nManager', () => ({
@@ -75,7 +75,7 @@ describe('main/views/viewManager', () => {
beforeEach(() => {
viewManager.createLoadingScreen = jest.fn();
viewManager.showByName = jest.fn();
getServerView.mockImplementation((srv, tab) => ({name: `${srv.name}-${tab.name}`}));
viewManager.getServerView = jest.fn().mockImplementation((srv, tabName) => ({name: `${srv.name}-${tabName}`}));
MattermostView.mockImplementation((tab) => ({
on: jest.fn(),
load: loadFn,
@@ -184,9 +184,9 @@ describe('main/views/viewManager', () => {
send: jest.fn(),
};
getServerView.mockImplementation((srv, tab) => ({
name: `${srv.name}-${tab.name}`,
urlTypeTuple: tuple(`http://${srv.name}.com/`, tab.name),
viewManager.getServerView = jest.fn().mockImplementation((srv, tabName) => ({
name: `${srv.name}-${tabName}`,
urlTypeTuple: tuple(`http://${srv.name}.com/`, tabName),
url: new URL(`http://${srv.name}.com`),
}));
MattermostServer.mockImplementation((name, url) => ({
@@ -684,6 +684,106 @@ describe('main/views/viewManager', () => {
});
});
describe('getViewByURL', () => {
const viewManager = new ViewManager({});
viewManager.getServers = () => [
{
name: 'server-1',
url: 'http://server-1.com',
tabs: [
{
name: 'tab',
},
{
name: 'tab-type1',
},
{
name: 'tab-type2',
},
],
},
{
name: 'server-2',
url: 'http://server-2.com/subpath',
tabs: [
{
name: 'tab-type1',
},
{
name: 'tab-type2',
},
{
name: 'tab',
},
],
},
];
viewManager.getServerView = (srv, tabName) => {
const postfix = tabName.split('-')[1];
return {
name: `${srv.name}_${tabName}`,
url: new URL(`${srv.url.toString().replace(/\/$/, '')}${postfix ? `/${postfix}` : ''}`),
};
};
beforeEach(() => {
MattermostServer.mockImplementation((name, url) => ({
name,
url: new URL(url),
}));
equalUrlsIgnoringSubpath.mockImplementation((url1, url2) => `${url1}`.startsWith(`${url2}`));
});
afterEach(() => {
jest.resetAllMocks();
});
it('should match the correct server - base URL', () => {
const inputURL = new URL('http://server-1.com');
expect(viewManager.getViewByURL(inputURL)).toStrictEqual({name: 'server-1_tab', url: new URL('http://server-1.com')});
});
it('should match the correct server - base tab', () => {
const inputURL = new URL('http://server-1.com/team');
expect(viewManager.getViewByURL(inputURL)).toStrictEqual({name: 'server-1_tab', url: new URL('http://server-1.com')});
});
it('should match the correct server - different tab', () => {
const inputURL = new URL('http://server-1.com/type1/app');
expect(viewManager.getViewByURL(inputURL)).toStrictEqual({name: 'server-1_tab-type1', url: new URL('http://server-1.com/type1')});
});
it('should return undefined for server with subpath and URL without', () => {
const inputURL = new URL('http://server-2.com');
expect(viewManager.getViewByURL(inputURL)).toBe(undefined);
});
it('should return undefined for server with subpath and URL with wrong subpath', () => {
const inputURL = new URL('http://server-2.com/different/subpath');
expect(viewManager.getViewByURL(inputURL)).toBe(undefined);
});
it('should match the correct server with a subpath - base URL', () => {
const inputURL = new URL('http://server-2.com/subpath');
expect(viewManager.getViewByURL(inputURL)).toStrictEqual({name: 'server-2_tab', url: new URL('http://server-2.com/subpath')});
});
it('should match the correct server with a subpath - base tab', () => {
const inputURL = new URL('http://server-2.com/subpath/team');
expect(viewManager.getViewByURL(inputURL)).toStrictEqual({name: 'server-2_tab', url: new URL('http://server-2.com/subpath')});
});
it('should match the correct server with a subpath - different tab', () => {
const inputURL = new URL('http://server-2.com/subpath/type2/team');
expect(viewManager.getViewByURL(inputURL)).toStrictEqual({name: 'server-2_tab-type2', url: new URL('http://server-2.com/subpath/type2')});
});
it('should return undefined for wrong server', () => {
const inputURL = new URL('http://server-3.com');
expect(viewManager.getViewByURL(inputURL)).toBe(undefined);
});
});
describe('handleDeepLink', () => {
const viewManager = new ViewManager({});
const baseView = {
@@ -705,6 +805,7 @@ describe('main/views/viewManager', () => {
beforeEach(() => {
viewManager.openClosedTab = jest.fn();
viewManager.getViewByURL = jest.fn();
});
afterEach(() => {
@@ -714,7 +815,7 @@ describe('main/views/viewManager', () => {
});
it('should load URL into matching view', () => {
urlUtils.getView.mockImplementation(() => ({name: 'view1', url: 'http://server-1.com/'}));
viewManager.getViewByURL.mockImplementation(() => ({name: 'view1', url: 'http://server-1.com/'}));
const view = {...baseView};
viewManager.views.set('view1', view);
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes');
@@ -722,7 +823,7 @@ describe('main/views/viewManager', () => {
});
it('should send the URL to the view if its already loaded on a 6.0 server', () => {
urlUtils.getView.mockImplementation(() => ({name: 'view1', url: 'http://server-1.com/'}));
viewManager.getViewByURL.mockImplementation(() => ({name: 'view1', url: 'http://server-1.com/'}));
const view = {
...baseView,
serverInfo: {
@@ -743,7 +844,7 @@ describe('main/views/viewManager', () => {
});
it('should throw error if view is missing', () => {
urlUtils.getView.mockImplementation(() => ({name: 'view1', url: 'http://server-1.com/'}));
viewManager.getViewByURL.mockImplementation(() => ({name: 'view1', url: 'http://server-1.com/'}));
const view = {...baseView};
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes');
expect(view.load).not.toHaveBeenCalled();
@@ -757,7 +858,7 @@ describe('main/views/viewManager', () => {
});
it('should reopen closed tab if called upon', () => {
urlUtils.getView.mockImplementation(() => ({name: 'view1', url: 'https://server-1.com/'}));
viewManager.getViewByURL.mockImplementation(() => ({name: 'view1', url: 'https://server-1.com/'}));
viewManager.closedViews.set('view1', {});
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes');
expect(viewManager.openClosedTab).toHaveBeenCalledWith('view1', 'https://server-1.com/deep/link?thing=yes');

View File

@@ -23,10 +23,13 @@ import {
MAIN_WINDOW_SHOWN,
} from 'common/communication';
import Config from 'common/config';
import urlUtils from 'common/utils/url';
import urlUtils, {equalUrlsIgnoringSubpath} from 'common/utils/url';
import Utils from 'common/utils/util';
import {MattermostServer} from 'common/servers/MattermostServer';
import {getServerView, getTabViewName, TabTuple, TabType} from 'common/tabs/TabView';
import {getTabViewName, TabTuple, TabType, TAB_FOCALBOARD, TAB_MESSAGING, TAB_PLAYBOOKS} from 'common/tabs/TabView';
import MessagingTabView from 'common/tabs/MessagingTabView';
import FocalboardTabView from 'common/tabs/FocalboardTabView';
import PlaybooksTabView from 'common/tabs/PlaybooksTabView';
import {localizeMessage} from 'main/i18nManager';
import {ServerInfo} from 'main/server/serverInfo';
@@ -82,7 +85,7 @@ export class ViewManager {
}
makeView = (srv: MattermostServer, serverInfo: ServerInfo, tab: Tab, url?: string): MattermostView => {
const tabView = getServerView(srv, tab);
const tabView = this.getServerView(srv, tab.name);
const view = new MattermostView(tabView, serverInfo, this.mainWindow, this.viewOptions);
view.once(LOAD_SUCCESS, this.activateView);
view.load(url);
@@ -146,10 +149,10 @@ export class ViewManager {
for (const [team, tab] of sortedTabs) {
const srv = new MattermostServer(team.name, team.url);
const info = new ServerInfo(srv);
const view = getServerView(srv, tab);
const tabTuple = tuple(new URL(team.url).href, tab.name as TabType);
const recycle = current.get(tabTuple);
if (!tab.isOpen) {
const view = this.getServerView(srv, tab.name);
closed.set(tabTuple, {srv, tab, name: view.name});
} else if (recycle) {
recycle.updateServerInfo(srv);
@@ -511,11 +514,38 @@ export class ViewManager {
view.removeListener(LOAD_SUCCESS, this.deeplinkSuccess);
}
getViewByURL = (inputURL: URL | string, ignoreScheme = false) => {
log.silly('ViewManager.getViewByURL', `${inputURL}`, ignoreScheme);
const parsedURL = urlUtils.parseURL(inputURL);
if (!parsedURL) {
return undefined;
}
const server = this.getServers().find((team) => {
const parsedServerUrl = urlUtils.parseURL(team.url)!;
return equalUrlsIgnoringSubpath(parsedURL, parsedServerUrl, ignoreScheme) && parsedURL.pathname.match(new RegExp(`^${parsedServerUrl.pathname}(.+)?(/(.+))?$`));
});
if (!server) {
return undefined;
}
const mmServer = new MattermostServer(server.name, server.url);
let selectedTab = this.getServerView(mmServer, TAB_MESSAGING);
server.tabs.
filter((tab) => tab.name !== TAB_MESSAGING).
forEach((tab) => {
const tabCandidate = this.getServerView(mmServer, tab.name);
if (parsedURL.pathname.match(new RegExp(`^${tabCandidate.url.pathname}(/(.+))?`))) {
selectedTab = tabCandidate;
}
});
return selectedTab;
}
handleDeepLink = (url: string | URL) => {
// TODO: fix for new tabs
if (url) {
const parsedURL = urlUtils.parseURL(url)!;
const tabView = urlUtils.getView(parsedURL, this.getServers(), true);
const tabView = this.getViewByURL(parsedURL, true);
if (tabView) {
const urlWithSchema = `${urlUtils.parseURL(tabView.url)?.origin}${parsedURL.pathname}${parsedURL.search}`;
if (this.closedViews.has(tabView.name)) {
@@ -555,4 +585,17 @@ export class ViewManager {
}
});
}
private getServerView = (srv: MattermostServer, tabName: string) => {
switch (tabName) {
case TAB_MESSAGING:
return new MessagingTabView(srv);
case TAB_FOCALBOARD:
return new FocalboardTabView(srv);
case TAB_PLAYBOOKS:
return new PlaybooksTabView(srv);
default:
throw new Error('Not implemeneted');
}
}
}

View File

@@ -30,6 +30,7 @@ jest.mock('electron', () => ({
jest.mock('../allowProtocolDialog', () => ({}));
jest.mock('../windows/windowManager', () => ({
getServerURLFromWebContentsId: jest.fn(),
showMainWindow: jest.fn(),
}));
@@ -83,7 +84,7 @@ describe('main/views/webContentsEvents', () => {
const willNavigate = webContentsEventManager.generateWillNavigate(jest.fn());
beforeEach(() => {
urlUtils.getView.mockImplementation(() => ({name: 'server_name', url: 'http://server-1.com'}));
WindowManager.getServerURLFromWebContentsId.mockImplementation(() => new URL('http://server-1.com'));
});
afterEach(() => {
@@ -100,7 +101,7 @@ describe('main/views/webContentsEvents', () => {
});
it('should allow navigation when url isAdminURL', () => {
urlUtils.isAdminUrl.mockImplementation((serverURL, parsedURL) => parsedURL.toString().startsWith(`${serverURL}/admin_console`));
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();
});
@@ -146,7 +147,7 @@ describe('main/views/webContentsEvents', () => {
const didStartNavigation = webContentsEventManager.generateDidStartNavigation(jest.fn());
beforeEach(() => {
urlUtils.getView.mockImplementation(() => ({name: 'server_name', url: 'http://server-1.com'}));
WindowManager.getServerURLFromWebContentsId.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'));
@@ -176,11 +177,11 @@ describe('main/views/webContentsEvents', () => {
beforeEach(() => {
urlUtils.isValidURI.mockReturnValue(true);
urlUtils.getView.mockReturnValue({name: 'server_name', 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}/myplugin`));
WindowManager.getServerURLFromWebContentsId.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}myplugin`));
});
afterEach(() => {
@@ -211,7 +212,7 @@ describe('main/views/webContentsEvents', () => {
});
it('should open in the browser when there is no server matching', () => {
urlUtils.getView.mockReturnValue(null);
WindowManager.getServerURLFromWebContentsId.mockReturnValue(undefined);
expect(newWindow({url: 'http://server-2.com/subpath'})).toStrictEqual({action: 'deny'});
expect(shell.openExternal).toBeCalledWith('http://server-2.com/subpath');
});

View File

@@ -37,7 +37,7 @@ export class WebContentsEventManager {
this.listeners = {};
}
isTrustedPopupWindow = (webContents: WebContents) => {
private isTrustedPopupWindow = (webContents: WebContents) => {
if (!webContents) {
return false;
}
@@ -47,24 +47,23 @@ export class WebContentsEventManager {
return BrowserWindow.fromWebContents(webContents) === this.popupWindow;
}
generateWillNavigate = (getServersFunction: () => TeamWithTabs[]) => {
generateWillNavigate = () => {
return (event: Event & {sender: WebContents}, url: string) => {
log.debug('webContentEvents.will-navigate', {webContentsId: event.sender.id, url});
const contentID = event.sender.id;
const parsedURL = urlUtils.parseURL(url)!;
const configServers = getServersFunction();
const server = urlUtils.getView(parsedURL, configServers);
const serverURL = WindowManager.getServerURLFromWebContentsId(event.sender.id);
if (server && (urlUtils.isTeamUrl(server.url, parsedURL) || urlUtils.isAdminUrl(server.url, parsedURL) || this.isTrustedPopupWindow(event.sender))) {
if (serverURL && (urlUtils.isTeamUrl(serverURL, parsedURL) || urlUtils.isAdminUrl(serverURL, parsedURL) || this.isTrustedPopupWindow(event.sender))) {
return;
}
if (server && urlUtils.isChannelExportUrl(server.url, parsedURL)) {
if (serverURL && urlUtils.isChannelExportUrl(serverURL, parsedURL)) {
return;
}
if (server && urlUtils.isCustomLoginURL(parsedURL, server, configServers)) {
if (serverURL && urlUtils.isCustomLoginURL(parsedURL, serverURL)) {
return;
}
if (parsedURL.protocol === 'mailto:') {
@@ -80,24 +79,21 @@ export class WebContentsEventManager {
};
};
generateDidStartNavigation = (getServersFunction: () => TeamWithTabs[]) => {
generateDidStartNavigation = () => {
return (event: Event & {sender: WebContents}, url: string) => {
log.debug('webContentEvents.did-start-navigation', {webContentsId: event.sender.id, url});
const serverList = getServersFunction();
const contentID = event.sender.id;
const parsedURL = urlUtils.parseURL(url)!;
const server = urlUtils.getView(parsedURL, serverList);
const serverURL = WindowManager.getServerURLFromWebContentsId(event.sender.id);
if (!urlUtils.isTrustedURL(parsedURL, serverList)) {
if (!serverURL || !urlUtils.isTrustedURL(parsedURL, serverURL)) {
return;
}
const serverURL = urlUtils.parseURL(server?.url || '');
if (server && urlUtils.isCustomLoginURL(parsedURL, server, serverList)) {
if (serverURL && urlUtils.isCustomLoginURL(parsedURL, serverURL)) {
this.customLogins[contentID].inProgress = true;
} else if (server && this.customLogins[contentID].inProgress && urlUtils.isInternalURL(serverURL || new URL(''), parsedURL)) {
} else if (serverURL && this.customLogins[contentID].inProgress && urlUtils.isInternalURL(serverURL || new URL(''), parsedURL)) {
this.customLogins[contentID].inProgress = false;
}
};
@@ -108,7 +104,7 @@ export class WebContentsEventManager {
return {action: 'deny'};
};
generateNewWindowListener = (getServersFunction: () => TeamWithTabs[], spellcheck?: boolean) => {
generateNewWindowListener = (webContentsId: number, spellcheck?: boolean) => {
return (details: Electron.HandlerDetails): {action: 'deny' | 'allow'} => {
log.debug('webContentEvents.new-window', details.url);
@@ -118,8 +114,6 @@ export class WebContentsEventManager {
return {action: 'deny'};
}
const configServers = getServersFunction();
// Dev tools case
if (parsedURL.protocol === 'devtools:') {
return {action: 'allow'};
@@ -138,9 +132,9 @@ export class WebContentsEventManager {
return {action: 'deny'};
}
const server = urlUtils.getView(parsedURL, configServers);
const serverURL = WindowManager.getServerURLFromWebContentsId(webContentsId);
if (!server) {
if (!serverURL) {
shell.openExternal(details.url);
return {action: 'deny'};
}
@@ -166,11 +160,11 @@ export class WebContentsEventManager {
return {action: 'deny'};
}
if (urlUtils.isTeamUrl(server.url, parsedURL, true)) {
if (urlUtils.isTeamUrl(serverURL, parsedURL, true)) {
WindowManager.showMainWindow(parsedURL);
return {action: 'deny'};
}
if (urlUtils.isAdminUrl(server.url, parsedURL)) {
if (urlUtils.isAdminUrl(serverURL, parsedURL)) {
log.info(`${details.url} is an admin console page, preventing to open a new window`);
return {action: 'deny'};
}
@@ -180,7 +174,7 @@ export class WebContentsEventManager {
}
// TODO: move popups to its own and have more than one.
if (urlUtils.isPluginUrl(server.url, parsedURL) || urlUtils.isManagedResource(server.url, parsedURL)) {
if (urlUtils.isPluginUrl(serverURL, parsedURL) || urlUtils.isManagedResource(serverURL, parsedURL)) {
if (!this.popupWindow) {
this.popupWindow = new BrowserWindow({
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
@@ -200,7 +194,7 @@ export class WebContentsEventManager {
});
}
if (urlUtils.isManagedResource(server.url, parsedURL)) {
if (urlUtils.isManagedResource(serverURL, parsedURL)) {
this.popupWindow.loadURL(details.url);
} else {
// currently changing the userAgent for popup windows to allow plugins to go through google's oAuth
@@ -258,7 +252,7 @@ export class WebContentsEventManager {
this.removeWebContentsListeners(contents.id);
}
const willNavigate = this.generateWillNavigate(getServersFunction);
const willNavigate = this.generateWillNavigate();
contents.on('will-navigate', willNavigate as (e: Event, u: string) => void); // TODO: Electron types don't include sender for some reason
// handle custom login requests (oath, saml):
@@ -266,11 +260,11 @@ export class WebContentsEventManager {
// - 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(getServersFunction);
const didStartNavigation = this.generateDidStartNavigation();
contents.on('did-start-navigation', didStartNavigation as (e: Event, u: string) => void);
const spellcheck = Config.useSpellChecker;
const newWindow = this.generateNewWindowListener(getServersFunction, spellcheck);
const newWindow = this.generateNewWindowListener(contents.id, spellcheck);
contents.setWindowOpenHandler(newWindow);
addListeners?.(contents);

View File

@@ -47,7 +47,6 @@ jest.mock('common/config', () => ({}));
jest.mock('common/utils/url', () => ({
isTeamUrl: jest.fn(),
isAdminUrl: jest.fn(),
getView: jest.fn(),
cleanPathName: jest.fn(),
}));
jest.mock('common/tabs/TabView', () => ({
@@ -874,6 +873,7 @@ describe('main/windows/windowManager', () => {
]),
openClosedTab: jest.fn(),
showByName: jest.fn(),
getViewByURL: jest.fn(),
};
windowManager.handleBrowserHistoryButton = jest.fn();
@@ -911,7 +911,7 @@ describe('main/windows/windowManager', () => {
});
it('should open closed view if pushing to it', () => {
urlUtils.getView.mockReturnValue({name: 'server-1_other_type_2'});
windowManager.viewManager.getViewByURL.mockReturnValue({name: 'server-1_other_type_2'});
windowManager.viewManager.openClosedTab.mockImplementation((name) => {
const view = windowManager.viewManager.closedViews.get(name);
windowManager.viewManager.closedViews.delete(name);
@@ -923,13 +923,13 @@ describe('main/windows/windowManager', () => {
});
it('should open redirect view if different from current view', () => {
urlUtils.getView.mockReturnValue({name: 'server-1_other_type_1'});
windowManager.viewManager.getViewByURL.mockReturnValue({name: 'server-1_other_type_1'});
windowManager.handleBrowserHistoryPush(null, 'server-1_tab-messaging', '/other_type_1/subpath');
expect(windowManager.viewManager.showByName).toBeCalledWith('server-1_other_type_1');
});
it('should ignore redirects to "/" to Messages from other tabs', () => {
urlUtils.getView.mockReturnValue({name: 'server-1_tab-messaging'});
windowManager.viewManager.getViewByURL.mockReturnValue({name: 'server-1_tab-messaging'});
windowManager.handleBrowserHistoryPush(null, 'server-1_other_type_1', '/');
expect(view1.view.webContents.send).not.toBeCalled();
});

View File

@@ -752,7 +752,7 @@ export class WindowManager {
const currentView = this.viewManager?.views.get(viewName);
const cleanedPathName = urlUtils.cleanPathName(currentView?.tab.server.url.pathname || '', pathName);
const redirectedViewName = urlUtils.getView(`${currentView?.tab.server.url}${cleanedPathName}`, Config.teams)?.name || viewName;
const redirectedViewName = this.viewManager?.getViewByURL(`${currentView?.tab.server.url.toString().replace(/\/$/, '')}${cleanedPathName}`)?.name || viewName;
if (this.viewManager?.closedViews.has(redirectedViewName)) {
// If it's a closed view, just open it and stop
this.viewManager.openClosedTab(redirectedViewName, `${currentView?.tab.server.url}${cleanedPathName}`);
@@ -857,6 +857,14 @@ export class WindowManager {
view?.reload();
this.viewManager?.showByName(view?.name);
}
getServerURLFromWebContentsId = (id: number) => {
const viewName = this.getViewNameByWebContentsId(id);
if (!viewName) {
return undefined;
}
return this.viewManager?.views.get(viewName)?.tab.server.url;
}
}
const windowManager = new WindowManager();