[MM-21835] Use URL instead of the url library (#1384)
Additionally, migrate all of the URL related helper functions from `src/utils/utils.js` to the new `src/utils/url.js` file and migrate tests. Issue MM-21835 Fixes #1206
This commit is contained in:
parent
ad1871ad95
commit
5d0a937bb9
|
@ -6,7 +6,6 @@
|
||||||
/* eslint-disable react/no-set-state */
|
/* eslint-disable react/no-set-state */
|
||||||
|
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import url from 'url';
|
|
||||||
|
|
||||||
import React, {Fragment} from 'react';
|
import React, {Fragment} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
@ -17,6 +16,7 @@ import DotsVerticalIcon from 'mdi-react/DotsVerticalIcon';
|
||||||
import {ipcRenderer, remote, shell} from 'electron';
|
import {ipcRenderer, remote, shell} from 'electron';
|
||||||
|
|
||||||
import Utils from '../../utils/util';
|
import Utils from '../../utils/util';
|
||||||
|
import urlUtils from '../../utils/url';
|
||||||
import contextmenu from '../js/contextMenu';
|
import contextmenu from '../js/contextMenu';
|
||||||
|
|
||||||
import restoreButton from '../../assets/titlebar/chrome-restore.svg';
|
import restoreButton from '../../assets/titlebar/chrome-restore.svg';
|
||||||
|
@ -75,16 +75,16 @@ export default class MainPage extends React.Component {
|
||||||
|
|
||||||
parseDeeplinkURL(deeplink, teams = this.props.teams) {
|
parseDeeplinkURL(deeplink, teams = this.props.teams) {
|
||||||
if (deeplink && Array.isArray(teams) && teams.length) {
|
if (deeplink && Array.isArray(teams) && teams.length) {
|
||||||
const deeplinkURL = url.parse(deeplink);
|
const deeplinkURL = urlUtils.parseURL(deeplink);
|
||||||
let parsedDeeplink = null;
|
let parsedDeeplink = null;
|
||||||
teams.forEach((team, index) => {
|
teams.forEach((team, index) => {
|
||||||
const teamURL = url.parse(team.url);
|
const teamURL = urlUtils.parseURL(team.url);
|
||||||
if (deeplinkURL.host === teamURL.host) {
|
if (deeplinkURL.host === teamURL.host) {
|
||||||
parsedDeeplink = {
|
parsedDeeplink = {
|
||||||
teamURL,
|
teamURL,
|
||||||
teamIndex: index,
|
teamIndex: index,
|
||||||
originalURL: deeplinkURL,
|
originalURL: deeplinkURL,
|
||||||
url: `${teamURL.protocol}//${teamURL.host}${deeplinkURL.pathname || '/'}`,
|
url: `${teamURL.origin}${deeplinkURL.pathname || '/'}`,
|
||||||
path: deeplinkURL.pathname || '/',
|
path: deeplinkURL.pathname || '/',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -389,18 +389,18 @@ export default class MainPage extends React.Component {
|
||||||
|
|
||||||
switchToTabForCertificateRequest = (origin) => {
|
switchToTabForCertificateRequest = (origin) => {
|
||||||
// origin is server name + port, if the port doesn't match the protocol, it is kept by URL
|
// origin is server name + port, if the port doesn't match the protocol, it is kept by URL
|
||||||
const originURL = new URL(`http://${origin.split(':')[0]}`);
|
const originURL = urlUtils.parseURL(`http://${origin.split(':')[0]}`);
|
||||||
const secureOriginURL = new URL(`https://${origin.split(':')[0]}`);
|
const secureOriginURL = urlUtils.parseURL(`https://${origin.split(':')[0]}`);
|
||||||
|
|
||||||
const key = this.props.teams.findIndex((team) => {
|
const key = this.props.teams.findIndex((team) => {
|
||||||
const parsedURL = new URL(team.url);
|
const parsedURL = urlUtils.parseURL(team.url);
|
||||||
return (parsedURL.origin === originURL.origin) || (parsedURL.origin === secureOriginURL.origin);
|
return (parsedURL.origin === originURL.origin) || (parsedURL.origin === secureOriginURL.origin);
|
||||||
});
|
});
|
||||||
this.handleSelect(key);
|
this.handleSelect(key);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleInterTeamLink = (linkUrl) => {
|
handleInterTeamLink = (linkUrl) => {
|
||||||
const selectedTeam = Utils.getServer(linkUrl, this.props.teams);
|
const selectedTeam = urlUtils.getServer(linkUrl, this.props.teams);
|
||||||
if (!selectedTeam) {
|
if (!selectedTeam) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -649,7 +649,7 @@ export default class MainPage extends React.Component {
|
||||||
showExtraBar = () => {
|
showExtraBar = () => {
|
||||||
const ref = this.refs[`mattermostView${this.state.key}`];
|
const ref = this.refs[`mattermostView${this.state.key}`];
|
||||||
if (typeof ref !== 'undefined') {
|
if (typeof ref !== 'undefined') {
|
||||||
return !Utils.isTeamUrl(this.props.teams[this.state.key].url, ref.getSrc());
|
return !urlUtils.isTeamUrl(this.props.teams[this.state.key].url, ref.getSrc());
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -814,8 +814,8 @@ export default class MainPage extends React.Component {
|
||||||
let authInfo = null;
|
let authInfo = null;
|
||||||
if (this.state.loginQueue.length !== 0) {
|
if (this.state.loginQueue.length !== 0) {
|
||||||
request = this.state.loginQueue[0].request;
|
request = this.state.loginQueue[0].request;
|
||||||
const tmpURL = url.parse(this.state.loginQueue[0].request.url);
|
const tmpURL = urlUtils.parseURL(this.state.loginQueue[0].request.url);
|
||||||
authServerURL = `${tmpURL.protocol}//${tmpURL.host}`;
|
authServerURL = tmpURL.origin;
|
||||||
authInfo = this.state.loginQueue[0].authInfo;
|
authInfo = this.state.loginQueue[0].authInfo;
|
||||||
}
|
}
|
||||||
const modal = (
|
const modal = (
|
||||||
|
|
|
@ -5,14 +5,13 @@
|
||||||
// This file uses setState().
|
// This file uses setState().
|
||||||
/* eslint-disable react/no-set-state */
|
/* eslint-disable react/no-set-state */
|
||||||
|
|
||||||
import url from 'url';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 Utils from '../../utils/util';
|
||||||
|
import urlUtils from '../../utils/url';
|
||||||
import {protocols} from '../../../electron-builder.json';
|
import {protocols} from '../../../electron-builder.json';
|
||||||
const scheme = protocols[0].schemes[0];
|
const scheme = protocols[0].schemes[0];
|
||||||
|
|
||||||
|
@ -78,38 +77,38 @@ export default class MattermostView extends React.Component {
|
||||||
|
|
||||||
// Open link in browserWindow. for example, attached files.
|
// Open link in browserWindow. for example, attached files.
|
||||||
webview.addEventListener('new-window', (e) => {
|
webview.addEventListener('new-window', (e) => {
|
||||||
if (!Utils.isValidURI(e.url)) {
|
if (!urlUtils.isValidURI(e.url)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const currentURL = url.parse(webview.getURL());
|
const currentURL = urlUtils.parseURL(webview.getURL());
|
||||||
const destURL = url.parse(e.url);
|
const destURL = urlUtils.parseURL(e.url);
|
||||||
if (destURL.protocol !== 'http:' && destURL.protocol !== 'https:' && destURL.protocol !== `${scheme}:`) {
|
if (destURL.protocol !== 'http:' && destURL.protocol !== 'https:' && destURL.protocol !== `${scheme}:`) {
|
||||||
ipcRenderer.send('confirm-protocol', destURL.protocol, e.url);
|
ipcRenderer.send('confirm-protocol', destURL.protocol, e.url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Utils.isInternalURL(destURL, currentURL, this.state.basename)) {
|
if (urlUtils.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 if (destURL.path.match(/^\/help\//)) {
|
} else if (destURL.path.match(/^\/help\//)) {
|
||||||
// continue to open special case internal urls in default browser
|
// continue to open special case internal urls in default browser
|
||||||
shell.openExternal(e.url);
|
shell.openExternal(e.url);
|
||||||
} else if (Utils.isTeamUrl(this.props.src, e.url, true) || Utils.isAdminUrl(this.props.src, e.url)) {
|
} else if (urlUtils.isTeamUrl(this.props.src, e.url, true) || urlUtils.isAdminUrl(this.props.src, e.url)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.webviewRef.current.loadURL(e.url);
|
this.webviewRef.current.loadURL(e.url);
|
||||||
} else if (Utils.isPluginUrl(this.props.src, e.url)) {
|
} else if (urlUtils.isPluginUrl(this.props.src, e.url)) {
|
||||||
// New window should disable nodeIntegration.
|
// New window should disable nodeIntegration.
|
||||||
window.open(e.url, remote.app.name, 'nodeIntegration=no, contextIsolation=yes, show=yes');
|
window.open(e.url, remote.app.name, 'nodeIntegration=no, contextIsolation=yes, show=yes');
|
||||||
} else if (Utils.isManagedResource(this.props.src, e.url)) {
|
} else if (urlUtils.isManagedResource(this.props.src, e.url)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else {
|
} else {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
shell.openExternal(e.url);
|
shell.openExternal(e.url);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const parsedURL = Utils.parseURL(e.url);
|
const parsedURL = urlUtils.parseURL(e.url);
|
||||||
const serverURL = Utils.getServer(parsedURL, this.props.teams);
|
const serverURL = urlUtils.getServer(parsedURL, this.props.teams);
|
||||||
if (serverURL !== null && Utils.isTeamUrl(serverURL.url, parsedURL)) {
|
if (serverURL !== null && urlUtils.isTeamUrl(serverURL.url, parsedURL)) {
|
||||||
this.props.handleInterTeamLink(parsedURL);
|
this.props.handleInterTeamLink(parsedURL);
|
||||||
} else {
|
} else {
|
||||||
// if the link is external, use default os' application.
|
// if the link is external, use default os' application.
|
||||||
|
|
|
@ -6,7 +6,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {Modal, Button, FormGroup, FormControl, ControlLabel, HelpBlock} from 'react-bootstrap';
|
import {Modal, Button, FormGroup, FormControl, ControlLabel, HelpBlock} from 'react-bootstrap';
|
||||||
|
|
||||||
import Utils from '../../utils/util';
|
import urlUtils from '../../utils/url';
|
||||||
|
|
||||||
export default class NewTeamModal extends React.Component {
|
export default class NewTeamModal extends React.Component {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -62,7 +62,7 @@ export default class NewTeamModal extends React.Component {
|
||||||
if (!(/^https?:\/\/.*/).test(this.state.teamUrl.trim())) {
|
if (!(/^https?:\/\/.*/).test(this.state.teamUrl.trim())) {
|
||||||
return 'URL should start with http:// or https://.';
|
return 'URL should start with http:// or https://.';
|
||||||
}
|
}
|
||||||
if (!Utils.isValidURL(this.state.teamUrl.trim())) {
|
if (!urlUtils.isValidURL(this.state.teamUrl.trim())) {
|
||||||
return 'URL is not formatted correctly.';
|
return 'URL is not formatted correctly.';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -5,6 +5,8 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {ipcRenderer} from 'electron';
|
import {ipcRenderer} from 'electron';
|
||||||
|
|
||||||
|
import urlUtils from '../../utils/url';
|
||||||
|
|
||||||
// this component is used to override some checks from the UI, leaving only to trust the protocol in case it wasn't http/s
|
// this component is used to override some checks from the UI, leaving only to trust the protocol in case it wasn't http/s
|
||||||
// it is used the same as an `a` JSX tag
|
// it is used the same as an `a` JSX tag
|
||||||
export default function ExternalLink(props) {
|
export default function ExternalLink(props) {
|
||||||
|
@ -12,7 +14,7 @@ export default function ExternalLink(props) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let parseUrl;
|
let parseUrl;
|
||||||
try {
|
try {
|
||||||
parseUrl = new URL(props.href);
|
parseUrl = urlUtils.parseURL(props.href);
|
||||||
ipcRenderer.send('confirm-protocol', parseUrl.protocol, props.href);
|
ipcRenderer.send('confirm-protocol', parseUrl.protocol, props.href);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`invalid url ${props.href} supplied to externallink: ${err}`);
|
console.error(`invalid url ${props.href} supplied to externallink: ${err}`);
|
||||||
|
|
|
@ -8,12 +8,12 @@ window.eval = global.eval = () => { // eslint-disable-line no-multi-assign, no-e
|
||||||
throw new Error('Sorry, Mattermost does not support window.eval() for security reasons.');
|
throw new Error('Sorry, Mattermost does not support window.eval() for security reasons.');
|
||||||
};
|
};
|
||||||
|
|
||||||
import url from 'url';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import {remote, ipcRenderer} from 'electron';
|
import {remote, ipcRenderer} from 'electron';
|
||||||
|
|
||||||
|
import urlUtils from '../utils/url';
|
||||||
|
|
||||||
import Config from '../common/config';
|
import Config from '../common/config';
|
||||||
|
|
||||||
import EnhancedNotification from './js/notification';
|
import EnhancedNotification from './js/notification';
|
||||||
|
@ -32,11 +32,12 @@ if (teams.length === 0) {
|
||||||
remote.getCurrentWindow().loadFile('browser/settings.html');
|
remote.getCurrentWindow().loadFile('browser/settings.html');
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedURL = url.parse(window.location.href, true);
|
const parsedURLSearchParams = urlUtils.parseURL(window.location.href).searchParams;
|
||||||
const initialIndex = parsedURL.query.index ? parseInt(parsedURL.query.index, 10) : getInitialIndex();
|
const parsedURLHasIndex = parsedURLSearchParams.has('index');
|
||||||
|
const initialIndex = parsedURLHasIndex ? parseInt(parsedURLSearchParams.get('index'), 10) : getInitialIndex();
|
||||||
|
|
||||||
let deeplinkingUrl = null;
|
let deeplinkingUrl = null;
|
||||||
if (!parsedURL.query.index || parsedURL.query.index === null) {
|
if (!parsedURLHasIndex) {
|
||||||
deeplinkingUrl = remote.getCurrentWindow().deeplinkingUrl;
|
deeplinkingUrl = remote.getCurrentWindow().deeplinkingUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
import {ipcRenderer, remote} from 'electron';
|
import {ipcRenderer, remote} from 'electron';
|
||||||
import electronContextMenu from 'electron-context-menu';
|
import electronContextMenu from 'electron-context-menu';
|
||||||
|
|
||||||
|
import urlUtils from '../../utils/url';
|
||||||
|
|
||||||
function getSuggestionsMenus(webcontents, suggestions) {
|
function getSuggestionsMenus(webcontents, suggestions) {
|
||||||
if (suggestions.length === 0) {
|
if (suggestions.length === 0) {
|
||||||
return [{
|
return [{
|
||||||
|
@ -57,7 +59,7 @@ export default {
|
||||||
const isInternalLink = p.linkURL.endsWith('#') && p.linkURL.slice(0, -1) === p.pageURL;
|
const isInternalLink = p.linkURL.endsWith('#') && p.linkURL.slice(0, -1) === p.pageURL;
|
||||||
let isInternalSrc;
|
let isInternalSrc;
|
||||||
try {
|
try {
|
||||||
const srcurl = new URL(p.srcURL);
|
const srcurl = urlUtils.parseURL(p.srcURL);
|
||||||
isInternalSrc = srcurl.protocol === 'file:';
|
isInternalSrc = srcurl.protocol === 'file:';
|
||||||
console.log(`srcrurl protocol: ${srcurl.protocol}`);
|
console.log(`srcrurl protocol: ${srcurl.protocol}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
// 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 url from 'url';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import propTypes from 'prop-types';
|
import propTypes from 'prop-types';
|
||||||
import {ipcRenderer, remote} from 'electron';
|
import {ipcRenderer, remote} from 'electron';
|
||||||
|
|
||||||
|
import urlUtils from '../utils/url';
|
||||||
|
|
||||||
import UpdaterPage from './components/UpdaterPage.jsx';
|
import UpdaterPage from './components/UpdaterPage.jsx';
|
||||||
|
|
||||||
const thisURL = url.parse(location.href, true);
|
const thisURL = urlUtils.parseURL(location.href);
|
||||||
const notifyOnly = thisURL.query.notifyOnly === 'true';
|
const notifyOnly = thisURL.searchParams.get('notifyOnly') === 'true';
|
||||||
|
|
||||||
class UpdaterPageContainer extends React.Component {
|
class UpdaterPageContainer extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
// 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.
|
||||||
|
|
||||||
|
|
53
src/main.js
53
src/main.js
|
@ -31,8 +31,14 @@ import initCookieManager from './main/cookieManager';
|
||||||
import SpellChecker from './main/SpellChecker';
|
import SpellChecker from './main/SpellChecker';
|
||||||
import UserActivityMonitor from './main/UserActivityMonitor';
|
import UserActivityMonitor from './main/UserActivityMonitor';
|
||||||
import Utils from './utils/util';
|
import Utils from './utils/util';
|
||||||
|
import urlUtils from './utils/url';
|
||||||
import parseArgs from './main/ParseArgs';
|
import parseArgs from './main/ParseArgs';
|
||||||
import {REQUEST_PERMISSION_CHANNEL, GRANT_PERMISSION_CHANNEL, DENY_PERMISSION_CHANNEL, BASIC_AUTH_PERMISSION} from './common/permissions';
|
import {
|
||||||
|
REQUEST_PERMISSION_CHANNEL,
|
||||||
|
GRANT_PERMISSION_CHANNEL,
|
||||||
|
DENY_PERMISSION_CHANNEL,
|
||||||
|
BASIC_AUTH_PERMISSION
|
||||||
|
} from './common/permissions';
|
||||||
|
|
||||||
// pull out required electron components like this
|
// pull out required electron components like this
|
||||||
// as not all components can be referenced before the app is ready
|
// as not all components can be referenced before the app is ready
|
||||||
|
@ -357,7 +363,7 @@ function handleSelectedCertificate(event, server, cert) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAppCertificateError(event, webContents, url, error, certificate, callback) {
|
function handleAppCertificateError(event, webContents, url, error, certificate, callback) {
|
||||||
const parsedURL = new URL(url);
|
const parsedURL = urlUtils.parseURL(url);
|
||||||
if (!parsedURL) {
|
if (!parsedURL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -425,8 +431,8 @@ function handleAppGPUProcessCrashed(event, killed) {
|
||||||
|
|
||||||
function handleAppLogin(event, webContents, request, authInfo, callback) {
|
function handleAppLogin(event, webContents, request, authInfo, callback) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const parsedURL = new URL(request.url);
|
const parsedURL = urlUtils.parseURL(request.url);
|
||||||
const server = Utils.getServer(parsedURL, config.teams);
|
const server = urlUtils.getServer(parsedURL, config.teams);
|
||||||
|
|
||||||
loginCallbackMap.set(request.url, typeof callback === 'undefined' ? null : callback); // if callback is undefined set it to null instead so we know we have set it up with no value
|
loginCallbackMap.set(request.url, typeof callback === 'undefined' ? null : callback); // if callback is undefined set it to null instead so we know we have set it up with no value
|
||||||
if (isTrustedURL(request.url) || isCustomLoginURL(parsedURL, server) || trustedOriginsStore.checkPermission(request.url, BASIC_AUTH_PERMISSION)) {
|
if (isTrustedURL(request.url) || isCustomLoginURL(parsedURL, server) || trustedOriginsStore.checkPermission(request.url, BASIC_AUTH_PERMISSION)) {
|
||||||
|
@ -461,6 +467,7 @@ function handleAppWillFinishLaunching() {
|
||||||
setTimeout(openDeepLink, 1000);
|
setTimeout(openDeepLink, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openDeepLink();
|
openDeepLink();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -479,10 +486,10 @@ function handleAppWebContentsCreated(dc, contents) {
|
||||||
|
|
||||||
contents.on('will-navigate', (event, url) => {
|
contents.on('will-navigate', (event, url) => {
|
||||||
const contentID = event.sender.id;
|
const contentID = event.sender.id;
|
||||||
const parsedURL = Utils.parseURL(url);
|
const parsedURL = urlUtils.parseURL(url);
|
||||||
const server = Utils.getServer(parsedURL, config.teams);
|
const server = urlUtils.getServer(parsedURL, config.teams);
|
||||||
|
|
||||||
if ((server !== null && (Utils.isTeamUrl(server.url, parsedURL) || Utils.isAdminUrl(server.url, parsedURL))) ||
|
if ((server !== null && (urlUtils.isTeamUrl(server.url, parsedURL) || urlUtils.isAdminUrl(server.url, parsedURL))) ||
|
||||||
isTrustedPopupWindow(event.sender)) {
|
isTrustedPopupWindow(event.sender)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -508,8 +515,8 @@ function handleAppWebContentsCreated(dc, contents) {
|
||||||
// - indicate custom login is NOT in progress
|
// - indicate custom login is NOT in progress
|
||||||
contents.on('did-start-navigation', (event, url) => {
|
contents.on('did-start-navigation', (event, url) => {
|
||||||
const contentID = event.sender.id;
|
const contentID = event.sender.id;
|
||||||
const parsedURL = Utils.parseURL(url);
|
const parsedURL = urlUtils.parseURL(url);
|
||||||
const server = Utils.getServer(parsedURL, config.teams);
|
const server = urlUtils.getServer(parsedURL, config.teams);
|
||||||
|
|
||||||
if (!isTrustedURL(parsedURL)) {
|
if (!isTrustedURL(parsedURL)) {
|
||||||
return;
|
return;
|
||||||
|
@ -525,18 +532,18 @@ function handleAppWebContentsCreated(dc, contents) {
|
||||||
contents.on('new-window', (event, url) => {
|
contents.on('new-window', (event, url) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const parsedURL = Utils.parseURL(url);
|
const parsedURL = urlUtils.parseURL(url);
|
||||||
const server = Utils.getServer(parsedURL, config.teams);
|
const server = urlUtils.getServer(parsedURL, config.teams);
|
||||||
|
|
||||||
if (!server) {
|
if (!server) {
|
||||||
log.info(`Untrusted popup window blocked: ${url}`);
|
log.info(`Untrusted popup window blocked: ${url}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Utils.isTeamUrl(server.url, parsedURL, true)) {
|
if (urlUtils.isTeamUrl(server.url, parsedURL, true)) {
|
||||||
log.info(`${url} is a known team, preventing to open a new window`);
|
log.info(`${url} is a known team, preventing to open a new window`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Utils.isAdminUrl(server.url, parsedURL)) {
|
if (urlUtils.isAdminUrl(server.url, parsedURL)) {
|
||||||
log.info(`${url} is an admin console page, preventing to open a new window`);
|
log.info(`${url} is an admin console page, preventing to open a new window`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -544,7 +551,7 @@ function handleAppWebContentsCreated(dc, contents) {
|
||||||
log.info(`Popup window already open at provided url: ${url}`);
|
log.info(`Popup window already open at provided url: ${url}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Utils.isPluginUrl(server.url, parsedURL) || Utils.isManagedResource(server.url, parsedURL)) {
|
if (urlUtils.isPluginUrl(server.url, parsedURL) || urlUtils.isManagedResource(server.url, parsedURL)) {
|
||||||
if (!popupWindow || popupWindow.closed) {
|
if (!popupWindow || popupWindow.closed) {
|
||||||
popupWindow = new BrowserWindow({
|
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
|
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
||||||
|
@ -564,7 +571,7 @@ function handleAppWebContentsCreated(dc, contents) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Utils.isManagedResource(server.url, parsedURL)) {
|
if (urlUtils.isManagedResource(server.url, parsedURL)) {
|
||||||
popupWindow.loadURL(url);
|
popupWindow.loadURL(url);
|
||||||
} else {
|
} else {
|
||||||
// currently changing the userAgent for popup windows to allow plugins to go through google's oAuth
|
// currently changing the userAgent for popup windows to allow plugins to go through google's oAuth
|
||||||
|
@ -791,7 +798,7 @@ function initializeAfterAppReady() {
|
||||||
mainWindow.webContents.send('download-complete', {
|
mainWindow.webContents.send('download-complete', {
|
||||||
fileName: filename,
|
fileName: filename,
|
||||||
path: item.savePath,
|
path: item.savePath,
|
||||||
serverInfo: Utils.getServer(webContents.getURL(), config.teams),
|
serverInfo: urlUtils.getServer(webContents.getURL(), config.teams),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1026,11 +1033,11 @@ function handleMainWindowWebContentsCrashed() {
|
||||||
//
|
//
|
||||||
|
|
||||||
function isTrustedURL(url) {
|
function isTrustedURL(url) {
|
||||||
const parsedURL = Utils.parseURL(url);
|
const parsedURL = urlUtils.parseURL(url);
|
||||||
if (!parsedURL) {
|
if (!parsedURL) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return Utils.getServer(parsedURL, config.teams) !== null;
|
return urlUtils.getServer(parsedURL, config.teams) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTrustedPopupWindow(webContents) {
|
function isTrustedPopupWindow(webContents) {
|
||||||
|
@ -1045,7 +1052,7 @@ function isTrustedPopupWindow(webContents) {
|
||||||
|
|
||||||
function isCustomLoginURL(url, server) {
|
function isCustomLoginURL(url, server) {
|
||||||
const subpath = (server === null || typeof server === 'undefined') ? '' : server.url.pathname;
|
const subpath = (server === null || typeof server === 'undefined') ? '' : server.url.pathname;
|
||||||
const parsedURL = Utils.parseURL(url);
|
const parsedURL = urlUtils.parseURL(url);
|
||||||
if (!parsedURL) {
|
if (!parsedURL) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1080,8 +1087,7 @@ function getTrayImages() {
|
||||||
unread: nativeImage.createFromPath(path.resolve(assetsDir, 'windows/tray_unread.ico')),
|
unread: nativeImage.createFromPath(path.resolve(assetsDir, 'windows/tray_unread.ico')),
|
||||||
mention: nativeImage.createFromPath(path.resolve(assetsDir, 'windows/tray_mention.ico')),
|
mention: nativeImage.createFromPath(path.resolve(assetsDir, 'windows/tray_mention.ico')),
|
||||||
};
|
};
|
||||||
case 'darwin':
|
case 'darwin': {
|
||||||
{
|
|
||||||
const icons = {
|
const icons = {
|
||||||
light: {
|
light: {
|
||||||
normal: nativeImage.createFromPath(path.resolve(assetsDir, 'osx/MenuIcon.png')),
|
normal: nativeImage.createFromPath(path.resolve(assetsDir, 'osx/MenuIcon.png')),
|
||||||
|
@ -1097,8 +1103,7 @@ function getTrayImages() {
|
||||||
switchMenuIconImages(icons, nativeTheme.shouldUseDarkColors);
|
switchMenuIconImages(icons, nativeTheme.shouldUseDarkColors);
|
||||||
return icons;
|
return icons;
|
||||||
}
|
}
|
||||||
case 'linux':
|
case 'linux': {
|
||||||
{
|
|
||||||
const theme = config.trayIconTheme;
|
const theme = config.trayIconTheme;
|
||||||
try {
|
try {
|
||||||
return {
|
return {
|
||||||
|
@ -1136,7 +1141,7 @@ function getDeeplinkingURL(args) {
|
||||||
if (Array.isArray(args) && args.length) {
|
if (Array.isArray(args) && args.length) {
|
||||||
// deeplink urls should always be the last argument, but may not be the first (i.e. Windows with the app already running)
|
// deeplink urls should always be the last argument, but may not be the first (i.e. Windows with the app already running)
|
||||||
const url = args[args.length - 1];
|
const url = args[args.length - 1];
|
||||||
if (url && scheme && url.startsWith(scheme) && Utils.isValidURI(url)) {
|
if (url && scheme && url.startsWith(scheme) && urlUtils.isValidURI(url)) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
import Joi from '@hapi/joi';
|
import Joi from '@hapi/joi';
|
||||||
|
|
||||||
import Utils from '../utils/util';
|
import urlUtils from '../utils/url';
|
||||||
|
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
stripUnknown: true,
|
stripUnknown: true,
|
||||||
|
@ -138,7 +138,7 @@ export function validateV1ConfigData(data) {
|
||||||
});
|
});
|
||||||
|
|
||||||
// next filter out urls that are still invalid so all is not lost
|
// next filter out urls that are still invalid so all is not lost
|
||||||
teams = teams.filter(({url}) => Utils.isValidURL(url));
|
teams = teams.filter(({url}) => urlUtils.isValidURL(url));
|
||||||
|
|
||||||
// replace original teams
|
// replace original teams
|
||||||
data.teams = teams;
|
data.teams = teams;
|
||||||
|
@ -158,7 +158,7 @@ export function validateV2ConfigData(data) {
|
||||||
});
|
});
|
||||||
|
|
||||||
// next filter out urls that are still invalid so all is not lost
|
// next filter out urls that are still invalid so all is not lost
|
||||||
teams = teams.filter(({url}) => Utils.isValidURL(url));
|
teams = teams.filter(({url}) => urlUtils.isValidURL(url));
|
||||||
|
|
||||||
// replace original teams
|
// replace original teams
|
||||||
data.teams = teams;
|
data.teams = teams;
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
|
import urlUtils from '../utils/url';
|
||||||
|
|
||||||
import * as Validator from './Validator';
|
import * as Validator from './Validator';
|
||||||
|
|
||||||
function comparableCertificate(certificate) {
|
function comparableCertificate(certificate) {
|
||||||
|
@ -24,11 +26,6 @@ function areEqual(certificate0, certificate1) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHost(targetURL) {
|
|
||||||
const parsedURL = new URL(targetURL);
|
|
||||||
return parsedURL.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
function CertificateStore(storeFile) {
|
function CertificateStore(storeFile) {
|
||||||
this.storeFile = storeFile;
|
this.storeFile = storeFile;
|
||||||
let storeStr;
|
let storeStr;
|
||||||
|
@ -49,15 +46,15 @@ CertificateStore.prototype.save = function save() {
|
||||||
};
|
};
|
||||||
|
|
||||||
CertificateStore.prototype.add = function add(targetURL, certificate) {
|
CertificateStore.prototype.add = function add(targetURL, certificate) {
|
||||||
this.data[getHost(targetURL)] = comparableCertificate(certificate);
|
this.data[urlUtils.getHost(targetURL)] = comparableCertificate(certificate);
|
||||||
};
|
};
|
||||||
|
|
||||||
CertificateStore.prototype.isExisting = function isExisting(targetURL) {
|
CertificateStore.prototype.isExisting = function isExisting(targetURL) {
|
||||||
return this.data.hasOwnProperty(getHost(targetURL));
|
return this.data.hasOwnProperty(urlUtils.getHost(targetURL));
|
||||||
};
|
};
|
||||||
|
|
||||||
CertificateStore.prototype.isTrusted = function isTrusted(targetURL, certificate) {
|
CertificateStore.prototype.isTrusted = function isTrusted(targetURL, certificate) {
|
||||||
const host = getHost(targetURL);
|
const host = urlUtils.getHost(targetURL);
|
||||||
if (!this.isExisting(targetURL)) {
|
if (!this.isExisting(targetURL)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import fs from 'fs';
|
||||||
|
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
import Utils from '../utils/util.js';
|
import urlUtils from '../utils/url';
|
||||||
|
|
||||||
import * as Validator from './Validator';
|
import * as Validator from './Validator';
|
||||||
|
|
||||||
|
@ -56,12 +56,12 @@ export default class TrustedOriginsStore {
|
||||||
if (!validPermissions) {
|
if (!validPermissions) {
|
||||||
throw new Error(`Invalid permissions set for trusting ${targetURL}`);
|
throw new Error(`Invalid permissions set for trusting ${targetURL}`);
|
||||||
}
|
}
|
||||||
this.data.set(Utils.getHost(targetURL), validPermissions);
|
this.data.set(urlUtils.getHost(targetURL), validPermissions);
|
||||||
};
|
};
|
||||||
|
|
||||||
// enables usage of `targetURL` for `permission`
|
// enables usage of `targetURL` for `permission`
|
||||||
addPermission = (targetURL, permission) => {
|
addPermission = (targetURL, permission) => {
|
||||||
const origin = Utils.getHost(targetURL);
|
const origin = urlUtils.getHost(targetURL);
|
||||||
const currentPermissions = this.data.get(origin) || {};
|
const currentPermissions = this.data.get(origin) || {};
|
||||||
currentPermissions[permission] = true;
|
currentPermissions[permission] = true;
|
||||||
this.set(origin, currentPermissions);
|
this.set(origin, currentPermissions);
|
||||||
|
@ -70,7 +70,7 @@ export default class TrustedOriginsStore {
|
||||||
delete = (targetURL) => {
|
delete = (targetURL) => {
|
||||||
let host;
|
let host;
|
||||||
try {
|
try {
|
||||||
host = Utils.getHost(targetURL);
|
host = urlUtils.getHost(targetURL);
|
||||||
this.data.delete(host);
|
this.data.delete(host);
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
|
@ -79,7 +79,7 @@ export default class TrustedOriginsStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
isExisting = (targetURL) => {
|
isExisting = (targetURL) => {
|
||||||
return (typeof this.data.get(Utils.getHost(targetURL)) !== 'undefined');
|
return (typeof this.data.get(urlUtils.getHost(targetURL)) !== 'undefined');
|
||||||
};
|
};
|
||||||
|
|
||||||
// if user hasn't set his preferences, it will return null (falsy)
|
// if user hasn't set his preferences, it will return null (falsy)
|
||||||
|
@ -90,7 +90,7 @@ export default class TrustedOriginsStore {
|
||||||
}
|
}
|
||||||
let origin;
|
let origin;
|
||||||
try {
|
try {
|
||||||
origin = Utils.getHost(targetURL);
|
origin = urlUtils.getHost(targetURL);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error(`invalid host to retrieve permissions: ${targetURL}: ${e}`);
|
log.error(`invalid host to retrieve permissions: ${targetURL}: ${e}`);
|
||||||
return null;
|
return null;
|
||||||
|
|
190
src/utils/url.js
Normal file
190
src/utils/url.js
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {isHttpsUri, isHttpUri, isUri} from 'valid-url';
|
||||||
|
|
||||||
|
import buildConfig from '../common/config/buildConfig';
|
||||||
|
|
||||||
|
function getDomain(inputURL) {
|
||||||
|
const parsedURL = parseURL(inputURL);
|
||||||
|
return parsedURL.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidURL(testURL) {
|
||||||
|
return Boolean(isHttpUri(testURL) || isHttpsUri(testURL)) && parseURL(testURL) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidURI(testURL) {
|
||||||
|
return Boolean(isUri(testURL));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseURL(inputURL) {
|
||||||
|
if (!inputURL) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (inputURL instanceof URL) {
|
||||||
|
return inputURL;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new URL(inputURL);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHost(inputURL) {
|
||||||
|
const parsedURL = parseURL(inputURL);
|
||||||
|
if (parsedURL) {
|
||||||
|
return parsedURL.origin;
|
||||||
|
}
|
||||||
|
throw new Error(`Couldn't parse url: ${inputURL}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getServerInfo(serverUrl) {
|
||||||
|
const parsedServer = parseURL(serverUrl);
|
||||||
|
if (!parsedServer) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// does the server have a subpath?
|
||||||
|
const pn = parsedServer.pathname.toLowerCase();
|
||||||
|
const subpath = pn.endsWith('/') ? pn.toLowerCase() : `${pn}/`;
|
||||||
|
return {origin: parsedServer.origin, subpath, url: parsedServer};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getManagedResources() {
|
||||||
|
if (!buildConfig) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildConfig.managedResources || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAdminUrl(serverUrl, inputUrl) {
|
||||||
|
const parsedURL = parseURL(inputUrl);
|
||||||
|
const server = getServerInfo(serverUrl);
|
||||||
|
if (!parsedURL || !server || (!equalUrlsIgnoringSubpath(server, parsedURL))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}/admin_console/`) ||
|
||||||
|
parsedURL.pathname.toLowerCase().startsWith('/admin_console/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTeamUrl(serverUrl, inputUrl, withApi) {
|
||||||
|
const parsedURL = parseURL(inputUrl);
|
||||||
|
const server = getServerInfo(serverUrl);
|
||||||
|
if (!parsedURL || !server || (!equalUrlsIgnoringSubpath(server, parsedURL))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pre process nonTeamUrlPaths
|
||||||
|
let nonTeamUrlPaths = [
|
||||||
|
'plugins',
|
||||||
|
'signup',
|
||||||
|
'login',
|
||||||
|
'admin',
|
||||||
|
'channel',
|
||||||
|
'post',
|
||||||
|
'oauth',
|
||||||
|
'admin_console',
|
||||||
|
];
|
||||||
|
const managedResources = getManagedResources();
|
||||||
|
nonTeamUrlPaths = nonTeamUrlPaths.concat(managedResources);
|
||||||
|
|
||||||
|
if (withApi) {
|
||||||
|
nonTeamUrlPaths.push('api');
|
||||||
|
}
|
||||||
|
return !(nonTeamUrlPaths.some((testPath) => (
|
||||||
|
parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}${testPath}/`) ||
|
||||||
|
parsedURL.pathname.toLowerCase().startsWith(`/${testPath}/`))));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPluginUrl(serverUrl, inputURL) {
|
||||||
|
const server = getServerInfo(serverUrl);
|
||||||
|
const parsedURL = parseURL(inputURL);
|
||||||
|
if (!parsedURL || !server) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
equalUrlsIgnoringSubpath(server, parsedURL) &&
|
||||||
|
(parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}plugins/`) ||
|
||||||
|
parsedURL.pathname.toLowerCase().startsWith('/plugins/')));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isManagedResource(serverUrl, inputURL) {
|
||||||
|
const server = getServerInfo(serverUrl);
|
||||||
|
const parsedURL = parseURL(inputURL);
|
||||||
|
if (!parsedURL || !server) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const managedResources = getManagedResources();
|
||||||
|
|
||||||
|
return (
|
||||||
|
equalUrlsIgnoringSubpath(server, parsedURL) && managedResources && managedResources.length &&
|
||||||
|
managedResources.some((managedResource) => (parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}${managedResource}/`) || parsedURL.pathname.toLowerCase().startsWith(`/${managedResource}/`))));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getServer(inputURL, teams) {
|
||||||
|
const parsedURL = parseURL(inputURL);
|
||||||
|
if (!parsedURL) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let parsedServerUrl;
|
||||||
|
let secondOption = null;
|
||||||
|
for (let i = 0; i < teams.length; i++) {
|
||||||
|
parsedServerUrl = parseURL(teams[i].url);
|
||||||
|
|
||||||
|
// check server and subpath matches (without subpath pathname is \ so it always matches)
|
||||||
|
if (equalUrlsWithSubpath(parsedServerUrl, parsedURL)) {
|
||||||
|
return {name: teams[i].name, url: parsedServerUrl, index: i};
|
||||||
|
}
|
||||||
|
if (equalUrlsIgnoringSubpath(parsedServerUrl, parsedURL)) {
|
||||||
|
// 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: teams[i].name, url: parsedServerUrl, index: i};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return secondOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
// next two functions are defined to clarify intent
|
||||||
|
function equalUrlsWithSubpath(url1, url2) {
|
||||||
|
return url1.origin === url2.origin && url2.pathname.toLowerCase().startsWith(url1.pathname.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function equalUrlsIgnoringSubpath(url1, url2) {
|
||||||
|
return url1.origin.toLowerCase() === url2.origin.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getDomain,
|
||||||
|
isValidURL,
|
||||||
|
isValidURI,
|
||||||
|
isInternalURL,
|
||||||
|
parseURL,
|
||||||
|
getServer,
|
||||||
|
getServerInfo,
|
||||||
|
isAdminUrl,
|
||||||
|
isTeamUrl,
|
||||||
|
isPluginUrl,
|
||||||
|
isManagedResource,
|
||||||
|
getHost,
|
||||||
|
};
|
|
@ -1,173 +1,9 @@
|
||||||
// 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 url from 'url';
|
|
||||||
|
|
||||||
import electron, {remote} from 'electron';
|
import electron, {remote} from 'electron';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
import {isUri, isHttpUri, isHttpsUri} from 'valid-url';
|
|
||||||
|
|
||||||
import buildConfig from '../common/config/buildConfig';
|
|
||||||
|
|
||||||
function getDomain(inputURL) {
|
|
||||||
const parsedURL = url.parse(inputURL);
|
|
||||||
return `${parsedURL.protocol}//${parsedURL.host}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidURL(testURL) {
|
|
||||||
return Boolean(isHttpUri(testURL) || isHttpsUri(testURL)) && parseURL(testURL) !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidURI(testURL) {
|
|
||||||
return Boolean(isUri(testURL));
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseURL(inputURL) {
|
|
||||||
if (!inputURL) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (inputURL instanceof URL) {
|
|
||||||
return inputURL;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return new URL(inputURL);
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHost(inputURL) {
|
|
||||||
const parsedURL = parseURL(inputURL);
|
|
||||||
if (parsedURL) {
|
|
||||||
return parsedURL.origin;
|
|
||||||
}
|
|
||||||
throw new Error(`Couldn't parse url: ${inputURL}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getServerInfo(serverUrl) {
|
|
||||||
const parsedServer = parseURL(serverUrl);
|
|
||||||
if (!parsedServer) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// does the server have a subpath?
|
|
||||||
const pn = parsedServer.pathname.toLowerCase();
|
|
||||||
const subpath = pn.endsWith('/') ? pn.toLowerCase() : `${pn}/`;
|
|
||||||
return {origin: parsedServer.origin, subpath, url: parsedServer};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getManagedResources() {
|
|
||||||
if (!buildConfig) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return buildConfig.managedResources || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAdminUrl(serverUrl, inputUrl) {
|
|
||||||
const parsedURL = parseURL(inputUrl);
|
|
||||||
const server = getServerInfo(serverUrl);
|
|
||||||
if (!parsedURL || !server || (!equalUrlsIgnoringSubpath(server, parsedURL))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}/admin_console/`) ||
|
|
||||||
parsedURL.pathname.toLowerCase().startsWith('/admin_console/'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTeamUrl(serverUrl, inputUrl, withApi) {
|
|
||||||
const parsedURL = parseURL(inputUrl);
|
|
||||||
const server = getServerInfo(serverUrl);
|
|
||||||
if (!parsedURL || !server || (!equalUrlsIgnoringSubpath(server, parsedURL))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// pre process nonTeamUrlPaths
|
|
||||||
let nonTeamUrlPaths = [
|
|
||||||
'plugins',
|
|
||||||
'signup',
|
|
||||||
'login',
|
|
||||||
'admin',
|
|
||||||
'channel',
|
|
||||||
'post',
|
|
||||||
'oauth',
|
|
||||||
'admin_console',
|
|
||||||
];
|
|
||||||
const managedResources = getManagedResources();
|
|
||||||
nonTeamUrlPaths = nonTeamUrlPaths.concat(managedResources);
|
|
||||||
|
|
||||||
if (withApi) {
|
|
||||||
nonTeamUrlPaths.push('api');
|
|
||||||
}
|
|
||||||
return !(nonTeamUrlPaths.some((testPath) => (
|
|
||||||
parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}${testPath}/`) ||
|
|
||||||
parsedURL.pathname.toLowerCase().startsWith(`/${testPath}/`))));
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPluginUrl(serverUrl, inputURL) {
|
|
||||||
const server = getServerInfo(serverUrl);
|
|
||||||
const parsedURL = parseURL(inputURL);
|
|
||||||
if (!parsedURL || !server) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
equalUrlsIgnoringSubpath(server, parsedURL) &&
|
|
||||||
(parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}plugins/`) ||
|
|
||||||
parsedURL.pathname.toLowerCase().startsWith('/plugins/')));
|
|
||||||
}
|
|
||||||
|
|
||||||
function isManagedResource(serverUrl, inputURL) {
|
|
||||||
const server = getServerInfo(serverUrl);
|
|
||||||
const parsedURL = parseURL(inputURL);
|
|
||||||
if (!parsedURL || !server) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const managedResources = getManagedResources();
|
|
||||||
|
|
||||||
return (
|
|
||||||
equalUrlsIgnoringSubpath(server, parsedURL) && managedResources && managedResources.length &&
|
|
||||||
managedResources.some((managedResource) => (parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}${managedResource}/`) || parsedURL.pathname.toLowerCase().startsWith(`/${managedResource}/`))));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getServer(inputURL, teams) {
|
|
||||||
const parsedURL = parseURL(inputURL);
|
|
||||||
if (!parsedURL) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let parsedServerUrl;
|
|
||||||
let secondOption = null;
|
|
||||||
for (let i = 0; i < teams.length; i++) {
|
|
||||||
parsedServerUrl = parseURL(teams[i].url);
|
|
||||||
|
|
||||||
// check server and subpath matches (without subpath pathname is \ so it always matches)
|
|
||||||
if (equalUrlsWithSubpath(parsedServerUrl, parsedURL)) {
|
|
||||||
return {name: teams[i].name, url: parsedServerUrl, index: i};
|
|
||||||
}
|
|
||||||
if (equalUrlsIgnoringSubpath(parsedServerUrl, parsedURL)) {
|
|
||||||
// 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: teams[i].name, url: parsedServerUrl, index: i};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return secondOption;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDisplayBoundaries() {
|
function getDisplayBoundaries() {
|
||||||
const {screen} = electron;
|
const {screen} = electron;
|
||||||
|
@ -186,15 +22,6 @@ function getDisplayBoundaries() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// next two functions are defined to clarify intent
|
|
||||||
function equalUrlsWithSubpath(url1, url2) {
|
|
||||||
return url1.origin === url2.origin && url2.pathname.toLowerCase().startsWith(url1.pathname.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
function equalUrlsIgnoringSubpath(url1, url2) {
|
|
||||||
return url1.origin.toLowerCase() === url2.origin.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
const dispatchNotification = async (title, body, silent, data, handleClick) => {
|
const dispatchNotification = async (title, body, silent, data, handleClick) => {
|
||||||
let permission;
|
let permission;
|
||||||
const appIconURL = `file:///${remote.app.getAppPath()}/assets/appicon_48.png`;
|
const appIconURL = `file:///${remote.app.getAppPath()}/assets/appicon_48.png`;
|
||||||
|
@ -228,18 +55,6 @@ const dispatchNotification = async (title, body, silent, data, handleClick) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getDomain,
|
|
||||||
isValidURL,
|
|
||||||
isValidURI,
|
|
||||||
isInternalURL,
|
|
||||||
parseURL,
|
|
||||||
getServer,
|
|
||||||
getServerInfo,
|
|
||||||
isAdminUrl,
|
|
||||||
isTeamUrl,
|
|
||||||
isPluginUrl,
|
|
||||||
isManagedResource,
|
|
||||||
getDisplayBoundaries,
|
getDisplayBoundaries,
|
||||||
dispatchNotification,
|
dispatchNotification,
|
||||||
getHost,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
// 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.
|
||||||
|
|
|
@ -2,106 +2,105 @@
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import url from 'url';
|
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
|
|
||||||
import Utils from '../../../src/utils/util';
|
import urlUtils from '../../../src/utils/url';
|
||||||
|
|
||||||
describe('Utils', () => {
|
describe('url', () => {
|
||||||
describe('isValidURL', () => {
|
describe('isValidURL', () => {
|
||||||
it('should be true for a valid web url', () => {
|
it('should be true for a valid web url', () => {
|
||||||
const testURL = 'https://developers.mattermost.com/';
|
const testURL = 'https://developers.mattermost.com/';
|
||||||
assert.equal(Utils.isValidURL(testURL), true);
|
assert.equal(urlUtils.isValidURL(testURL), true);
|
||||||
});
|
});
|
||||||
it('should be true for a valid, non-https web url', () => {
|
it('should be true for a valid, non-https web url', () => {
|
||||||
const testURL = 'http://developers.mattermost.com/';
|
const testURL = 'http://developers.mattermost.com/';
|
||||||
assert.equal(Utils.isValidURL(testURL), true);
|
assert.equal(urlUtils.isValidURL(testURL), true);
|
||||||
});
|
});
|
||||||
it('should be true for an invalid, self-defined, top-level domain', () => {
|
it('should be true for an invalid, self-defined, top-level domain', () => {
|
||||||
const testURL = 'https://www.example.x';
|
const testURL = 'https://www.example.x';
|
||||||
assert.equal(Utils.isValidURL(testURL), true);
|
assert.equal(urlUtils.isValidURL(testURL), true);
|
||||||
});
|
});
|
||||||
it('should be true for a file download url', () => {
|
it('should be true for a file download url', () => {
|
||||||
const testURL = 'https://community.mattermost.com/api/v4/files/ka3xbfmb3ffnmgdmww8otkidfw?download=1';
|
const testURL = 'https://community.mattermost.com/api/v4/files/ka3xbfmb3ffnmgdmww8otkidfw?download=1';
|
||||||
assert.equal(Utils.isValidURL(testURL), true);
|
assert.equal(urlUtils.isValidURL(testURL), true);
|
||||||
});
|
});
|
||||||
it('should be true for a permalink url', () => {
|
it('should be true for a permalink url', () => {
|
||||||
const testURL = 'https://community.mattermost.com/test-channel/pl/pdqowkij47rmbyk78m5hwc7r6r';
|
const testURL = 'https://community.mattermost.com/test-channel/pl/pdqowkij47rmbyk78m5hwc7r6r';
|
||||||
assert.equal(Utils.isValidURL(testURL), true);
|
assert.equal(urlUtils.isValidURL(testURL), true);
|
||||||
});
|
});
|
||||||
it('should be true for a valid, internal domain', () => {
|
it('should be true for a valid, internal domain', () => {
|
||||||
const testURL = 'https://mattermost.company-internal';
|
const testURL = 'https://mattermost.company-internal';
|
||||||
assert.equal(Utils.isValidURL(testURL), true);
|
assert.equal(urlUtils.isValidURL(testURL), true);
|
||||||
});
|
});
|
||||||
it('should be true for a second, valid internal domain', () => {
|
it('should be true for a second, valid internal domain', () => {
|
||||||
const testURL = 'https://serverXY/mattermost';
|
const testURL = 'https://serverXY/mattermost';
|
||||||
assert.equal(Utils.isValidURL(testURL), true);
|
assert.equal(urlUtils.isValidURL(testURL), true);
|
||||||
});
|
});
|
||||||
it('should be true for a valid, non-https internal domain', () => {
|
it('should be true for a valid, non-https internal domain', () => {
|
||||||
const testURL = 'http://mattermost.local';
|
const testURL = 'http://mattermost.local';
|
||||||
assert.equal(Utils.isValidURL(testURL), true);
|
assert.equal(urlUtils.isValidURL(testURL), true);
|
||||||
});
|
});
|
||||||
it('should be true for a valid, non-https, ip address with port number', () => {
|
it('should be true for a valid, non-https, ip address with port number', () => {
|
||||||
const testURL = 'http://localhost:8065';
|
const testURL = 'http://localhost:8065';
|
||||||
assert.equal(Utils.isValidURL(testURL), true);
|
assert.equal(urlUtils.isValidURL(testURL), true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('isValidURI', () => {
|
describe('isValidURI', () => {
|
||||||
it('should be true for a deeplink url', () => {
|
it('should be true for a deeplink url', () => {
|
||||||
const testURL = 'mattermost://community-release.mattermost.com/core/channels/developers';
|
const testURL = 'mattermost://community-release.mattermost.com/core/channels/developers';
|
||||||
assert.equal(Utils.isValidURI(testURL), true);
|
assert.equal(urlUtils.isValidURI(testURL), true);
|
||||||
});
|
});
|
||||||
it('should be false for a malicious url', () => {
|
it('should be false for a malicious url', () => {
|
||||||
const testURL = String.raw`mattermost:///" --data-dir "\\deans-mbp\mattermost`;
|
const testURL = String.raw`mattermost:///" --data-dir "\\deans-mbp\mattermost`;
|
||||||
assert.equal(Utils.isValidURI(testURL), false);
|
assert.equal(urlUtils.isValidURI(testURL), false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('isInternalURL', () => {
|
describe('isInternalURL', () => {
|
||||||
it('should be false for different hosts', () => {
|
it('should be false for different hosts', () => {
|
||||||
const currentURL = url.parse('http://localhost/team/channel1');
|
const currentURL = new URL('http://localhost/team/channel1');
|
||||||
const targetURL = url.parse('http://example.com/team/channel2');
|
const targetURL = new URL('http://example.com/team/channel2');
|
||||||
const basename = '/';
|
const basename = '/';
|
||||||
assert.equal(Utils.isInternalURL(targetURL, currentURL, basename), false);
|
assert.equal(urlUtils.isInternalURL(targetURL, currentURL, basename), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be false for same hosts, non-matching basename', () => {
|
it('should be false for same hosts, non-matching basename', () => {
|
||||||
const currentURL = url.parse('http://localhost/subpath/team/channel1');
|
const currentURL = new URL('http://localhost/subpath/team/channel1');
|
||||||
const targetURL = url.parse('http://localhost/team/channel2');
|
const targetURL = new URL('http://localhost/team/channel2');
|
||||||
const basename = '/subpath';
|
const basename = '/subpath';
|
||||||
assert.equal(Utils.isInternalURL(targetURL, currentURL, basename), false);
|
assert.equal(urlUtils.isInternalURL(targetURL, currentURL, basename), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be true for same hosts, matching basename', () => {
|
it('should be true for same hosts, matching basename', () => {
|
||||||
const currentURL = url.parse('http://localhost/subpath/team/channel1');
|
const currentURL = new URL('http://localhost/subpath/team/channel1');
|
||||||
const targetURL = url.parse('http://localhost/subpath/team/channel2');
|
const targetURL = new URL('http://localhost/subpath/team/channel2');
|
||||||
const basename = '/subpath';
|
const basename = '/subpath';
|
||||||
assert.equal(Utils.isInternalURL(targetURL, currentURL, basename), true);
|
assert.equal(urlUtils.isInternalURL(targetURL, currentURL, basename), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be true for same hosts, default basename', () => {
|
it('should be true for same hosts, default basename', () => {
|
||||||
const currentURL = url.parse('http://localhost/team/channel1');
|
const currentURL = new URL('http://localhost/team/channel1');
|
||||||
const targetURL = url.parse('http://localhost/team/channel2');
|
const targetURL = new URL('http://localhost/team/channel2');
|
||||||
const basename = '/';
|
const basename = '/';
|
||||||
assert.equal(Utils.isInternalURL(targetURL, currentURL, basename), true);
|
assert.equal(urlUtils.isInternalURL(targetURL, currentURL, basename), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be true for same hosts, default basename, empty target path', () => {
|
it('should be true for same hosts, default basename, empty target path', () => {
|
||||||
const currentURL = url.parse('http://localhost/team/channel1');
|
const currentURL = new URL('http://localhost/team/channel1');
|
||||||
const targetURL = url.parse('http://localhost/');
|
const targetURL = new URL('http://localhost/');
|
||||||
const basename = '/';
|
const basename = '/';
|
||||||
assert.equal(Utils.isInternalURL(targetURL, currentURL, basename), true);
|
assert.equal(urlUtils.isInternalURL(targetURL, currentURL, basename), true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getHost', () => {
|
describe('getHost', () => {
|
||||||
it('should return the origin of a well formed url', () => {
|
it('should return the origin of a well formed url', () => {
|
||||||
const myurl = 'https://mattermost.com/download';
|
const myurl = 'https://mattermost.com/download';
|
||||||
assert.equal(Utils.getHost(myurl), 'https://mattermost.com');
|
assert.equal(urlUtils.getHost(myurl), 'https://mattermost.com');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shoud raise an error on malformed urls', () => {
|
it('shoud raise an error on malformed urls', () => {
|
||||||
const myurl = 'http://example.com:-80/';
|
const myurl = 'http://example.com:-80/';
|
||||||
assert.throws(() => Utils.getHost(myurl), Error);
|
assert.throws(() => urlUtils.getHost(myurl), Error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
Loading…
Reference in a new issue