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:
parent
f12f9da798
commit
e12d47ea62
53
package-lock.json
generated
53
package-lock.json
generated
|
@ -5180,9 +5180,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eslint-plugin-eslint-comments": {
|
"eslint-plugin-eslint-comments": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.1.2.tgz",
|
||||||
"integrity": "sha512-GZDKhOFqJLKlaABX+kdoLskcTINMrVOWxGca54KcFb1QCPd0CLmqgAMRxkkUfGSmN+5NJUMGh7NGccIMcWPSfQ==",
|
"integrity": "sha512-QexaqrNeteFfRTad96W+Vi4Zj1KFbkHHNMMaHZEYcovKav6gdomyGzaxSDSL3GoIyUOo078wRAdYlu1caiauIQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"escape-string-regexp": "^1.0.5",
|
"escape-string-regexp": "^1.0.5",
|
||||||
|
@ -5190,9 +5190,9 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ignore": {
|
"ignore": {
|
||||||
"version": "5.0.5",
|
"version": "5.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz",
|
||||||
"integrity": "sha512-kOC8IUb8HSDMVcYrDVezCxpJkzSQWTAzf3olpKM6o9rM5zpojx23O0Fl8Wr4+qJ6ZbPEHqf1fdwev/DS7v7pmA==",
|
"integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6114,7 +6114,8 @@
|
||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"aproba": {
|
"aproba": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
@ -6135,12 +6136,14 @@
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
|
@ -6155,17 +6158,20 @@
|
||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
@ -6282,7 +6288,8 @@
|
||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
|
@ -6294,6 +6301,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
@ -6308,6 +6316,7 @@
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
|
@ -6315,12 +6324,14 @@
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.3.5",
|
"version": "2.3.5",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.2",
|
"safe-buffer": "^5.1.2",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
|
@ -6339,6 +6350,7 @@
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
|
@ -6419,7 +6431,8 @@
|
||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
|
@ -6431,6 +6444,7 @@
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
|
@ -6516,7 +6530,8 @@
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
|
@ -6552,6 +6567,7 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
|
@ -6571,6 +6587,7 @@
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-regex": "^2.0.0"
|
"ansi-regex": "^2.0.0"
|
||||||
}
|
}
|
||||||
|
@ -6614,12 +6631,14 @@
|
||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"yallist": {
|
"yallist": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
"eslint": "^5.9.0",
|
"eslint": "^5.9.0",
|
||||||
"eslint-config-mattermost": "github:mattermost/eslint-config-mattermost",
|
"eslint-config-mattermost": "github:mattermost/eslint-config-mattermost",
|
||||||
"eslint-plugin-cypress": "^2.1.2",
|
"eslint-plugin-cypress": "^2.1.2",
|
||||||
"eslint-plugin-eslint-comments": "^3.1.1",
|
"eslint-plugin-eslint-comments": "^3.1.2",
|
||||||
"eslint-plugin-header": "^2.0.0",
|
"eslint-plugin-header": "^2.0.0",
|
||||||
"eslint-plugin-import": "^2.14.0",
|
"eslint-plugin-import": "^2.14.0",
|
||||||
"eslint-plugin-react": "^7.11.1",
|
"eslint-plugin-react": "^7.11.1",
|
||||||
|
|
|
@ -14,8 +14,6 @@ import {Grid, Row} from 'react-bootstrap';
|
||||||
|
|
||||||
import {ipcRenderer, remote} from 'electron';
|
import {ipcRenderer, remote} from 'electron';
|
||||||
|
|
||||||
import Utils from '../../utils/util.js';
|
|
||||||
|
|
||||||
import LoginModal from './LoginModal.jsx';
|
import LoginModal from './LoginModal.jsx';
|
||||||
import MattermostView from './MattermostView.jsx';
|
import MattermostView from './MattermostView.jsx';
|
||||||
import TabBar from './TabBar.jsx';
|
import TabBar from './TabBar.jsx';
|
||||||
|
@ -30,11 +28,9 @@ export default class MainPage extends React.Component {
|
||||||
|
|
||||||
let key = this.props.initialIndex;
|
let key = this.props.initialIndex;
|
||||||
if (this.props.deeplinkingUrl !== null) {
|
if (this.props.deeplinkingUrl !== null) {
|
||||||
for (let i = 0; i < this.props.teams.length; i++) {
|
const parsedDeeplink = this.parseDeeplinkURL(this.props.deeplinkingUrl);
|
||||||
if (this.props.deeplinkingUrl.includes(this.props.teams[i].url)) {
|
if (parsedDeeplink) {
|
||||||
key = i;
|
key = parsedDeeplink.teamIndex;
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +58,27 @@ export default class MainPage extends React.Component {
|
||||||
this.markReadAtActive = this.markReadAtActive.bind(this);
|
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() {
|
componentDidMount() {
|
||||||
const self = this;
|
const self = this;
|
||||||
ipcRenderer.on('login-request', (event, request, authInfo) => {
|
ipcRenderer.on('login-request', (event, request, authInfo) => {
|
||||||
|
@ -141,15 +158,12 @@ export default class MainPage extends React.Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcRenderer.on('protocol-deeplink', (event, deepLinkUrl) => {
|
ipcRenderer.on('protocol-deeplink', (event, deepLinkUrl) => {
|
||||||
const lastUrlDomain = Utils.getDomain(deepLinkUrl);
|
const parsedDeeplink = this.parseDeeplinkURL(deepLinkUrl);
|
||||||
for (let i = 0; i < this.props.teams.length; i++) {
|
if (parsedDeeplink) {
|
||||||
if (lastUrlDomain === Utils.getDomain(self.refs[`mattermostView${i}`].getSrc())) {
|
if (this.state.key !== parsedDeeplink.teamIndex) {
|
||||||
if (this.state.key !== i) {
|
this.handleSelect(parsedDeeplink.teamIndex);
|
||||||
this.handleSelect(i);
|
|
||||||
}
|
|
||||||
self.refs[`mattermostView${i}`].handleDeepLink(deepLinkUrl.replace(lastUrlDomain, ''));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
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;
|
const isActive = self.state.key === index;
|
||||||
|
|
||||||
let teamUrl = team.url;
|
let teamUrl = team.url;
|
||||||
const deeplinkingUrl = this.props.deeplinkingUrl;
|
|
||||||
if (deeplinkingUrl !== null && deeplinkingUrl.includes(teamUrl)) {
|
if (this.props.deeplinkingUrl) {
|
||||||
teamUrl = deeplinkingUrl;
|
const parsedDeeplink = this.parseDeeplinkURL(this.props.deeplinkingUrl, [team]);
|
||||||
|
if (parsedDeeplink) {
|
||||||
|
teamUrl = parsedDeeplink.url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -90,6 +90,9 @@ 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.isValidURL(e.url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const currentURL = url.parse(webview.getURL());
|
const currentURL = url.parse(webview.getURL());
|
||||||
const destURL = url.parse(e.url);
|
const destURL = url.parse(e.url);
|
||||||
if (destURL.protocol !== 'http:' && destURL.protocol !== 'https:' && destURL.protocol !== `${scheme}:`) {
|
if (destURL.protocol !== 'http:' && destURL.protocol !== 'https:' && destURL.protocol !== `${scheme}:`) {
|
||||||
|
|
|
@ -6,6 +6,8 @@ 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';
|
||||||
|
|
||||||
export default class NewTeamModal extends React.Component {
|
export default class NewTeamModal extends React.Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
@ -54,6 +56,9 @@ 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())) {
|
||||||
|
return 'URL is not formatted correctly.';
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ const defaultPreferences = {
|
||||||
useSpellChecker: true,
|
useSpellChecker: true,
|
||||||
enableHardwareAcceleration: true,
|
enableHardwareAcceleration: true,
|
||||||
autostart: true,
|
autostart: true,
|
||||||
|
spellCheckerLocale: 'en-US',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defaultPreferences;
|
export default defaultPreferences;
|
||||||
|
|
|
@ -7,6 +7,8 @@ import path from 'path';
|
||||||
|
|
||||||
import {EventEmitter} from 'events';
|
import {EventEmitter} from 'events';
|
||||||
|
|
||||||
|
import * as Validator from '../../main/Validator';
|
||||||
|
|
||||||
import defaultPreferences from './defaultPreferences';
|
import defaultPreferences from './defaultPreferences';
|
||||||
import upgradeConfigData from './upgradePreferences';
|
import upgradeConfigData from './upgradePreferences';
|
||||||
import buildConfig from './buildConfig';
|
import buildConfig from './buildConfig';
|
||||||
|
@ -205,6 +207,18 @@ export default class Config extends EventEmitter {
|
||||||
let configData = {};
|
let configData = {};
|
||||||
try {
|
try {
|
||||||
configData = this.readFileSync(this.configFilePath);
|
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) {
|
} catch (e) {
|
||||||
console.log('Failed to load configuration file from the filesystem. Using defaults.');
|
console.log('Failed to load configuration file from the filesystem. Using defaults.');
|
||||||
configData = this.copy(this.defaultConfigData);
|
configData = this.copy(this.defaultConfigData);
|
||||||
|
|
80
src/main.js
80
src/main.js
|
@ -10,7 +10,6 @@ import {URL} from 'url';
|
||||||
import electron from 'electron';
|
import electron from 'electron';
|
||||||
import isDev from 'electron-is-dev';
|
import isDev from 'electron-is-dev';
|
||||||
import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-devtools-installer';
|
import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-devtools-installer';
|
||||||
import {parse as parseArgv} from 'yargs';
|
|
||||||
|
|
||||||
import {protocols} from '../electron-builder.json';
|
import {protocols} from '../electron-builder.json';
|
||||||
|
|
||||||
|
@ -33,6 +32,8 @@ import initCookieManager from './main/cookieManager';
|
||||||
import {shouldBeHiddenOnStartup} from './main/utils';
|
import {shouldBeHiddenOnStartup} from './main/utils';
|
||||||
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 parseArgs from './main/ParseArgs';
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -48,15 +49,14 @@ const {
|
||||||
} = electron;
|
} = electron;
|
||||||
const criticalErrorHandler = new CriticalErrorHandler();
|
const criticalErrorHandler = new CriticalErrorHandler();
|
||||||
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||||
const argv = parseArgv(process.argv.slice(1));
|
|
||||||
const hideOnStartup = shouldBeHiddenOnStartup(argv);
|
|
||||||
const loginCallbackMap = new Map();
|
const loginCallbackMap = new Map();
|
||||||
const certificateStore = CertificateStore.load(path.resolve(app.getPath('userData'), 'certificate.json'));
|
|
||||||
const userActivityMonitor = new UserActivityMonitor();
|
const userActivityMonitor = new UserActivityMonitor();
|
||||||
|
|
||||||
// Keep a global reference of the window object, if you don't, the window will
|
// 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.
|
// be closed automatically when the JavaScript object is garbage collected.
|
||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
|
let hideOnStartup = null;
|
||||||
|
let certificateStore = null;
|
||||||
let spellChecker = null;
|
let spellChecker = null;
|
||||||
let deeplinkingUrl = null;
|
let deeplinkingUrl = null;
|
||||||
let scheme = null;
|
let scheme = null;
|
||||||
|
@ -74,15 +74,9 @@ async function initialize() {
|
||||||
process.on('uncaughtException', criticalErrorHandler.processUncaughtExceptionHandler.bind(criticalErrorHandler));
|
process.on('uncaughtException', criticalErrorHandler.processUncaughtExceptionHandler.bind(criticalErrorHandler));
|
||||||
|
|
||||||
global.willAppQuit = false;
|
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
|
// initialization that can run before the app is ready
|
||||||
|
initializeArgs();
|
||||||
initializeConfig();
|
initializeConfig();
|
||||||
initializeAppEventListeners();
|
initializeAppEventListeners();
|
||||||
initializeBeforeAppReady();
|
initializeBeforeAppReady();
|
||||||
|
@ -115,6 +109,24 @@ try {
|
||||||
// initialization sub functions
|
// 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() {
|
function initializeConfig() {
|
||||||
registryConfig = new RegistryConfig();
|
registryConfig = new RegistryConfig();
|
||||||
config = new Config(app.getPath('userData') + '/config.json');
|
config = new Config(app.getPath('userData') + '/config.json');
|
||||||
|
@ -136,6 +148,8 @@ function initializeAppEventListeners() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeBeforeAppReady() {
|
function initializeBeforeAppReady() {
|
||||||
|
certificateStore = CertificateStore.load(path.resolve(app.getPath('userData'), 'certificate.json'));
|
||||||
|
|
||||||
// can only call this before the app is ready
|
// can only call this before the app is ready
|
||||||
if (config.enableHardwareAcceleration === false) {
|
if (config.enableHardwareAcceleration === false) {
|
||||||
app.disableHardwareAcceleration();
|
app.disableHardwareAcceleration();
|
||||||
|
@ -179,7 +193,7 @@ function initializeInterCommunicationEventListeners() {
|
||||||
if (shouldShowTrayIcon()) {
|
if (shouldShowTrayIcon()) {
|
||||||
ipcMain.on('update-unread', handleUpdateUnreadEvent);
|
ipcMain.on('update-unread', handleUpdateUnreadEvent);
|
||||||
}
|
}
|
||||||
if (config.enableAutoUpdater) {
|
if (!isDev && config.enableAutoUpdater) {
|
||||||
ipcMain.on('check-for-updates', autoUpdater.checkForUpdates);
|
ipcMain.on('check-for-updates', autoUpdater.checkForUpdates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,7 +203,7 @@ function initializeMainWindowListeners() {
|
||||||
mainWindow.on('unresponsive', criticalErrorHandler.windowUnresponsiveHandler.bind(criticalErrorHandler));
|
mainWindow.on('unresponsive', criticalErrorHandler.windowUnresponsiveHandler.bind(criticalErrorHandler));
|
||||||
mainWindow.webContents.on('crashed', handleMainWindowWebContentsCrashed);
|
mainWindow.webContents.on('crashed', handleMainWindowWebContentsCrashed);
|
||||||
mainWindow.on('ready-to-show', handleMainWindowReadyToShow);
|
mainWindow.on('ready-to-show', handleMainWindowReadyToShow);
|
||||||
if (config.enableAutoUpdater) {
|
if (!isDev && config.enableAutoUpdater) {
|
||||||
mainWindow.once('show', handleMainWindowShow);
|
mainWindow.once('show', handleMainWindowShow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -233,13 +247,12 @@ function handleReloadConfig() {
|
||||||
//
|
//
|
||||||
|
|
||||||
// activate first app instance, subsequent instances will quit themselves
|
// activate first app instance, subsequent instances will quit themselves
|
||||||
function handleAppSecondInstance(event, secondArgv) {
|
function handleAppSecondInstance(event, argv) {
|
||||||
// Protocol handler for win32
|
// Protocol handler for win32
|
||||||
// argv: An array of the second instance’s (command line / deep linked) arguments
|
// argv: An array of the second instance’s (command line / deep linked) arguments
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
// Keep only command line / deep linked arguments
|
deeplinkingUrl = getDeeplinkingURL(argv);
|
||||||
if (Array.isArray(secondArgv.slice(1)) && secondArgv.slice(1).length > 0) {
|
if (deeplinkingUrl) {
|
||||||
setDeeplinkingUrl(secondArgv.slice(1)[0]);
|
|
||||||
mainWindow.webContents.send('protocol-deeplink', deeplinkingUrl);
|
mainWindow.webContents.send('protocol-deeplink', deeplinkingUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -337,12 +350,14 @@ function handleAppWillFinishLaunching() {
|
||||||
// Protocol handler for osx
|
// Protocol handler for osx
|
||||||
app.on('open-url', (event, url) => {
|
app.on('open-url', (event, url) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setDeeplinkingUrl(url);
|
deeplinkingUrl = getDeeplinkingURL([url]);
|
||||||
if (app.isReady()) {
|
if (app.isReady()) {
|
||||||
function openDeepLink() {
|
function openDeepLink() {
|
||||||
try {
|
try {
|
||||||
|
if (deeplinkingUrl) {
|
||||||
mainWindow.webContents.send('protocol-deeplink', deeplinkingUrl);
|
mainWindow.webContents.send('protocol-deeplink', deeplinkingUrl);
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setTimeout(openDeepLink, 1000);
|
setTimeout(openDeepLink, 1000);
|
||||||
}
|
}
|
||||||
|
@ -379,6 +394,8 @@ function handleAppWebContentsCreated(dc, contents) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeAfterAppReady() {
|
function initializeAfterAppReady() {
|
||||||
|
app.setAppUserModelId('Mattermost.Desktop'); // Use explicit AppUserModelID
|
||||||
|
|
||||||
const appStateJson = path.join(app.getPath('userData'), 'app-state.json');
|
const appStateJson = path.join(app.getPath('userData'), 'app-state.json');
|
||||||
appState = new AppStateManager(appStateJson);
|
appState = new AppStateManager(appStateJson);
|
||||||
if (wasUpdated(appState.lastAppVersion)) {
|
if (wasUpdated(appState.lastAppVersion)) {
|
||||||
|
@ -398,13 +415,9 @@ function initializeAfterAppReady() {
|
||||||
|
|
||||||
// Protocol handler for win32
|
// Protocol handler for win32
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
// Keep only command line / deep linked argument. Make sure it's not squirrel command
|
const args = process.argv.slice(1);
|
||||||
const tmpArgs = process.argv.slice(1);
|
if (Array.isArray(args) && args.length > 0) {
|
||||||
if (
|
deeplinkingUrl = getDeeplinkingURL(args);
|
||||||
Array.isArray(tmpArgs) && tmpArgs.length > 0 &&
|
|
||||||
tmpArgs[0].match(/^--squirrel-/) === null
|
|
||||||
) {
|
|
||||||
setDeeplinkingUrl(tmpArgs[0]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -502,7 +515,7 @@ function initializeAfterAppReady() {
|
||||||
permissionManager = new PermissionManager(permissionFile, trustedURLs);
|
permissionManager = new PermissionManager(permissionFile, trustedURLs);
|
||||||
session.defaultSession.setPermissionRequestHandler(permissionRequestHandler(mainWindow, permissionManager));
|
session.defaultSession.setPermissionRequestHandler(permissionRequestHandler(mainWindow, permissionManager));
|
||||||
|
|
||||||
if (config.enableAutoUpdater) {
|
if (!isDev && config.enableAutoUpdater) {
|
||||||
const updaterConfig = autoUpdater.loadConfig();
|
const updaterConfig = autoUpdater.loadConfig();
|
||||||
autoUpdater.initialize(appState, mainWindow, updaterConfig.isNotifyOnly());
|
autoUpdater.initialize(appState, mainWindow, updaterConfig.isNotifyOnly());
|
||||||
ipcMain.on('check-for-updates', autoUpdater.checkForUpdates);
|
ipcMain.on('check-for-updates', autoUpdater.checkForUpdates);
|
||||||
|
@ -680,10 +693,13 @@ function handleMainWindowWebContentsCrashed() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMainWindowReadyToShow() {
|
function handleMainWindowReadyToShow() {
|
||||||
|
if (!isDev) {
|
||||||
autoUpdater.checkForUpdates();
|
autoUpdater.checkForUpdates();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleMainWindowShow() {
|
function handleMainWindowShow() {
|
||||||
|
if (!isDev) {
|
||||||
if (autoUpdater.shouldCheckForUpdatesOnStart(appState.updateCheckedDate)) {
|
if (autoUpdater.shouldCheckForUpdatesOnStart(appState.updateCheckedDate)) {
|
||||||
ipcMain.emit('check-for-updates');
|
ipcMain.emit('check-for-updates');
|
||||||
} else {
|
} else {
|
||||||
|
@ -692,6 +708,7 @@ function handleMainWindowShow() {
|
||||||
}, autoUpdater.UPDATER_INTERVAL_IN_MS);
|
}, autoUpdater.UPDATER_INTERVAL_IN_MS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// helper functions
|
// helper functions
|
||||||
|
@ -757,11 +774,16 @@ function switchMenuIconImages(icons, isDarkMode) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDeeplinkingUrl(url) {
|
function getDeeplinkingURL(args) {
|
||||||
if (scheme) {
|
if (Array.isArray(args) && args.length) {
|
||||||
deeplinkingUrl = url.replace(new RegExp('^' + scheme), 'https');
|
// 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() {
|
function shouldShowTrayIcon() {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
|
|
|
@ -3,7 +3,18 @@
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
import JsonFileManager from '../common/JsonFileManager';
|
import JsonFileManager from '../common/JsonFileManager';
|
||||||
|
|
||||||
|
import * as Validator from './Validator';
|
||||||
|
|
||||||
export default class AppStateManager extends JsonFileManager {
|
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) {
|
set lastAppVersion(version) {
|
||||||
this.setValue('lastAppVersion', version);
|
this.setValue('lastAppVersion', version);
|
||||||
}
|
}
|
||||||
|
|
37
src/main/ParseArgs.js
Normal file
37
src/main/ParseArgs.js
Normal 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) || {};
|
||||||
|
}
|
|
@ -5,6 +5,8 @@ import fs from 'fs';
|
||||||
|
|
||||||
import utils from '../utils/util';
|
import utils from '../utils/util';
|
||||||
|
|
||||||
|
import * as Validator from './Validator';
|
||||||
|
|
||||||
const PERMISSION_GRANTED = 'granted';
|
const PERMISSION_GRANTED = 'granted';
|
||||||
const PERMISSION_DENIED = 'denied';
|
const PERMISSION_DENIED = 'denied';
|
||||||
|
|
||||||
|
@ -15,6 +17,10 @@ export default class PermissionManager {
|
||||||
if (fs.existsSync(file)) {
|
if (fs.existsSync(file)) {
|
||||||
try {
|
try {
|
||||||
this.permissions = JSON.parse(fs.readFileSync(this.file, 'utf-8'));
|
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) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
this.permissions = {};
|
this.permissions = {};
|
||||||
|
|
151
src/main/Validator.js
Normal file
151
src/main/Validator.js
Normal 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;
|
||||||
|
}
|
|
@ -8,6 +8,8 @@ import fs from 'fs';
|
||||||
|
|
||||||
import {app, dialog, ipcMain, shell} from 'electron';
|
import {app, dialog, ipcMain, shell} from 'electron';
|
||||||
|
|
||||||
|
import * as Validator from './Validator';
|
||||||
|
|
||||||
const allowedProtocolFile = path.resolve(app.getPath('userData'), 'allowedProtocols.json');
|
const allowedProtocolFile = path.resolve(app.getPath('userData'), 'allowedProtocols.json');
|
||||||
let allowedProtocols = [];
|
let allowedProtocols = [];
|
||||||
|
|
||||||
|
@ -15,6 +17,7 @@ function init(mainWindow) {
|
||||||
fs.readFile(allowedProtocolFile, 'utf-8', (err, data) => {
|
fs.readFile(allowedProtocolFile, 'utf-8', (err, data) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
allowedProtocols = JSON.parse(data);
|
allowedProtocols = JSON.parse(data);
|
||||||
|
allowedProtocols = Validator.validateAllowedProtocols(allowedProtocols) || [];
|
||||||
}
|
}
|
||||||
initDialogEvent(mainWindow);
|
initDialogEvent(mainWindow);
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
|
|
||||||
|
import * as Validator from './Validator';
|
||||||
|
|
||||||
function comparableCertificate(certificate) {
|
function comparableCertificate(certificate) {
|
||||||
return {
|
return {
|
||||||
data: certificate.data.toString(),
|
data: certificate.data.toString(),
|
||||||
|
@ -32,6 +34,10 @@ function CertificateStore(storeFile) {
|
||||||
let storeStr;
|
let storeStr;
|
||||||
try {
|
try {
|
||||||
storeStr = fs.readFileSync(storeFile, 'utf-8');
|
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) {
|
} catch (e) {
|
||||||
storeStr = '{}';
|
storeStr = '{}';
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@ import path from 'path';
|
||||||
|
|
||||||
import {app, BrowserWindow} from 'electron';
|
import {app, BrowserWindow} from 'electron';
|
||||||
|
|
||||||
|
import * as Validator from './Validator';
|
||||||
|
|
||||||
function saveWindowState(file, window) {
|
function saveWindowState(file, window) {
|
||||||
const windowState = window.getBounds();
|
const windowState = window.getBounds();
|
||||||
windowState.maximized = window.isMaximized();
|
windowState.maximized = window.isMaximized();
|
||||||
|
@ -28,6 +30,10 @@ function createMainWindow(config, options) {
|
||||||
let windowOptions;
|
let windowOptions;
|
||||||
try {
|
try {
|
||||||
windowOptions = JSON.parse(fs.readFileSync(boundsInfoPath, 'utf-8'));
|
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) {
|
} catch (e) {
|
||||||
// Follow Electron's defaults, except for window dimensions which targets 1024x768 screen resolution.
|
// Follow Electron's defaults, except for window dimensions which targets 1024x768 screen resolution.
|
||||||
windowOptions = {width: defaultWindowWidth, height: defaultWindowHeight};
|
windowOptions = {width: defaultWindowWidth, height: defaultWindowHeight};
|
||||||
|
|
|
@ -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
63
src/package-lock.json
generated
|
@ -26,6 +26,47 @@
|
||||||
"regenerator-runtime": "^0.12.0"
|
"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": {
|
"ansi-regex": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/cross-unzip/-/cross-unzip-0.0.2.tgz",
|
||||||
"integrity": "sha1-UYO8R6CVWb78+YzEZXlkmZNZNy8="
|
"integrity": "sha1-UYO8R6CVWb78+YzEZXlkmZNZNy8="
|
||||||
},
|
},
|
||||||
|
"damerau-levenshtein": {
|
||||||
|
"version": "git://github.com/cbaatz/damerau-levenshtein.git#8d7440a51ae9dc6912e44385115c7cb1da4e8ebc",
|
||||||
|
"from": "git://github.com/cbaatz/damerau-levenshtein.git"
|
||||||
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||||
|
@ -228,9 +273,9 @@
|
||||||
"integrity": "sha512-iwM3EotA9HTXqMGpQRkR/kT8OZqBbdfHTnlwcxsjSLYqY8svvsq0MuujsWCn3/vtgRmDv/PC/gKUUpoZvi5C1w=="
|
"integrity": "sha512-iwM3EotA9HTXqMGpQRkR/kT8OZqBbdfHTnlwcxsjSLYqY8svvsq0MuujsWCn3/vtgRmDv/PC/gKUUpoZvi5C1w=="
|
||||||
},
|
},
|
||||||
"electron-log": {
|
"electron-log": {
|
||||||
"version": "2.2.15",
|
"version": "2.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.17.tgz",
|
||||||
"integrity": "sha512-lpTQmU9ZjTtTkg2hHEQCvnrkrqLwvhfnMCXPhjSWA5sFXvybGMn13M0xU3CbvVbZuHSFZww6t7HyWGt+Tnye0g=="
|
"integrity": "sha512-v+Af5W5z99ehhaLOfE9eTSXUwjzh2wFlQjz51dvkZ6ZIrET6OB/zAZPvsuwT6tm3t5x+M1r+Ed3U3xtPZYAyuQ=="
|
||||||
},
|
},
|
||||||
"electron-updater": {
|
"electron-updater": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
|
@ -638,14 +683,9 @@
|
||||||
"integrity": "sha512-RGgpBBn4p65nOhZdBHDt84Remz3QvmoQDppKJX0tYS53SbuUJwm7zG1swT5O770zPvN6wZaZlc8ie/jwlZbeHA==",
|
"integrity": "sha512-RGgpBBn4p65nOhZdBHDt84Remz3QvmoQDppKJX0tYS53SbuUJwm7zG1swT5O770zPvN6wZaZlc8ie/jwlZbeHA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"binarysearch": "^0.2.4",
|
"binarysearch": "^0.2.4",
|
||||||
|
"damerau-levenshtein": "git://github.com/cbaatz/damerau-levenshtein.git",
|
||||||
"strip-bom": "^2.0.0",
|
"strip-bom": "^2.0.0",
|
||||||
"unzip-stream": "^0.3.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": {
|
"sort-keys": {
|
||||||
|
@ -755,6 +795,11 @@
|
||||||
"mkdirp": "^0.5.1"
|
"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": {
|
"warning": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz",
|
||||||
|
|
|
@ -9,12 +9,13 @@
|
||||||
"homepage": "https://about.mattermost.com",
|
"homepage": "https://about.mattermost.com",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hapi/joi": "^15.1.0",
|
||||||
"auto-launch": "^5.0.5",
|
"auto-launch": "^5.0.5",
|
||||||
"bootstrap": "^3.3.7",
|
"bootstrap": "^3.3.7",
|
||||||
"electron-context-menu": "^0.10.1",
|
"electron-context-menu": "^0.10.1",
|
||||||
"electron-devtools-installer": "^2.2.4",
|
"electron-devtools-installer": "^2.2.4",
|
||||||
"electron-is-dev": "^1.0.1",
|
"electron-is-dev": "^1.0.1",
|
||||||
"electron-log": "^2.2.15",
|
"electron-log": "^2.2.17",
|
||||||
"electron-updater": "4.0.6",
|
"electron-updater": "4.0.6",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.6.3",
|
"react": "^16.6.3",
|
||||||
|
@ -24,6 +25,7 @@
|
||||||
"semver": "^5.5.0",
|
"semver": "^5.5.0",
|
||||||
"simple-spellchecker": "^0.9.8",
|
"simple-spellchecker": "^0.9.8",
|
||||||
"underscore": "^1.9.1",
|
"underscore": "^1.9.1",
|
||||||
|
"valid-url": "^1.0.9",
|
||||||
"winreg": "^1.2.4",
|
"winreg": "^1.2.4",
|
||||||
"yargs": "^3.32.0"
|
"yargs": "^3.32.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,21 @@
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
|
|
||||||
|
import {isUri, isHttpUri, isHttpsUri} from 'valid-url';
|
||||||
|
|
||||||
function getDomain(inputURL) {
|
function getDomain(inputURL) {
|
||||||
const parsedURL = url.parse(inputURL);
|
const parsedURL = url.parse(inputURL);
|
||||||
return `${parsedURL.protocol}//${parsedURL.host}`;
|
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.
|
// isInternalURL determines if the target url is internal to the application.
|
||||||
// - currentURL is the current url inside the webview
|
// - currentURL is the current url inside the webview
|
||||||
// - basename is the global export from the Mattermost application defining the subpath, if any
|
// - 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 {
|
export default {
|
||||||
getDomain,
|
getDomain,
|
||||||
|
isValidURL,
|
||||||
|
isValidURI,
|
||||||
isInternalURL,
|
isInternalURL,
|
||||||
};
|
};
|
||||||
|
|
|
@ -71,9 +71,29 @@ describe('application', function desc() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show index.html when there is config file', async () => {
|
it('should show index.html when there is config file', async () => {
|
||||||
fs.writeFileSync(env.configFilePath, JSON.stringify({
|
const config = {
|
||||||
|
version: 1,
|
||||||
|
teams: [{
|
||||||
|
name: 'example',
|
||||||
url: env.mattermostURL,
|
url: env.mattermostURL,
|
||||||
}));
|
}, {
|
||||||
|
name: 'github',
|
||||||
|
url: 'https://github.com/',
|
||||||
|
}],
|
||||||
|
showTrayIcon: false,
|
||||||
|
trayIconTheme: 'light',
|
||||||
|
minimizeToTray: false,
|
||||||
|
notifications: {
|
||||||
|
flashWindow: 0,
|
||||||
|
bounceIcon: false,
|
||||||
|
bounceIconType: 'informational',
|
||||||
|
},
|
||||||
|
showUnreadBadge: true,
|
||||||
|
useSpellChecker: true,
|
||||||
|
enableHardwareAcceleration: true,
|
||||||
|
autostart: true,
|
||||||
|
};
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||||
await this.app.restart();
|
await this.app.restart();
|
||||||
|
|
||||||
const url = await this.app.client.getUrl();
|
const url = await this.app.client.getUrl();
|
||||||
|
@ -82,18 +102,19 @@ describe('application', function desc() {
|
||||||
|
|
||||||
it('should upgrade v0 config file', async () => {
|
it('should upgrade v0 config file', async () => {
|
||||||
const Config = require('../../src/common/config').default;
|
const Config = require('../../src/common/config').default;
|
||||||
const config = new Config(env.configFilePath);
|
const newConfig = new Config(env.configFilePath);
|
||||||
fs.writeFileSync(env.configFilePath, JSON.stringify({
|
const oldConfig = {
|
||||||
url: env.mattermostURL,
|
url: env.mattermostURL,
|
||||||
}));
|
};
|
||||||
|
fs.writeFileSync(env.configFilePath, JSON.stringify(oldConfig));
|
||||||
await this.app.restart();
|
await this.app.restart();
|
||||||
|
|
||||||
const url = await this.app.client.getUrl();
|
const url = await this.app.client.getUrl();
|
||||||
url.should.match(/\/index.html$/);
|
url.should.match(/\/index.html$/);
|
||||||
|
|
||||||
const str = fs.readFileSync(env.configFilePath, 'utf8');
|
const str = fs.readFileSync(env.configFilePath, 'utf8');
|
||||||
const localConfigData = JSON.parse(str);
|
const upgradedConfig = JSON.parse(str);
|
||||||
localConfigData.version.should.equal(config.defaultData.version);
|
upgradedConfig.version.should.equal(newConfig.defaultData.version);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('should be stopped when the app instance already exists', (done) => {
|
it.skip('should be stopped when the app instance already exists', (done) => {
|
||||||
|
|
|
@ -22,6 +22,18 @@ describe('browser/index.html', function desc() {
|
||||||
name: 'github',
|
name: 'github',
|
||||||
url: 'https://github.com/',
|
url: 'https://github.com/',
|
||||||
}],
|
}],
|
||||||
|
showTrayIcon: false,
|
||||||
|
trayIconTheme: 'light',
|
||||||
|
minimizeToTray: false,
|
||||||
|
notifications: {
|
||||||
|
flashWindow: 0,
|
||||||
|
bounceIcon: false,
|
||||||
|
bounceIconType: 'informational',
|
||||||
|
},
|
||||||
|
showUnreadBadge: true,
|
||||||
|
useSpellChecker: true,
|
||||||
|
enableHardwareAcceleration: true,
|
||||||
|
autostart: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const serverPort = 8181;
|
const serverPort = 8181;
|
||||||
|
@ -91,7 +103,8 @@ describe('browser/index.html', function desc() {
|
||||||
waitForVisible('#mattermostView0', 2000, true);
|
waitForVisible('#mattermostView0', 2000, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show error when using incorrect URL', async () => {
|
// validation now prevents incorrect url's from being used
|
||||||
|
it.skip('should show error when using incorrect URL', async () => {
|
||||||
this.timeout(30000);
|
this.timeout(30000);
|
||||||
fs.writeFileSync(env.configFilePath, JSON.stringify({
|
fs.writeFileSync(env.configFilePath, JSON.stringify({
|
||||||
version: 1,
|
version: 1,
|
||||||
|
|
|
@ -20,6 +20,18 @@ describe('browser/settings.html', function desc() {
|
||||||
name: 'github',
|
name: 'github',
|
||||||
url: 'https://github.com/',
|
url: 'https://github.com/',
|
||||||
}],
|
}],
|
||||||
|
showTrayIcon: false,
|
||||||
|
trayIconTheme: 'light',
|
||||||
|
minimizeToTray: false,
|
||||||
|
notifications: {
|
||||||
|
flashWindow: 0,
|
||||||
|
bounceIcon: false,
|
||||||
|
bounceIconType: 'informational',
|
||||||
|
},
|
||||||
|
showUnreadBadge: true,
|
||||||
|
useSpellChecker: true,
|
||||||
|
enableHardwareAcceleration: true,
|
||||||
|
autostart: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|
|
@ -9,6 +9,15 @@ import PermissionManager from '../../src/main/PermissionManager';
|
||||||
|
|
||||||
const permissionFile = path.join(env.userDataDir, 'permission.json');
|
const permissionFile = path.join(env.userDataDir, 'permission.json');
|
||||||
|
|
||||||
|
const ORIGIN1 = 'https://example.com';
|
||||||
|
const PERMISSION1 = 'notifications';
|
||||||
|
|
||||||
|
const ORIGIN2 = 'https://example2.com';
|
||||||
|
const PERMISSION2 = 'test';
|
||||||
|
|
||||||
|
const DENIED = 'denied';
|
||||||
|
const GRANTED = 'granted';
|
||||||
|
|
||||||
describe('PermissionManager', function() {
|
describe('PermissionManager', function() {
|
||||||
beforeEach(function(done) {
|
beforeEach(function(done) {
|
||||||
fs.unlink(permissionFile, () => {
|
fs.unlink(permissionFile, () => {
|
||||||
|
@ -17,73 +26,67 @@ describe('PermissionManager', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should grant a permission for an origin', function() {
|
it('should grant a permission for an origin', function() {
|
||||||
const ORIGIN = 'origin';
|
|
||||||
const PERMISSION = 'permission';
|
|
||||||
const manager = new PermissionManager(permissionFile);
|
const manager = new PermissionManager(permissionFile);
|
||||||
|
|
||||||
manager.isGranted(ORIGIN, PERMISSION).should.be.false;
|
manager.isGranted(ORIGIN1, PERMISSION1).should.be.false;
|
||||||
manager.isDenied(ORIGIN, PERMISSION).should.be.false;
|
manager.isDenied(ORIGIN1, PERMISSION1).should.be.false;
|
||||||
|
|
||||||
manager.grant(ORIGIN, PERMISSION);
|
manager.grant(ORIGIN1, PERMISSION1);
|
||||||
|
|
||||||
manager.isGranted(ORIGIN, PERMISSION).should.be.true;
|
manager.isGranted(ORIGIN1, PERMISSION1).should.be.true;
|
||||||
manager.isDenied(ORIGIN, PERMISSION).should.be.false;
|
manager.isDenied(ORIGIN1, PERMISSION1).should.be.false;
|
||||||
|
|
||||||
manager.isGranted(ORIGIN + '_another', PERMISSION).should.be.false;
|
manager.isGranted(ORIGIN2, PERMISSION1).should.be.false;
|
||||||
manager.isGranted(ORIGIN, PERMISSION + '_another').should.be.false;
|
manager.isGranted(ORIGIN1, PERMISSION2).should.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should deny a permission for an origin', function() {
|
it('should deny a permission for an origin', function() {
|
||||||
const ORIGIN = 'origin';
|
|
||||||
const PERMISSION = 'permission';
|
|
||||||
const manager = new PermissionManager(permissionFile);
|
const manager = new PermissionManager(permissionFile);
|
||||||
|
|
||||||
manager.isGranted(ORIGIN, PERMISSION).should.be.false;
|
manager.isGranted(ORIGIN1, PERMISSION1).should.be.false;
|
||||||
manager.isDenied(ORIGIN, PERMISSION).should.be.false;
|
manager.isDenied(ORIGIN1, PERMISSION1).should.be.false;
|
||||||
|
|
||||||
manager.deny(ORIGIN, PERMISSION);
|
manager.deny(ORIGIN1, PERMISSION1);
|
||||||
|
|
||||||
manager.isGranted(ORIGIN, PERMISSION).should.be.false;
|
manager.isGranted(ORIGIN1, PERMISSION1).should.be.false;
|
||||||
manager.isDenied(ORIGIN, PERMISSION).should.be.true;
|
manager.isDenied(ORIGIN1, PERMISSION1).should.be.true;
|
||||||
|
|
||||||
manager.isDenied(ORIGIN + '_another', PERMISSION).should.be.false;
|
manager.isDenied(ORIGIN2, PERMISSION1).should.be.false;
|
||||||
manager.isDenied(ORIGIN, PERMISSION + '_another').should.be.false;
|
manager.isDenied(ORIGIN1, PERMISSION2).should.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should save permissions to the file', function() {
|
it('should save permissions to the file', function() {
|
||||||
const ORIGIN = 'origin';
|
|
||||||
const PERMISSION = 'permission';
|
|
||||||
const manager = new PermissionManager(permissionFile);
|
const manager = new PermissionManager(permissionFile);
|
||||||
manager.deny(ORIGIN, PERMISSION);
|
manager.deny(ORIGIN1, PERMISSION1);
|
||||||
manager.grant(ORIGIN + '_another', PERMISSION + '_another');
|
manager.grant(ORIGIN2, PERMISSION2);
|
||||||
JSON.parse(fs.readFileSync(permissionFile)).should.deep.equal({
|
JSON.parse(fs.readFileSync(permissionFile)).should.deep.equal({
|
||||||
origin: {
|
[ORIGIN1]: {
|
||||||
permission: 'denied',
|
[PERMISSION1]: DENIED,
|
||||||
},
|
},
|
||||||
origin_another: {
|
[ORIGIN2]: {
|
||||||
permission_another: 'granted',
|
[PERMISSION2]: GRANTED,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should restore permissions from the file', function() {
|
it('should restore permissions from the file', function() {
|
||||||
fs.writeFileSync(permissionFile, JSON.stringify({
|
fs.writeFileSync(permissionFile, JSON.stringify({
|
||||||
origin: {
|
[ORIGIN1]: {
|
||||||
permission: 'denied',
|
[PERMISSION1]: DENIED,
|
||||||
},
|
},
|
||||||
origin_another: {
|
[ORIGIN2]: {
|
||||||
permission_another: 'granted',
|
[PERMISSION2]: GRANTED,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
const manager = new PermissionManager(permissionFile);
|
const manager = new PermissionManager(permissionFile);
|
||||||
manager.isDenied('origin', 'permission').should.be.true;
|
manager.isDenied(ORIGIN1, PERMISSION1).should.be.true;
|
||||||
manager.isGranted('origin_another', 'permission_another').should.be.true;
|
manager.isGranted(ORIGIN2, PERMISSION2).should.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow permissions for trusted URLs', function() {
|
it('should allow permissions for trusted URLs', function() {
|
||||||
fs.writeFileSync(permissionFile, JSON.stringify({}));
|
fs.writeFileSync(permissionFile, JSON.stringify({}));
|
||||||
const manager = new PermissionManager(permissionFile, ['https://example.com', 'https://example2.com/2']);
|
const manager = new PermissionManager(permissionFile, [ORIGIN1, ORIGIN2]);
|
||||||
manager.isGranted('https://example.com', 'notifications').should.be.true;
|
manager.isGranted(ORIGIN1, PERMISSION1).should.be.true;
|
||||||
manager.isGranted('https://example2.com', 'test').should.be.true;
|
manager.isGranted(ORIGIN2, PERMISSION2).should.be.true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,7 +9,7 @@ const http = require('http');
|
||||||
|
|
||||||
const env = require('../modules/environment');
|
const env = require('../modules/environment');
|
||||||
|
|
||||||
describe.skip('application', function desc() {
|
describe.skip('security', function desc() {
|
||||||
this.timeout(30000);
|
this.timeout(30000);
|
||||||
|
|
||||||
const serverPort = 8181;
|
const serverPort = 8181;
|
||||||
|
|
|
@ -8,6 +8,54 @@ import assert from 'assert';
|
||||||
import Utils from '../../../src/utils/util';
|
import Utils from '../../../src/utils/util';
|
||||||
|
|
||||||
describe('Utils', () => {
|
describe('Utils', () => {
|
||||||
|
describe('isValidURL', () => {
|
||||||
|
it('should be true for a valid web url', () => {
|
||||||
|
const testURL = 'https://developers.mattermost.com/';
|
||||||
|
assert.equal(Utils.isValidURL(testURL), true);
|
||||||
|
});
|
||||||
|
it('should be true for a valid, non-https web url', () => {
|
||||||
|
const testURL = 'http://developers.mattermost.com/';
|
||||||
|
assert.equal(Utils.isValidURL(testURL), true);
|
||||||
|
});
|
||||||
|
it('should be true for an invalid, self-defined, top-level domain', () => {
|
||||||
|
const testURL = 'https://www.example.x';
|
||||||
|
assert.equal(Utils.isValidURL(testURL), true);
|
||||||
|
});
|
||||||
|
it('should be true for a file download url', () => {
|
||||||
|
const testURL = 'https://community.mattermost.com/api/v4/files/ka3xbfmb3ffnmgdmww8otkidfw?download=1';
|
||||||
|
assert.equal(Utils.isValidURL(testURL), true);
|
||||||
|
});
|
||||||
|
it('should be true for a permalink url', () => {
|
||||||
|
const testURL = 'https://community.mattermost.com/test-channel/pl/pdqowkij47rmbyk78m5hwc7r6r';
|
||||||
|
assert.equal(Utils.isValidURL(testURL), true);
|
||||||
|
});
|
||||||
|
it('should be true for a valid, internal domain', () => {
|
||||||
|
const testURL = 'https://mattermost.company-internal';
|
||||||
|
assert.equal(Utils.isValidURL(testURL), true);
|
||||||
|
});
|
||||||
|
it('should be true for a second, valid internal domain', () => {
|
||||||
|
const testURL = 'https://serverXY/mattermost';
|
||||||
|
assert.equal(Utils.isValidURL(testURL), true);
|
||||||
|
});
|
||||||
|
it('should be true for a valid, non-https internal domain', () => {
|
||||||
|
const testURL = 'http://mattermost.local';
|
||||||
|
assert.equal(Utils.isValidURL(testURL), true);
|
||||||
|
});
|
||||||
|
it('should be true for a valid, non-https, ip address with port number', () => {
|
||||||
|
const testURL = 'http://localhost:8065';
|
||||||
|
assert.equal(Utils.isValidURL(testURL), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('isValidURI', () => {
|
||||||
|
it('should be true for a deeplink url', () => {
|
||||||
|
const testURL = 'mattermost://community-release.mattermost.com/core/channels/developers';
|
||||||
|
assert.equal(Utils.isValidURI(testURL), true);
|
||||||
|
});
|
||||||
|
it('should be false for a malicious url', () => {
|
||||||
|
const testURL = String.raw`mattermost:///" --data-dir "\\deans-mbp\mattermost`;
|
||||||
|
assert.equal(Utils.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 = url.parse('http://localhost/team/channel1');
|
||||||
|
|
Loading…
Reference in a new issue