[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:
FalseHonesty 2020-11-04 14:59:07 -05:00 committed by GitHub
parent ad1871ad95
commit 5d0a937bb9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 308 additions and 299 deletions

View file

@ -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 = (

View file

@ -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.

View file

@ -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;

View file

@ -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}`);

View file

@ -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;
} }

View file

@ -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) {

View file

@ -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) {

View file

@ -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.

View file

@ -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
@ -621,8 +628,8 @@ function handleAppWebContentsCreated(dc, contents) {
mainWindow.webContents.send('zoom-reset'); mainWindow.webContents.send('zoom-reset');
break; break;
// Manually handle undo/redo keyboard shortcuts // Manually handle undo/redo keyboard shortcuts
// - temporary fix for https://mattermost.atlassian.net/browse/MM-19198 // - temporary fix for https://mattermost.atlassian.net/browse/MM-19198
case 'z': case 'z':
if (input.shift) { if (input.shift) {
mainWindow.webContents.send('redo'); mainWindow.webContents.send('redo');
@ -631,7 +638,7 @@ function handleAppWebContentsCreated(dc, contents) {
} }
break; break;
// Manually handle copy/cut/paste keyboard shortcuts // Manually handle copy/cut/paste keyboard shortcuts
case 'c': case 'c':
mainWindow.webContents.send('copy'); mainWindow.webContents.send('copy');
break; break;
@ -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;
} }
} }

View file

@ -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;

View file

@ -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;
} }

View file

@ -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
View 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,
};

View file

@ -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,
}; };

View file

@ -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.

View file

@ -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);
}); });
}); });
}); });