Mm 16694 master validate urls (#1000)

* validate urls before deeplink or link click

* tests for isValidURL utility function

* review change - invert condition

* add validation for loaded files

bounds-info.json, app-state.json, config.json

* further validation and tweaks

certificate.json, permission.json

* add 2 more files for validation

* parse and validate deeplinks

- includes fix for windows deeplink when app is open

* disable auto-updator when in dev

* Squirrel is not used anymore

* fix validating allowedProtocols

* discard any args following a deeplink url

* tweaks

* update test

* support scheme’s with and without slashes

* stop after finding the first occurance of a deep link

* test updates

* updates to run tests successfully

* port updates to validation from 4.2

* url validation updates

changed validation package to better support internal domains and punycode domains
This commit is contained in:
Dean Whillier
2019-09-09 12:38:31 -04:00
committed by GitHub
parent f12f9da798
commit e12d47ea62
25 changed files with 585 additions and 171 deletions

View File

@@ -14,8 +14,6 @@ import {Grid, Row} from 'react-bootstrap';
import {ipcRenderer, remote} from 'electron';
import Utils from '../../utils/util.js';
import LoginModal from './LoginModal.jsx';
import MattermostView from './MattermostView.jsx';
import TabBar from './TabBar.jsx';
@@ -30,11 +28,9 @@ export default class MainPage extends React.Component {
let key = this.props.initialIndex;
if (this.props.deeplinkingUrl !== null) {
for (let i = 0; i < this.props.teams.length; i++) {
if (this.props.deeplinkingUrl.includes(this.props.teams[i].url)) {
key = i;
break;
}
const parsedDeeplink = this.parseDeeplinkURL(this.props.deeplinkingUrl);
if (parsedDeeplink) {
key = parsedDeeplink.teamIndex;
}
}
@@ -62,6 +58,27 @@ export default class MainPage extends React.Component {
this.markReadAtActive = this.markReadAtActive.bind(this);
}
parseDeeplinkURL(deeplink, teams = this.props.teams) {
if (deeplink && Array.isArray(teams) && teams.length) {
const deeplinkURL = url.parse(deeplink);
let parsedDeeplink = null;
teams.forEach((team, index) => {
const teamURL = url.parse(team.url);
if (deeplinkURL.host === teamURL.host) {
parsedDeeplink = {
teamURL,
teamIndex: index,
originalURL: deeplinkURL,
url: `${teamURL.protocol}//${teamURL.host}${deeplinkURL.pathname}`,
path: deeplinkURL.pathname,
};
}
});
return parsedDeeplink;
}
return null;
}
componentDidMount() {
const self = this;
ipcRenderer.on('login-request', (event, request, authInfo) => {
@@ -141,15 +158,12 @@ export default class MainPage extends React.Component {
});
ipcRenderer.on('protocol-deeplink', (event, deepLinkUrl) => {
const lastUrlDomain = Utils.getDomain(deepLinkUrl);
for (let i = 0; i < this.props.teams.length; i++) {
if (lastUrlDomain === Utils.getDomain(self.refs[`mattermostView${i}`].getSrc())) {
if (this.state.key !== i) {
this.handleSelect(i);
}
self.refs[`mattermostView${i}`].handleDeepLink(deepLinkUrl.replace(lastUrlDomain, ''));
break;
const parsedDeeplink = this.parseDeeplinkURL(deepLinkUrl);
if (parsedDeeplink) {
if (this.state.key !== parsedDeeplink.teamIndex) {
this.handleSelect(parsedDeeplink.teamIndex);
}
self.refs[`mattermostView${parsedDeeplink.teamIndex}`].handleDeepLink(parsedDeeplink.path);
}
});
@@ -337,9 +351,12 @@ export default class MainPage extends React.Component {
const isActive = self.state.key === index;
let teamUrl = team.url;
const deeplinkingUrl = this.props.deeplinkingUrl;
if (deeplinkingUrl !== null && deeplinkingUrl.includes(teamUrl)) {
teamUrl = deeplinkingUrl;
if (this.props.deeplinkingUrl) {
const parsedDeeplink = this.parseDeeplinkURL(this.props.deeplinkingUrl, [team]);
if (parsedDeeplink) {
teamUrl = parsedDeeplink.url;
}
}
return (

View File

@@ -90,6 +90,9 @@ export default class MattermostView extends React.Component {
// Open link in browserWindow. for example, attached files.
webview.addEventListener('new-window', (e) => {
if (!Utils.isValidURL(e.url)) {
return;
}
const currentURL = url.parse(webview.getURL());
const destURL = url.parse(e.url);
if (destURL.protocol !== 'http:' && destURL.protocol !== 'https:' && destURL.protocol !== `${scheme}:`) {

View File

@@ -6,6 +6,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import {Modal, Button, FormGroup, FormControl, ControlLabel, HelpBlock} from 'react-bootstrap';
import Utils from '../../utils/util';
export default class NewTeamModal extends React.Component {
constructor() {
super();
@@ -54,6 +56,9 @@ export default class NewTeamModal extends React.Component {
if (!(/^https?:\/\/.*/).test(this.state.teamUrl.trim())) {
return 'URL should start with http:// or https://.';
}
if (!Utils.isValidURL(this.state.teamUrl.trim())) {
return 'URL is not formatted correctly.';
}
return null;
}

View File

@@ -21,6 +21,7 @@ const defaultPreferences = {
useSpellChecker: true,
enableHardwareAcceleration: true,
autostart: true,
spellCheckerLocale: 'en-US',
};
export default defaultPreferences;

View File

@@ -7,6 +7,8 @@ import path from 'path';
import {EventEmitter} from 'events';
import * as Validator from '../../main/Validator';
import defaultPreferences from './defaultPreferences';
import upgradeConfigData from './upgradePreferences';
import buildConfig from './buildConfig';
@@ -205,6 +207,18 @@ export default class Config extends EventEmitter {
let configData = {};
try {
configData = this.readFileSync(this.configFilePath);
// validate based on config file version
switch (configData.version) {
case 1:
configData = Validator.validateV1ConfigData(configData);
break;
default:
configData = Validator.validateV0ConfigData(configData);
}
if (!configData) {
throw new Error('Provided configuration file does not validate, using defaults instead.');
}
} catch (e) {
console.log('Failed to load configuration file from the filesystem. Using defaults.');
configData = this.copy(this.defaultConfigData);

View File

@@ -10,7 +10,6 @@ import {URL} from 'url';
import electron from 'electron';
import isDev from 'electron-is-dev';
import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-devtools-installer';
import {parse as parseArgv} from 'yargs';
import {protocols} from '../electron-builder.json';
@@ -33,6 +32,8 @@ import initCookieManager from './main/cookieManager';
import {shouldBeHiddenOnStartup} from './main/utils';
import SpellChecker from './main/SpellChecker';
import UserActivityMonitor from './main/UserActivityMonitor';
import Utils from './utils/util';
import parseArgs from './main/ParseArgs';
// pull out required electron components like this
// as not all components can be referenced before the app is ready
@@ -48,15 +49,14 @@ const {
} = electron;
const criticalErrorHandler = new CriticalErrorHandler();
const assetsDir = path.resolve(app.getAppPath(), 'assets');
const argv = parseArgv(process.argv.slice(1));
const hideOnStartup = shouldBeHiddenOnStartup(argv);
const loginCallbackMap = new Map();
const certificateStore = CertificateStore.load(path.resolve(app.getPath('userData'), 'certificate.json'));
const userActivityMonitor = new UserActivityMonitor();
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow = null;
let hideOnStartup = null;
let certificateStore = null;
let spellChecker = null;
let deeplinkingUrl = null;
let scheme = null;
@@ -74,15 +74,9 @@ async function initialize() {
process.on('uncaughtException', criticalErrorHandler.processUncaughtExceptionHandler.bind(criticalErrorHandler));
global.willAppQuit = false;
global.isDev = isDev && !argv.disableDevMode;
app.setAppUserModelId('com.squirrel.mattermost.Mattermost'); // Use explicit AppUserModelID
if (argv['data-dir']) {
app.setPath('userData', path.resolve(argv['data-dir']));
}
// initialization that can run before the app is ready
initializeArgs();
initializeConfig();
initializeAppEventListeners();
initializeBeforeAppReady();
@@ -115,6 +109,24 @@ try {
// initialization sub functions
//
function initializeArgs() {
global.args = parseArgs(process.argv.slice(1));
// output the application version via cli when requested (-v or --version)
if (global.args.version) {
process.stdout.write(`v.${app.getVersion()}\n`);
process.exit(0); // eslint-disable-line no-process-exit
}
hideOnStartup = shouldBeHiddenOnStartup(global.args);
global.isDev = isDev && !global.args.disableDevMode; // this doesn't seem to be right and isn't used as the single source of truth
if (global.args['data-dir']) {
app.setPath('userData', path.resolve(global.args['data-dir']));
}
}
function initializeConfig() {
registryConfig = new RegistryConfig();
config = new Config(app.getPath('userData') + '/config.json');
@@ -136,6 +148,8 @@ function initializeAppEventListeners() {
}
function initializeBeforeAppReady() {
certificateStore = CertificateStore.load(path.resolve(app.getPath('userData'), 'certificate.json'));
// can only call this before the app is ready
if (config.enableHardwareAcceleration === false) {
app.disableHardwareAcceleration();
@@ -179,7 +193,7 @@ function initializeInterCommunicationEventListeners() {
if (shouldShowTrayIcon()) {
ipcMain.on('update-unread', handleUpdateUnreadEvent);
}
if (config.enableAutoUpdater) {
if (!isDev && config.enableAutoUpdater) {
ipcMain.on('check-for-updates', autoUpdater.checkForUpdates);
}
}
@@ -189,7 +203,7 @@ function initializeMainWindowListeners() {
mainWindow.on('unresponsive', criticalErrorHandler.windowUnresponsiveHandler.bind(criticalErrorHandler));
mainWindow.webContents.on('crashed', handleMainWindowWebContentsCrashed);
mainWindow.on('ready-to-show', handleMainWindowReadyToShow);
if (config.enableAutoUpdater) {
if (!isDev && config.enableAutoUpdater) {
mainWindow.once('show', handleMainWindowShow);
}
}
@@ -233,13 +247,12 @@ function handleReloadConfig() {
//
// activate first app instance, subsequent instances will quit themselves
function handleAppSecondInstance(event, secondArgv) {
function handleAppSecondInstance(event, argv) {
// Protocol handler for win32
// argv: An array of the second instances (command line / deep linked) arguments
if (process.platform === 'win32') {
// Keep only command line / deep linked arguments
if (Array.isArray(secondArgv.slice(1)) && secondArgv.slice(1).length > 0) {
setDeeplinkingUrl(secondArgv.slice(1)[0]);
deeplinkingUrl = getDeeplinkingURL(argv);
if (deeplinkingUrl) {
mainWindow.webContents.send('protocol-deeplink', deeplinkingUrl);
}
}
@@ -337,12 +350,14 @@ function handleAppWillFinishLaunching() {
// Protocol handler for osx
app.on('open-url', (event, url) => {
event.preventDefault();
setDeeplinkingUrl(url);
deeplinkingUrl = getDeeplinkingURL([url]);
if (app.isReady()) {
function openDeepLink() {
try {
mainWindow.webContents.send('protocol-deeplink', deeplinkingUrl);
mainWindow.show();
if (deeplinkingUrl) {
mainWindow.webContents.send('protocol-deeplink', deeplinkingUrl);
mainWindow.show();
}
} catch (err) {
setTimeout(openDeepLink, 1000);
}
@@ -379,6 +394,8 @@ function handleAppWebContentsCreated(dc, contents) {
}
function initializeAfterAppReady() {
app.setAppUserModelId('Mattermost.Desktop'); // Use explicit AppUserModelID
const appStateJson = path.join(app.getPath('userData'), 'app-state.json');
appState = new AppStateManager(appStateJson);
if (wasUpdated(appState.lastAppVersion)) {
@@ -398,13 +415,9 @@ function initializeAfterAppReady() {
// Protocol handler for win32
if (process.platform === 'win32') {
// Keep only command line / deep linked argument. Make sure it's not squirrel command
const tmpArgs = process.argv.slice(1);
if (
Array.isArray(tmpArgs) && tmpArgs.length > 0 &&
tmpArgs[0].match(/^--squirrel-/) === null
) {
setDeeplinkingUrl(tmpArgs[0]);
const args = process.argv.slice(1);
if (Array.isArray(args) && args.length > 0) {
deeplinkingUrl = getDeeplinkingURL(args);
}
}
@@ -502,7 +515,7 @@ function initializeAfterAppReady() {
permissionManager = new PermissionManager(permissionFile, trustedURLs);
session.defaultSession.setPermissionRequestHandler(permissionRequestHandler(mainWindow, permissionManager));
if (config.enableAutoUpdater) {
if (!isDev && config.enableAutoUpdater) {
const updaterConfig = autoUpdater.loadConfig();
autoUpdater.initialize(appState, mainWindow, updaterConfig.isNotifyOnly());
ipcMain.on('check-for-updates', autoUpdater.checkForUpdates);
@@ -680,16 +693,20 @@ function handleMainWindowWebContentsCrashed() {
}
function handleMainWindowReadyToShow() {
autoUpdater.checkForUpdates();
if (!isDev) {
autoUpdater.checkForUpdates();
}
}
function handleMainWindowShow() {
if (autoUpdater.shouldCheckForUpdatesOnStart(appState.updateCheckedDate)) {
ipcMain.emit('check-for-updates');
} else {
setTimeout(() => {
if (!isDev) {
if (autoUpdater.shouldCheckForUpdatesOnStart(appState.updateCheckedDate)) {
ipcMain.emit('check-for-updates');
}, autoUpdater.UPDATER_INTERVAL_IN_MS);
} else {
setTimeout(() => {
ipcMain.emit('check-for-updates');
}, autoUpdater.UPDATER_INTERVAL_IN_MS);
}
}
}
@@ -757,10 +774,15 @@ function switchMenuIconImages(icons, isDarkMode) {
}
}
function setDeeplinkingUrl(url) {
if (scheme) {
deeplinkingUrl = url.replace(new RegExp('^' + scheme), 'https');
function getDeeplinkingURL(args) {
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)
const url = args[args.length - 1];
if (url && scheme && url.startsWith(scheme) && Utils.isValidURI(url)) {
return url;
}
}
return null;
}
function shouldShowTrayIcon() {

View File

@@ -3,7 +3,18 @@
// See LICENSE.txt for license information.
import JsonFileManager from '../common/JsonFileManager';
import * as Validator from './Validator';
export default class AppStateManager extends JsonFileManager {
constructor(file) {
super(file);
// ensure data loaded from file is valid
const validatedJSON = Validator.validateAppState(this.json);
if (!validatedJSON) {
this.setJson({});
}
}
set lastAppVersion(version) {
this.setValue('lastAppVersion', version);
}

37
src/main/ParseArgs.js Normal file
View File

@@ -0,0 +1,37 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import yargs from 'yargs';
import {protocols} from '../../electron-builder.json';
import * as Validator from './Validator';
export default function parse(args) {
return validateArgs(parseArgs(triageArgs(args)));
}
function triageArgs(args) {
// ensure any args following a possible deeplink are discarded
if (protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0]) {
const scheme = protocols[0].schemes[0].toLowerCase();
const deeplinkIndex = args.findIndex((arg) => arg.toLowerCase().includes(`${scheme}:`));
if (deeplinkIndex !== -1) {
return args.slice(0, deeplinkIndex + 1);
}
}
return args;
}
function parseArgs(args) {
return yargs.
boolean('hidden').describe('hidden', 'Launch the app in hidden mode.').
alias('disable-dev-mode', 'disableDevMode').boolean('disable-dev-mode').describe('disable-dev-mode', 'Disable dev mode.').
alias('data-dir', 'dataDir').string('data-dir').describe('data-dir', 'Set the path to where user data is stored.').
alias('v', 'version').boolean('v').describe('version', 'Prints the application version.').
parse(args);
}
function validateArgs(args) {
return Validator.validateArgs(args) || {};
}

View File

@@ -5,6 +5,8 @@ import fs from 'fs';
import utils from '../utils/util';
import * as Validator from './Validator';
const PERMISSION_GRANTED = 'granted';
const PERMISSION_DENIED = 'denied';
@@ -15,6 +17,10 @@ export default class PermissionManager {
if (fs.existsSync(file)) {
try {
this.permissions = JSON.parse(fs.readFileSync(this.file, 'utf-8'));
this.permissions = Validator.validatePermissionsList(this.permissions);
if (!this.permissions) {
throw new Error('Provided permissions file does not validate, using defaults instead.');
}
} catch (err) {
console.error(err);
this.permissions = {};

151
src/main/Validator.js Normal file
View File

@@ -0,0 +1,151 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Joi from '@hapi/joi';
import Utils from '../utils/util';
const defaultOptions = {
stripUnknown: true,
};
const defaultWindowWidth = 1000;
const defaultWindowHeight = 700;
const minWindowWidth = 400;
const minWindowHeight = 240;
const argsSchema = Joi.object({
hidden: Joi.boolean(),
'disable-dev-mode': Joi.boolean(),
disableDevMode: Joi.boolean(),
'data-dir': Joi.string(),
dataDir: Joi.array().items(Joi.string()),
version: Joi.boolean(),
});
const boundsInfoSchema = Joi.object({
x: Joi.number().integer().min(0),
y: Joi.number().integer().min(0),
width: Joi.number().integer().min(minWindowWidth).required().default(defaultWindowWidth),
height: Joi.number().integer().min(minWindowHeight).required().default(defaultWindowHeight),
maximized: Joi.boolean().default(false),
fullscreen: Joi.boolean().default(false),
});
const appStateSchema = Joi.object({
lastAppVersion: Joi.string(),
skippedVersion: Joi.string(),
updateCheckedDate: Joi.string(),
});
const configDataSchemaV0 = Joi.object({
url: Joi.string().required(),
});
const configDataSchemaV1 = Joi.object({
version: Joi.number().min(1).default(1),
teams: Joi.array().items(Joi.object({
name: Joi.string().required(),
url: Joi.string().required(),
})).default([]),
showTrayIcon: Joi.boolean().default(false),
trayIconTheme: Joi.any().allow('').valid('light', 'dark').default('light'),
minimizeToTray: Joi.boolean().default(false),
notifications: Joi.object({
flashWindow: Joi.any().valid(0, 2).default(0),
bounceIcon: Joi.boolean().default(false),
bounceIconType: Joi.any().allow('').valid('informational', 'critical').default('informational'),
}),
showUnreadBadge: Joi.boolean().default(true),
useSpellChecker: Joi.boolean().default(true),
enableHardwareAcceleration: Joi.boolean().default(true),
autostart: Joi.boolean().default(true),
spellCheckerLocale: Joi.string().regex(/^[a-z]{2}-[A-Z]{2}$/).default('en-US'),
});
// eg. data['https://community.mattermost.com']['notifications'] = 'granted';
// eg. data['http://localhost:8065']['notifications'] = 'denied';
const permissionsSchema = Joi.object().pattern(
Joi.string().uri(),
Joi.object().pattern(
Joi.string(),
Joi.any().valid('granted', 'denied'),
),
);
// eg. data['community.mattermost.com'] = { data: 'certificate data', issuerName: 'COMODO RSA Domain Validation Secure Server CA'};
const certificateStoreSchema = Joi.object().pattern(
Joi.string().uri(),
Joi.object({
data: Joi.string(),
issuerName: Joi.string(),
})
);
const allowedProtocolsSchema = Joi.array().items(Joi.string().regex(/^[a-z-]+:$/i));
// validate bounds_info.json
export function validateArgs(data) {
return validateAgainstSchema(data, argsSchema);
}
// validate bounds_info.json
export function validateBoundsInfo(data) {
return validateAgainstSchema(data, boundsInfoSchema);
}
// validate app_state.json
export function validateAppState(data) {
return validateAgainstSchema(data, appStateSchema);
}
// validate v.0 config.json
export function validateV0ConfigData(data) {
return validateAgainstSchema(data, configDataSchemaV0);
}
// validate v.1 config.json
export function validateV1ConfigData(data) {
if (Array.isArray(data.teams) && data.teams.length) {
// first replace possible backslashes with forward slashes
let teams = data.teams.map(({name, url}) => {
let updatedURL = url;
if (updatedURL.includes('\\')) {
updatedURL = updatedURL.toLowerCase().replace(/\\/gi, '/');
}
return {name, url: updatedURL};
});
// next filter out urls that are still invalid so all is not lost
teams = teams.filter(({url}) => Utils.isValidURL(url));
// replace original teams
data.teams = teams;
}
return validateAgainstSchema(data, configDataSchemaV1);
}
// validate permission.json
export function validatePermissionsList(data) {
return validateAgainstSchema(data, permissionsSchema);
}
// validate certificate.json
export function validateCertificateStore(data) {
return validateAgainstSchema(data, certificateStoreSchema);
}
// validate allowedProtocols.json
export function validateAllowedProtocols(data) {
return validateAgainstSchema(data, allowedProtocolsSchema);
}
function validateAgainstSchema(data, schema) {
if (typeof data !== 'object' || !schema) {
return false;
}
const {error, value} = Joi.validate(data, schema, defaultOptions);
if (error) {
return false;
}
return value;
}

View File

@@ -8,6 +8,8 @@ import fs from 'fs';
import {app, dialog, ipcMain, shell} from 'electron';
import * as Validator from './Validator';
const allowedProtocolFile = path.resolve(app.getPath('userData'), 'allowedProtocols.json');
let allowedProtocols = [];
@@ -15,6 +17,7 @@ function init(mainWindow) {
fs.readFile(allowedProtocolFile, 'utf-8', (err, data) => {
if (!err) {
allowedProtocols = JSON.parse(data);
allowedProtocols = Validator.validateAllowedProtocols(allowedProtocols) || [];
}
initDialogEvent(mainWindow);
});

View File

@@ -6,6 +6,8 @@
import fs from 'fs';
import url from 'url';
import * as Validator from './Validator';
function comparableCertificate(certificate) {
return {
data: certificate.data.toString(),
@@ -32,6 +34,10 @@ function CertificateStore(storeFile) {
let storeStr;
try {
storeStr = fs.readFileSync(storeFile, 'utf-8');
storeStr = Validator.validateCertificateStore(storeStr);
if (!storeStr) {
throw new Error('Provided certificate store file does not validate, using defaults instead.');
}
} catch (e) {
storeStr = '{}';
}

View File

@@ -6,6 +6,8 @@ import path from 'path';
import {app, BrowserWindow} from 'electron';
import * as Validator from './Validator';
function saveWindowState(file, window) {
const windowState = window.getBounds();
windowState.maximized = window.isMaximized();
@@ -28,6 +30,10 @@ function createMainWindow(config, options) {
let windowOptions;
try {
windowOptions = JSON.parse(fs.readFileSync(boundsInfoPath, 'utf-8'));
windowOptions = Validator.validateBoundsInfo(windowOptions);
if (!windowOptions) {
throw new Error('Provided bounds info file does not validate, using defaults instead.');
}
} catch (e) {
// Follow Electron's defaults, except for window dimensions which targets 1024x768 screen resolution.
windowOptions = {width: defaultWindowWidth, height: defaultWindowHeight};

View File

@@ -1,43 +0,0 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import AutoLaunch from 'auto-launch';
import {app} from 'electron';
function shouldQuitApp(cmd) {
if (process.platform !== 'win32') {
return false;
}
const squirrelCommands = ['--squirrel-install', '--squirrel-updated', '--squirrel-uninstall', '--squirrel-obsolete'];
return squirrelCommands.includes(cmd);
}
async function setupAutoLaunch(cmd) {
const appLauncher = new AutoLaunch({
name: app.getName(),
isHidden: true,
});
if (cmd === '--squirrel-uninstall') {
// If we're uninstalling, make sure we also delete our auto launch registry key
await appLauncher.disable();
} else if (cmd === '--squirrel-install' || cmd === '--squirrel-updated') {
// If we're updating and already have an registry entry for auto launch, make sure to update the path
const enabled = await appLauncher.isEnabled();
if (enabled) {
await appLauncher.enable();
}
}
}
export default function squirrelStartup(callback) {
if (process.platform === 'win32') {
const cmd = process.argv[1];
setupAutoLaunch(cmd).then(() => {
if (require('electron-squirrel-startup') && callback) { // eslint-disable-line global-require
callback();
}
});
return shouldQuitApp(cmd);
}
return false;
}

63
src/package-lock.json generated
View File

@@ -26,6 +26,47 @@
"regenerator-runtime": "^0.12.0"
}
},
"@hapi/address": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.0.0.tgz",
"integrity": "sha512-mV6T0IYqb0xL1UALPFplXYQmR0twnXG0M6jUswpquqT2sD12BOiCiLy3EvMp/Fy7s3DZElC4/aPjEjo2jeZpvw=="
},
"@hapi/hoek": {
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-6.2.4.tgz",
"integrity": "sha512-HOJ20Kc93DkDVvjwHyHawPwPkX44sIrbXazAUDiUXaY2R9JwQGo2PhFfnQtdrsIe4igjG2fPgMra7NYw7qhy0A=="
},
"@hapi/joi": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.0.tgz",
"integrity": "sha512-n6kaRQO8S+kepUTbXL9O/UOL788Odqs38/VOfoCrATDtTvyfiO3fgjlSRaNkHabpTLgM7qru9ifqXlXbXk8SeQ==",
"requires": {
"@hapi/address": "2.x.x",
"@hapi/hoek": "6.x.x",
"@hapi/marker": "1.x.x",
"@hapi/topo": "3.x.x"
}
},
"@hapi/marker": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@hapi/marker/-/marker-1.0.0.tgz",
"integrity": "sha512-JOfdekTXnJexfE8PyhZFyHvHjt81rBFSAbTIRAhF2vv/2Y1JzoKsGqxH/GpZJoF7aEfYok8JVcAHmSz1gkBieA=="
},
"@hapi/topo": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.2.tgz",
"integrity": "sha512-r+aumOqJ5QbD6aLPJWqVjMAPsx5pZKz+F5yPqXZ/WWG9JTtHbQqlzrJoknJ0iJxLj9vlXtmpSdjlkszseeG8OA==",
"requires": {
"@hapi/hoek": "8.x.x"
},
"dependencies": {
"@hapi/hoek": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.0.2.tgz",
"integrity": "sha512-O6o6mrV4P65vVccxymuruucb+GhP2zl9NLCG8OdoFRS8BEGw3vwpPp20wpAtpbQQxz1CEUtmxJGgWhjq1XA3qw=="
}
}
},
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
@@ -171,6 +212,10 @@
"resolved": "https://registry.npmjs.org/cross-unzip/-/cross-unzip-0.0.2.tgz",
"integrity": "sha1-UYO8R6CVWb78+YzEZXlkmZNZNy8="
},
"damerau-levenshtein": {
"version": "git://github.com/cbaatz/damerau-levenshtein.git#8d7440a51ae9dc6912e44385115c7cb1da4e8ebc",
"from": "git://github.com/cbaatz/damerau-levenshtein.git"
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@@ -228,9 +273,9 @@
"integrity": "sha512-iwM3EotA9HTXqMGpQRkR/kT8OZqBbdfHTnlwcxsjSLYqY8svvsq0MuujsWCn3/vtgRmDv/PC/gKUUpoZvi5C1w=="
},
"electron-log": {
"version": "2.2.15",
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.15.tgz",
"integrity": "sha512-lpTQmU9ZjTtTkg2hHEQCvnrkrqLwvhfnMCXPhjSWA5sFXvybGMn13M0xU3CbvVbZuHSFZww6t7HyWGt+Tnye0g=="
"version": "2.2.17",
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.17.tgz",
"integrity": "sha512-v+Af5W5z99ehhaLOfE9eTSXUwjzh2wFlQjz51dvkZ6ZIrET6OB/zAZPvsuwT6tm3t5x+M1r+Ed3U3xtPZYAyuQ=="
},
"electron-updater": {
"version": "4.0.6",
@@ -638,14 +683,9 @@
"integrity": "sha512-RGgpBBn4p65nOhZdBHDt84Remz3QvmoQDppKJX0tYS53SbuUJwm7zG1swT5O770zPvN6wZaZlc8ie/jwlZbeHA==",
"requires": {
"binarysearch": "^0.2.4",
"damerau-levenshtein": "git://github.com/cbaatz/damerau-levenshtein.git",
"strip-bom": "^2.0.0",
"unzip-stream": "^0.3.0"
},
"dependencies": {
"damerau-levenshtein": {
"version": "git://github.com/cbaatz/damerau-levenshtein.git#8d7440a51ae9dc6912e44385115c7cb1da4e8ebc",
"from": "git://github.com/cbaatz/damerau-levenshtein.git#8d7440a51ae9dc6912e44385115c7cb1da4e8ebc"
}
}
},
"sort-keys": {
@@ -755,6 +795,11 @@
"mkdirp": "^0.5.1"
}
},
"valid-url": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz",
"integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA="
},
"warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz",

View File

@@ -9,12 +9,13 @@
"homepage": "https://about.mattermost.com",
"license": "Apache-2.0",
"dependencies": {
"@hapi/joi": "^15.1.0",
"auto-launch": "^5.0.5",
"bootstrap": "^3.3.7",
"electron-context-menu": "^0.10.1",
"electron-devtools-installer": "^2.2.4",
"electron-is-dev": "^1.0.1",
"electron-log": "^2.2.15",
"electron-log": "^2.2.17",
"electron-updater": "4.0.6",
"prop-types": "^15.6.2",
"react": "^16.6.3",
@@ -24,6 +25,7 @@
"semver": "^5.5.0",
"simple-spellchecker": "^0.9.8",
"underscore": "^1.9.1",
"valid-url": "^1.0.9",
"winreg": "^1.2.4",
"yargs": "^3.32.0"
}

View File

@@ -3,11 +3,21 @@
// See LICENSE.txt for license information.
import url from 'url';
import {isUri, isHttpUri, isHttpsUri} from 'valid-url';
function getDomain(inputURL) {
const parsedURL = url.parse(inputURL);
return `${parsedURL.protocol}//${parsedURL.host}`;
}
function isValidURL(testURL) {
return Boolean(isHttpUri(testURL) || isHttpsUri(testURL));
}
function isValidURI(testURL) {
return Boolean(isUri(testURL));
}
// 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
@@ -25,5 +35,7 @@ function isInternalURL(targetURL, currentURL, basename = '/') {
export default {
getDomain,
isValidURL,
isValidURI,
isInternalURL,
};