MM-14446: consider subpath when evaluating if url is internal (#946)
* MM-14446: consider subpath when evaluating if url is internal When clicking on an URL with `target=_blank`, the webview decides if it should launch an external browser or a new window within the Electron application. Update this logic to consider the application's configured subpath so as to treat links outside the subpath but on the same domain as external. * fix licensing on new file * fix .eslintrc.json indentation * tweak header eslint rules for specific files
This commit is contained in:

committed by
William Gathoye

parent
6e2b3d7fab
commit
79e020ba43
138
.eslintrc.json
138
.eslintrc.json
@@ -1,31 +1,115 @@
|
|||||||
{
|
{
|
||||||
"extends": [
|
"extends": [
|
||||||
"./.eslintrc-webapp.json",
|
"./.eslintrc-webapp.json",
|
||||||
"plugin:eslint-comments/recommended"
|
"plugin:eslint-comments/recommended"
|
||||||
],
|
],
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2017
|
"ecmaVersion": 2017
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"import/resolver": "node"
|
"import/resolver": "node"
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"eslint-comments/no-unused-disable": "error",
|
"header/header": [2, "line", [
|
||||||
|
" Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.",
|
||||||
|
" See LICENSE.txt for license information."
|
||||||
|
]],
|
||||||
|
"import/no-commonjs": 2,
|
||||||
|
"indent": [2, 2, {"SwitchCase": 0}],
|
||||||
|
"no-console": 0,
|
||||||
|
"no-process-env": 0,
|
||||||
|
"no-underscore-dangle": 1,
|
||||||
|
"no-var": 2,
|
||||||
|
"react/jsx-indent": [2, 2],
|
||||||
|
"react/jsx-indent-props": [2, 2],
|
||||||
|
"react/no-find-dom-node": 2,
|
||||||
|
"react/no-set-state": 1,
|
||||||
|
"react/require-optimization": 0
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"webpack.config.renderer.js",
|
||||||
|
"test/specs/settings_test.js",
|
||||||
|
"test/specs/spellchecker_test.js",
|
||||||
|
"test/specs/app_test.js",
|
||||||
|
"test/specs/security_test.js",
|
||||||
|
"test/specs/permisson_test.js",
|
||||||
|
"test/specs/browser/index_test.js",
|
||||||
|
"test/specs/browser/settings_test.js",
|
||||||
|
"test/modules/utils.js",
|
||||||
|
"test/modules/environment.js",
|
||||||
|
"webpack.config.main.js",
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"webpack.config.base.js",
|
||||||
|
"babel.config.js",
|
||||||
|
"README.md",
|
||||||
|
"scripts/watch_main_and_preload.js",
|
||||||
|
"scripts/extract_dict.js",
|
||||||
|
"scripts/manipulate_windows_zip.js",
|
||||||
|
"scripts/check_build_config.js",
|
||||||
|
"LICENSE.txt",
|
||||||
|
"src/utils/util.js",
|
||||||
|
"src/main.js",
|
||||||
|
"src/browser/config/AppConfig.js",
|
||||||
|
"src/browser/js/contextMenu.js",
|
||||||
|
"src/browser/updater.jsx",
|
||||||
|
"src/browser/js/notification.js",
|
||||||
|
"src/browser/js/badge.js",
|
||||||
|
"src/browser/webview/mattermost.js",
|
||||||
|
"src/browser/components/RemoveServerModal.jsx",
|
||||||
|
"src/browser/components/MainPage.jsx",
|
||||||
|
"src/browser/components/HoveringURL.jsx",
|
||||||
|
"src/browser/components/AutoSaveIndicator.jsx",
|
||||||
|
"src/browser/components/MattermostView.jsx",
|
||||||
|
"src/browser/components/TabBar.jsx",
|
||||||
|
"src/browser/components/DestructiveConfirmModal.jsx",
|
||||||
|
"src/browser/components/ErrorView.jsx",
|
||||||
|
"src/browser/components/UpdaterPage.jsx",
|
||||||
|
"src/browser/components/PermissionRequestDialog.jsx",
|
||||||
|
"src/browser/components/Finder.jsx",
|
||||||
|
"src/browser/components/SettingsPage.jsx",
|
||||||
|
"src/browser/components/TeamListItem.jsx",
|
||||||
|
"src/browser/components/UpdaterPage/UpdaterPage.stories.jsx",
|
||||||
|
"src/browser/components/Button/Button.stories.jsx",
|
||||||
|
"src/browser/components/TeamList.jsx",
|
||||||
|
"src/browser/components/LoginModal.jsx",
|
||||||
|
"src/browser/components/NewTeamModal.jsx",
|
||||||
|
"src/browser/settings.jsx",
|
||||||
|
"src/browser/index.jsx",
|
||||||
|
"src/common/deepmerge.js",
|
||||||
|
"src/common/config/buildConfig.js",
|
||||||
|
"src/common/config/pastDefaultPreferences.js",
|
||||||
|
"src/common/config/upgradePreferences.js",
|
||||||
|
"src/common/osVersion.js",
|
||||||
|
"src/common/config/defaultPreferences.js",
|
||||||
|
"src/common/JsonFileManager.js",
|
||||||
|
"src/common/settings.js",
|
||||||
|
"src/main/certificateStore.js",
|
||||||
|
"src/main/mainWindow.js",
|
||||||
|
"src/main/allowProtocolDialog.js",
|
||||||
|
"src/main/permissionRequestHandler.js",
|
||||||
|
"src/main/squirrelStartup.js",
|
||||||
|
"src/main/autoLaunch.js",
|
||||||
|
"src/main/PermissionManager.js",
|
||||||
|
"src/main/AutoLauncher.js",
|
||||||
|
"src/main/AppStateManager.js",
|
||||||
|
"src/main/menus/tray.js",
|
||||||
|
"src/main/CriticalErrorHandler.js",
|
||||||
|
"src/main/cookieManager.js",
|
||||||
|
"src/main/utils.js",
|
||||||
|
"src/main/downloadURL.js",
|
||||||
|
"src/main/autoUpdater.js",
|
||||||
|
"src/main/SpellChecker.js",
|
||||||
|
"src/main/menus/app.js"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
"header/header": [2, "line", [
|
"header/header": [2, "line", [
|
||||||
" Copyright (c) 2015-2016 Yuya Ochiai",
|
" Copyright (c) 2015-2016 Yuya Ochiai",
|
||||||
" Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.",
|
" Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.",
|
||||||
" See LICENSE.txt for license information."
|
" See LICENSE.txt for license information."
|
||||||
]],
|
]]
|
||||||
"import/no-commonjs": 2,
|
}
|
||||||
"indent": [2, 2, {"SwitchCase": 0}],
|
|
||||||
"no-console": 0,
|
|
||||||
"no-process-env": 0,
|
|
||||||
"no-underscore-dangle": 1,
|
|
||||||
"no-var": 2,
|
|
||||||
"react/jsx-indent": [2, 2],
|
|
||||||
"react/jsx-indent-props": [2, 2],
|
|
||||||
"react/no-find-dom-node": 2,
|
|
||||||
"react/no-set-state": 1,
|
|
||||||
"react/require-optimization": 0
|
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@ import PropTypes from 'prop-types';
|
|||||||
import {ipcRenderer, remote, shell} from 'electron';
|
import {ipcRenderer, remote, shell} from 'electron';
|
||||||
|
|
||||||
import contextMenu from '../js/contextMenu';
|
import contextMenu from '../js/contextMenu';
|
||||||
|
import Utils from '../../utils/util';
|
||||||
import {protocols} from '../../../electron-builder.json';
|
import {protocols} from '../../../electron-builder.json';
|
||||||
const scheme = protocols[0].schemes[0];
|
const scheme = protocols[0].schemes[0];
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ export default class MattermostView extends React.Component {
|
|||||||
isContextMenuAdded: false,
|
isContextMenuAdded: false,
|
||||||
reloadTimeoutID: null,
|
reloadTimeoutID: null,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
|
basename: '/',
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleUnreadCountChange = this.handleUnreadCountChange.bind(this);
|
this.handleUnreadCountChange = this.handleUnreadCountChange.bind(this);
|
||||||
@@ -94,7 +96,7 @@ export default class MattermostView extends React.Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentURL.host === destURL.host) {
|
if (Utils.isInternalURL(destURL, currentURL, this.state.basename)) {
|
||||||
if (destURL.path.match(/^\/api\/v[3-4]\/public\/files\//)) {
|
if (destURL.path.match(/^\/api\/v[3-4]\/public\/files\//)) {
|
||||||
ipcRenderer.send('download-url', e.url);
|
ipcRenderer.send('download-url', e.url);
|
||||||
} else {
|
} else {
|
||||||
@@ -137,6 +139,7 @@ export default class MattermostView extends React.Component {
|
|||||||
case 'onGuestInitialized':
|
case 'onGuestInitialized':
|
||||||
self.setState({
|
self.setState({
|
||||||
isLoaded: true,
|
isLoaded: true,
|
||||||
|
basename: event.args[0] || '/',
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'onBadgeChange': {
|
case 'onBadgeChange': {
|
||||||
|
@@ -46,7 +46,7 @@ window.addEventListener('load', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
watchReactAppUntilInitialized(() => {
|
watchReactAppUntilInitialized(() => {
|
||||||
ipcRenderer.sendToHost('onGuestInitialized');
|
ipcRenderer.sendToHost('onGuestInitialized', window.basename);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -8,4 +8,22 @@ function getDomain(inputURL) {
|
|||||||
return `${parsedURL.protocol}//${parsedURL.host}`;
|
return `${parsedURL.protocol}//${parsedURL.host}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {getDomain};
|
// isInternalURL determines if the target url is internal to the application.
|
||||||
|
// - currentURL is the current url inside the webview
|
||||||
|
// - basename is the global export from the Mattermost application defining the subpath, if any
|
||||||
|
function isInternalURL(targetURL, currentURL, basename = '/') {
|
||||||
|
if (targetURL.host !== currentURL.host) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(targetURL.pathname || '/').startsWith(basename)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getDomain,
|
||||||
|
isInternalURL,
|
||||||
|
};
|
||||||
|
47
test/specs/utils/util_test.js
Normal file
47
test/specs/utils/util_test.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import url from 'url';
|
||||||
|
import assert from 'assert';
|
||||||
|
|
||||||
|
import Utils from '../../../src/utils/util';
|
||||||
|
|
||||||
|
describe('Utils', () => {
|
||||||
|
describe('isInternalURL', () => {
|
||||||
|
it('should be false for different hosts', () => {
|
||||||
|
const currentURL = url.parse('http://localhost/team/channel1');
|
||||||
|
const targetURL = url.parse('http://example.com/team/channel2');
|
||||||
|
const basename = '/';
|
||||||
|
assert.equal(Utils.isInternalURL(targetURL, currentURL, basename), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be false for same hosts, non-matching basename', () => {
|
||||||
|
const currentURL = url.parse('http://localhost/subpath/team/channel1');
|
||||||
|
const targetURL = url.parse('http://localhost/team/channel2');
|
||||||
|
const basename = '/subpath';
|
||||||
|
assert.equal(Utils.isInternalURL(targetURL, currentURL, basename), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be true for same hosts, matching basename', () => {
|
||||||
|
const currentURL = url.parse('http://localhost/subpath/team/channel1');
|
||||||
|
const targetURL = url.parse('http://localhost/subpath/team/channel2');
|
||||||
|
const basename = '/subpath';
|
||||||
|
assert.equal(Utils.isInternalURL(targetURL, currentURL, basename), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be true for same hosts, default basename', () => {
|
||||||
|
const currentURL = url.parse('http://localhost/team/channel1');
|
||||||
|
const targetURL = url.parse('http://localhost/team/channel2');
|
||||||
|
const basename = '/';
|
||||||
|
assert.equal(Utils.isInternalURL(targetURL, currentURL, basename), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be true for same hosts, default basename, empty target path', () => {
|
||||||
|
const currentURL = url.parse('http://localhost/team/channel1');
|
||||||
|
const targetURL = url.parse('http://localhost/');
|
||||||
|
const basename = '/';
|
||||||
|
assert.equal(Utils.isInternalURL(targetURL, currentURL, basename), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Reference in New Issue
Block a user