From 761ef8d0e640e163b61f353dfb34da06c77d70e1 Mon Sep 17 00:00:00 2001 From: Dean Whillier Date: Mon, 23 Sep 2019 14:59:12 -0400 Subject: [PATCH] [MM-18152] Desktop notifications (#1040) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * temp * add in html5 notification tests * strip out custom permissions handling * disable middle click * validate as URI instead of URL allow’s custom protocol’s to pass through * add context isolation to new window requests * add new permissions handling * prevent setting user to away from quit/shutdown * dispatch desktop notifications from renderer * remove test code * log desktop notification errors * should deny as a last resort * only trigger callback once --- electron-builder.json | 2 +- src/browser/components/MainPage.jsx | 15 --- src/browser/components/MattermostView.jsx | 41 +++++++-- .../components/PermissionRequestDialog.jsx | 90 ------------------ src/browser/components/TabBar.jsx | 28 +----- src/browser/index.jsx | 54 +---------- src/browser/webview/mattermost.js | 49 +++++++++- src/main.js | 47 +++++++--- src/main/PermissionManager.js | 81 ---------------- src/main/UserActivityMonitor.js | 2 - src/main/Validator.js | 15 --- src/main/mainWindow.js | 1 + src/main/permissionRequestHandler.js | 67 -------------- test/specs/permisson_test.js | 92 ------------------- 14 files changed, 116 insertions(+), 468 deletions(-) delete mode 100644 src/browser/components/PermissionRequestDialog.jsx delete mode 100644 src/main/PermissionManager.js delete mode 100644 src/main/permissionRequestHandler.js delete mode 100644 test/specs/permisson_test.js diff --git a/electron-builder.json b/electron-builder.json index 8a98a5a8..f2e287bd 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -3,7 +3,7 @@ "provider": "generic", "url": "https://releases.mattermost.com/desktop/" }], - "appId": "com.mattermost.desktop", + "appId": "Mattermost.Desktop", "artifactName": "${name}-${version}-${os}-${arch}.${ext}", "directories": { "buildResources": "resources", diff --git a/src/browser/components/MainPage.jsx b/src/browser/components/MainPage.jsx index dd1815bb..c7ac52bf 100644 --- a/src/browser/components/MainPage.jsx +++ b/src/browser/components/MainPage.jsx @@ -18,7 +18,6 @@ import LoginModal from './LoginModal.jsx'; import MattermostView from './MattermostView.jsx'; import TabBar from './TabBar.jsx'; import HoveringURL from './HoveringURL.jsx'; -import PermissionRequestDialog from './PermissionRequestDialog.jsx'; import Finder from './Finder.jsx'; import NewTeamModal from './NewTeamModal.jsx'; @@ -333,8 +332,6 @@ export default class MainPage extends React.Component { onSelect={this.handleSelect} onAddServer={this.addServer} showAddServerButton={this.props.showAddServerButton} - requestingPermission={this.props.requestingPermission} - onClickPermissionDialog={this.props.onClickPermissionDialog} /> ); @@ -421,16 +418,6 @@ export default class MainPage extends React.Component { onLogin={this.handleLogin} onCancel={this.handleLoginCancel} /> - {this.props.teams.length === 1 && this.props.requestingPermission[0] ? // eslint-disable-line multiline-ternary - : null - } { tabsRow } { viewsRow } @@ -474,8 +461,6 @@ MainPage.propTypes = { onSelectSpellCheckerLocale: PropTypes.func.isRequired, deeplinkingUrl: PropTypes.string, showAddServerButton: PropTypes.bool.isRequired, - requestingPermission: TabBar.propTypes.requestingPermission, - onClickPermissionDialog: PropTypes.func, }; /* eslint-enable react/no-set-state */ diff --git a/src/browser/components/MattermostView.jsx b/src/browser/components/MattermostView.jsx index 931fab90..6325402c 100644 --- a/src/browser/components/MattermostView.jsx +++ b/src/browser/components/MattermostView.jsx @@ -10,6 +10,7 @@ import url from 'url'; import React from 'react'; import PropTypes from 'prop-types'; import {ipcRenderer, remote, shell} from 'electron'; +import log from 'electron-log'; import contextMenu from '../js/contextMenu'; import Utils from '../../utils/util'; @@ -23,6 +24,8 @@ const preloadJS = `file://${remote.app.getAppPath()}/browser/webview/mattermost_ const ERR_NOT_IMPLEMENTED = -11; const U2F_EXTENSION_URL = 'chrome-extension://kmendfapggjehodndflmmgagdbamhnfd/u2f-comms.html'; +const appIconURL = `file:///${remote.app.getAppPath()}/assets/appicon.png`; + export default class MattermostView extends React.Component { constructor(props) { super(props); @@ -36,6 +39,7 @@ export default class MattermostView extends React.Component { }; this.handleUnreadCountChange = this.handleUnreadCountChange.bind(this); + this.dispatchNotification = this.dispatchNotification.bind(this); this.reload = this.reload.bind(this); this.clearCacheAndReload = this.clearCacheAndReload.bind(this); this.focusOnWebView = this.focusOnWebView.bind(this); @@ -56,6 +60,27 @@ export default class MattermostView extends React.Component { } } + async dispatchNotification(title, body, channel, teamId, silent) { + const permission = await Notification.requestPermission(); + if (permission !== 'granted') { + log.error('Notifications not granted'); + return; + } + const notification = new Notification(title, { + body, + tag: body, + icon: appIconURL, + requireInteraction: false, + silent, + }); + notification.onclick = () => { + this.webviewRef.current.send('notification-clicked', {channel, teamId}); + }; + notification.onerror = () => { + log.error('Notification failed to show'); + }; + } + componentDidMount() { const self = this; const webview = this.webviewRef.current; @@ -90,7 +115,7 @@ 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)) { + if (!Utils.isValidURI(e.url)) { return; } const currentURL = url.parse(webview.getURL()); @@ -108,7 +133,7 @@ export default class MattermostView extends React.Component { shell.openExternal(e.url); } else { // New window should disable nodeIntegration. - window.open(e.url, remote.app.getName(), 'nodeIntegration=no, show=yes'); + window.open(e.url, remote.app.getName(), 'nodeIntegration=no, contextIsolation=yes, show=yes'); } } else { // if the link is external, use default browser. @@ -153,13 +178,11 @@ export default class MattermostView extends React.Component { }); break; case 'onBadgeChange': { - const sessionExpired = event.args[0]; - const unreadCount = event.args[1]; - const mentionCount = event.args[2]; - const isUnread = event.args[3]; - const isMentioned = event.args[4]; - self.handleUnreadCountChange(sessionExpired, unreadCount, mentionCount, isUnread, isMentioned); - + self.handleUnreadCountChange(...event.args); + break; + } + case 'dispatchNotification': { + self.dispatchNotification(...event.args); break; } case 'onNotificationClick': diff --git a/src/browser/components/PermissionRequestDialog.jsx b/src/browser/components/PermissionRequestDialog.jsx deleted file mode 100644 index 1ca600ab..00000000 --- a/src/browser/components/PermissionRequestDialog.jsx +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2015-2016 Yuya Ochiai -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import React from 'react'; -import PropTypes from 'prop-types'; -import {Button, Glyphicon, Popover} from 'react-bootstrap'; - -const PERMISSIONS = { - media: { - description: 'Use your camera and microphone', - glyph: 'facetime-video', - }, - geolocation: { - description: 'Know your location', - glyph: 'map-marker', - }, - notifications: { - description: 'Show notifications', - glyph: 'bell', - }, - midiSysex: { - description: 'Use your MIDI devices', - glyph: 'music', - }, - pointerLock: { - description: 'Lock your mouse cursor', - glyph: 'hand-up', - }, - fullscreen: { - description: 'Enter full screen', - glyph: 'resize-full', - }, - openExternal: { - description: 'Open external', - glyph: 'new-window', - }, -}; - -function glyph(permission) { - const data = PERMISSIONS[permission]; - if (data) { - return data.glyph; - } - return 'alert'; -} - -function description(permission) { - const data = PERMISSIONS[permission]; - if (data) { - return data.description; - } - return `Be granted "${permission}" permission`; -} - -export default function PermissionRequestDialog(props) { - const {origin, permission, onClickAllow, onClickBlock, onClickClose, ...reft} = props; - return ( - -
-

{`${origin} wants to:`}

-

- - {description(permission)} -

-

- - -

- -
-
- ); -} - -PermissionRequestDialog.propTypes = { - origin: PropTypes.string, - permission: PropTypes.oneOf(['media', 'geolocation', 'notifications', 'midiSysex', 'pointerLock', 'fullscreen', 'openExternal']), - onClickAllow: PropTypes.func, - onClickBlock: PropTypes.func, - onClickClose: PropTypes.func, -}; diff --git a/src/browser/components/TabBar.jsx b/src/browser/components/TabBar.jsx index 70ec1f9d..b378fb0b 100644 --- a/src/browser/components/TabBar.jsx +++ b/src/browser/components/TabBar.jsx @@ -3,9 +3,7 @@ // See LICENSE.txt for license information. import React from 'react'; import PropTypes from 'prop-types'; -import {Glyphicon, Nav, NavItem, Overlay} from 'react-bootstrap'; - -import PermissionRequestDialog from './PermissionRequestDialog.jsx'; +import {Glyphicon, Nav, NavItem} from 'react-bootstrap'; export default class TabBar extends React.Component { // need "this" render() { @@ -41,24 +39,6 @@ export default class TabBar extends React.Component { // need "this" ); } const id = 'teamTabItem' + index; - const requestingPermission = this.props.requestingPermission[index]; - const permissionOverlay = ( - this.refs[id]} - > - - - ); // draggable=false is a workaround for https://github.com/mattermost/desktop/issues/667 // It would obstruct https://github.com/mattermost/desktop/issues/478 @@ -79,7 +59,6 @@ export default class TabBar extends React.Component { // need "this" { ' ' } { badgeDiv } - {permissionOverlay} ); }); if (this.props.showAddServerButton === true) { @@ -127,10 +106,5 @@ TabBar.propTypes = { mentionCounts: PropTypes.array, mentionAtActiveCounts: PropTypes.array, showAddServerButton: PropTypes.bool, - requestingPermission: PropTypes.arrayOf(PropTypes.shape({ - origin: PropTypes.string, - permission: PropTypes.string, - })), onAddServer: PropTypes.func, - onClickPermissionDialog: PropTypes.func, }; diff --git a/src/browser/index.jsx b/src/browser/index.jsx index f69898b0..bd628942 100644 --- a/src/browser/index.jsx +++ b/src/browser/index.jsx @@ -14,13 +14,14 @@ import React from 'react'; import ReactDOM from 'react-dom'; import {remote, ipcRenderer} from 'electron'; -import utils from '../utils/util'; - import Config from '../common/config'; +import EnhancedNotification from './js/notification'; import MainPage from './components/MainPage.jsx'; import {createDataURL as createBadgeDataURL} from './js/badge'; +Notification = EnhancedNotification; // eslint-disable-line no-global-assign, no-native-reassign + const config = new Config(remote.app.getPath('userData') + '/config.json', remote.getCurrentWindow().registryConfigData); const teams = config.teams; @@ -31,9 +32,6 @@ if (teams.length === 0) { remote.getCurrentWindow().loadFile('browser/settings.html'); } -const permissionRequestQueue = []; -const requestingPermission = new Array(teams.length); - const parsedURL = url.parse(window.location.href, true); const initialIndex = parsedURL.query.index ? parseInt(parsedURL.query.index, 10) : 0; @@ -44,7 +42,6 @@ if (!parsedURL.query.index || parsedURL.query.index === null) { config.on('update', (configData) => { teams.splice(0, teams.length, ...configData.teams); - requestingPermission.length = teams.length; }); config.on('synchronize', () => { @@ -134,53 +131,10 @@ function teamConfigChange(updatedTeams) { config.set('teams', updatedTeams); } -function feedPermissionRequest() { - const webviews = document.getElementsByTagName('webview'); - const webviewOrigins = Array.from(webviews).map((w) => utils.getDomain(w.getAttribute('src'))); - for (let index = 0; index < requestingPermission.length; index++) { - if (requestingPermission[index]) { - break; - } - for (const request of permissionRequestQueue) { - if (request.origin === webviewOrigins[index]) { - requestingPermission[index] = request; - break; - } - } - } -} - -function handleClickPermissionDialog(index, status) { - const requesting = requestingPermission[index]; - ipcRenderer.send('update-permission', requesting.origin, requesting.permission, status); - if (status === 'allow' || status === 'block') { - const newRequests = permissionRequestQueue.filter((request) => { - if (request.permission === requesting.permission && request.origin === requesting.origin) { - return false; - } - return true; - }); - permissionRequestQueue.splice(0, permissionRequestQueue.length, ...newRequests); - } else if (status === 'close') { - const i = permissionRequestQueue.findIndex((e) => e.permission === requesting.permission && e.origin === requesting.origin); - permissionRequestQueue.splice(i, 1); - } - requestingPermission[index] = null; - feedPermissionRequest(); -} - function handleSelectSpellCheckerLocale(locale) { config.set('spellCheckerLocale', locale); } -ipcRenderer.on('request-permission', (event, origin, permission) => { - if (permissionRequestQueue.length >= 100) { - return; - } - permissionRequestQueue.push({origin, permission}); - feedPermissionRequest(); -}); - ReactDOM.render( , document.getElementById('content') ); diff --git a/src/browser/webview/mattermost.js b/src/browser/webview/mattermost.js index d890d160..51e011f2 100644 --- a/src/browser/webview/mattermost.js +++ b/src/browser/webview/mattermost.js @@ -3,16 +3,13 @@ // See LICENSE.txt for license information. 'use strict'; -import {ipcRenderer, webFrame} from 'electron'; +/* eslint-disable no-magic-numbers */ -import EnhancedNotification from '../js/notification'; +import {ipcRenderer, webFrame, remote} from 'electron'; const UNREAD_COUNT_INTERVAL = 1000; -//eslint-disable-next-line no-magic-numbers const CLEAR_CACHE_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours -Notification = EnhancedNotification; // eslint-disable-line no-global-assign, no-native-reassign - Reflect.deleteProperty(global.Buffer); // http://electron.atom.io/docs/tutorial/security/#buffer-global function isReactAppInitialized() { @@ -50,6 +47,46 @@ window.addEventListener('load', () => { }); }); +// listen for messages from the webapp +window.addEventListener('message', ({origin, data: {type, message = {}} = {}} = {}) => { + if (origin !== window.location.origin) { + return; + } + switch (type) { + case 'webapp-ready': { + // register with the webapp to enable custom integration functionality + window.postMessage( + { + type: 'register-desktop', + message: { + version: remote.app.getVersion(), + }, + }, + window.location.origin + ); + break; + } + case 'dispatch-notification': { + const {title, body, channel, teamId, silent} = message; + ipcRenderer.sendToHost('dispatchNotification', title, body, channel, teamId, silent); + break; + } + } +}); + +ipcRenderer.on('notification-clicked', (event, {channel, teamId}) => { + window.postMessage( + { + type: 'notification-clicked', + message: { + channel, + teamId, + }, + }, + window.location.origin + ); +}); + function hasClass(element, className) { const rclass = /[\t\r\n\f]/g; if ((' ' + element.className + ' ').replace(rclass, ' ').indexOf(className) > -1) { @@ -209,3 +246,5 @@ ipcRenderer.on('user-activity-update', (event, {userIsActive, isSystemEvent}) => setInterval(() => { webFrame.clearCache(); }, CLEAR_CACHE_INTERVAL); + +/* eslint-enable no-magic-numbers */ diff --git a/src/main.js b/src/main.js index f1de490d..2f3c5537 100644 --- a/src/main.js +++ b/src/main.js @@ -25,8 +25,6 @@ import appMenu from './main/menus/app'; import trayMenu from './main/menus/tray'; import downloadURL from './main/downloadURL'; import allowProtocolDialog from './main/allowProtocolDialog'; -import PermissionManager from './main/PermissionManager'; -import permissionRequestHandler from './main/permissionRequestHandler'; import AppStateManager from './main/AppStateManager'; import initCookieManager from './main/cookieManager'; import {shouldBeHiddenOnStartup} from './main/utils'; @@ -61,7 +59,6 @@ let spellChecker = null; let deeplinkingUrl = null; let scheme = null; let appState = null; -let permissionManager = null; let registryConfig = null; let config = null; let trayIcon = null; @@ -233,12 +230,6 @@ function handleConfigUpdate(configData) { }); } - if (permissionManager) { - const trustedURLs = config.teams.map((team) => team.url); - permissionManager.setTrustedURLs(trustedURLs); - ipcMain.emit('update-dict', true, config.spellCheckerLocale); - } - ipcMain.emit('update-menu', true, configData); } @@ -571,10 +562,40 @@ function initializeAfterAppReady() { ipcMain.emit('update-dict'); - const permissionFile = path.join(app.getPath('userData'), 'permission.json'); - const trustedURLs = config.teams.map((team) => team.url); - permissionManager = new PermissionManager(permissionFile, trustedURLs); - session.defaultSession.setPermissionRequestHandler(permissionRequestHandler(mainWindow, permissionManager)); + // supported permission types + const supportedPermissionTypes = [ + 'media', + 'geolocation', + 'notifications', + 'fullscreen', + 'openExternal', + ]; + + // handle permission requests + // - approve if a supported permission type and the request comes from the renderer or one of the defined servers + session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => { + // is the requested permission type supported? + if (!supportedPermissionTypes.includes(permission)) { + callback(false); + return; + } + + // is the request coming from the renderer? + if (webContents.id === mainWindow.webContents.id) { + callback(true); + return; + } + + // get the requesting webContents url + const requestingURL = webContents.getURL(); + + // is the target url trusted? + const matchingTeamIndex = config.teams.findIndex((team) => { + return requestingURL.startsWith(team.url); + }); + + callback(matchingTeamIndex >= 0); + }); } // diff --git a/src/main/PermissionManager.js b/src/main/PermissionManager.js deleted file mode 100644 index 195834ac..00000000 --- a/src/main/PermissionManager.js +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2015-2016 Yuya Ochiai -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import fs from 'fs'; - -import utils from '../utils/util'; - -import * as Validator from './Validator'; - -const PERMISSION_GRANTED = 'granted'; -const PERMISSION_DENIED = 'denied'; - -export default class PermissionManager { - constructor(file, trustedURLs = []) { - this.file = file; - this.setTrustedURLs(trustedURLs); - 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 = {}; - } - } else { - this.permissions = {}; - } - } - - writeFileSync() { - fs.writeFileSync(this.file, JSON.stringify(this.permissions, null, ' ')); - } - - grant(origin, permission) { - if (!this.permissions[origin]) { - this.permissions[origin] = {}; - } - this.permissions[origin][permission] = PERMISSION_GRANTED; - this.writeFileSync(); - } - - deny(origin, permission) { - if (!this.permissions[origin]) { - this.permissions[origin] = {}; - } - this.permissions[origin][permission] = PERMISSION_DENIED; - this.writeFileSync(); - } - - clear(origin, permission) { - delete this.permissions[origin][permission]; - } - - isGranted(origin, permission) { - if (this.trustedOrigins[origin] === true) { - return true; - } - if (this.permissions[origin]) { - return this.permissions[origin][permission] === PERMISSION_GRANTED; - } - return false; - } - - isDenied(origin, permission) { - if (this.permissions[origin]) { - return this.permissions[origin][permission] === PERMISSION_DENIED; - } - return false; - } - - setTrustedURLs(trustedURLs) { - this.trustedOrigins = {}; - for (const url of trustedURLs) { - const origin = utils.getDomain(url); - this.trustedOrigins[origin] = true; - } - } -} diff --git a/src/main/UserActivityMonitor.js b/src/main/UserActivityMonitor.js index acfe4241..669383ee 100644 --- a/src/main/UserActivityMonitor.js +++ b/src/main/UserActivityMonitor.js @@ -66,7 +66,6 @@ export default class UserActivityMonitor extends EventEmitter { // NOTE: electron.powerMonitor cannot be referenced until the app is ready electron.powerMonitor.on('suspend', this.handleSystemGoingAway); electron.powerMonitor.on('resume', this.handleSystemComingBack); - electron.powerMonitor.on('shutdown', this.handleSystemGoingAway); electron.powerMonitor.on('lock-screen', this.handleSystemGoingAway); electron.powerMonitor.on('unlock-screen', this.handleSystemComingBack); @@ -87,7 +86,6 @@ export default class UserActivityMonitor extends EventEmitter { stopMonitoring() { electron.powerMonitor.off('suspend', this.handleSystemGoingAway); electron.powerMonitor.off('resume', this.handleSystemComingBack); - electron.powerMonitor.off('shutdown', this.handleSystemGoingAway); electron.powerMonitor.off('lock-screen', this.handleSystemGoingAway); electron.powerMonitor.off('unlock-screen', this.handleSystemComingBack); diff --git a/src/main/Validator.js b/src/main/Validator.js index 3895a5cb..01abe01f 100644 --- a/src/main/Validator.js +++ b/src/main/Validator.js @@ -62,16 +62,6 @@ const configDataSchemaV1 = Joi.object({ 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(), @@ -124,11 +114,6 @@ export function validateV1ConfigData(data) { 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); diff --git a/src/main/mainWindow.js b/src/main/mainWindow.js index cd40acf4..8aa74a0b 100644 --- a/src/main/mainWindow.js +++ b/src/main/mainWindow.js @@ -53,6 +53,7 @@ function createMainWindow(config, options) { nodeIntegration: true, contextIsolation: false, webviewTag: true, + disableBlinkFeatures: 'Auxclick', }, }); diff --git a/src/main/permissionRequestHandler.js b/src/main/permissionRequestHandler.js deleted file mode 100644 index 63749bd3..00000000 --- a/src/main/permissionRequestHandler.js +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2015-2016 Yuya Ochiai -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {URL} from 'url'; - -import {ipcMain} from 'electron'; - -function dequeueRequests(requestQueue, permissionManager, origin, permission, status) { - switch (status) { - case 'allow': - permissionManager.grant(origin, permission); - break; - case 'block': - permissionManager.deny(origin, permission); - break; - default: - break; - } - if (status === 'allow' || status === 'block') { - const newQueue = requestQueue.filter((request) => { - if (request.origin === origin && request.permission === permission) { - request.callback(status === 'allow'); - return false; - } - return true; - }); - requestQueue.splice(0, requestQueue.length, ...newQueue); - } else { - const index = requestQueue.findIndex((request) => { - return request.origin === origin && request.permission === permission; - }); - requestQueue[index].callback(false); - requestQueue.splice(index, 1); - } -} - -export default function permissionRequestHandler(mainWindow, permissionManager) { - const requestQueue = []; - ipcMain.on('update-permission', (event, origin, permission, status) => { - dequeueRequests(requestQueue, permissionManager, origin, permission, status); - }); - return (webContents, permission, callback) => { - let targetURL; - try { - targetURL = new URL(webContents.getURL()); - } catch (err) { - console.log(err); - callback(false); - return; - } - if (permissionManager.isDenied(targetURL.origin, permission)) { - callback(false); - return; - } - if (permissionManager.isGranted(targetURL.origin, permission)) { - callback(true); - return; - } - - requestQueue.push({ - origin: targetURL.origin, - permission, - callback, - }); - mainWindow.webContents.send('request-permission', targetURL.origin, permission); - }; -} diff --git a/test/specs/permisson_test.js b/test/specs/permisson_test.js deleted file mode 100644 index bf042da6..00000000 --- a/test/specs/permisson_test.js +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) 2015-2016 Yuya Ochiai -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import fs from 'fs'; -import path from 'path'; - -import env from '../modules/environment'; -import PermissionManager from '../../src/main/PermissionManager'; - -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() { - beforeEach(function(done) { - fs.unlink(permissionFile, () => { - done(); - }); - }); - - it('should grant a permission for an origin', function() { - const manager = new PermissionManager(permissionFile); - - manager.isGranted(ORIGIN1, PERMISSION1).should.be.false; - manager.isDenied(ORIGIN1, PERMISSION1).should.be.false; - - manager.grant(ORIGIN1, PERMISSION1); - - manager.isGranted(ORIGIN1, PERMISSION1).should.be.true; - manager.isDenied(ORIGIN1, PERMISSION1).should.be.false; - - manager.isGranted(ORIGIN2, PERMISSION1).should.be.false; - manager.isGranted(ORIGIN1, PERMISSION2).should.be.false; - }); - - it('should deny a permission for an origin', function() { - const manager = new PermissionManager(permissionFile); - - manager.isGranted(ORIGIN1, PERMISSION1).should.be.false; - manager.isDenied(ORIGIN1, PERMISSION1).should.be.false; - - manager.deny(ORIGIN1, PERMISSION1); - - manager.isGranted(ORIGIN1, PERMISSION1).should.be.false; - manager.isDenied(ORIGIN1, PERMISSION1).should.be.true; - - manager.isDenied(ORIGIN2, PERMISSION1).should.be.false; - manager.isDenied(ORIGIN1, PERMISSION2).should.be.false; - }); - - it('should save permissions to the file', function() { - const manager = new PermissionManager(permissionFile); - manager.deny(ORIGIN1, PERMISSION1); - manager.grant(ORIGIN2, PERMISSION2); - JSON.parse(fs.readFileSync(permissionFile)).should.deep.equal({ - [ORIGIN1]: { - [PERMISSION1]: DENIED, - }, - [ORIGIN2]: { - [PERMISSION2]: GRANTED, - }, - }); - }); - - it('should restore permissions from the file', function() { - fs.writeFileSync(permissionFile, JSON.stringify({ - [ORIGIN1]: { - [PERMISSION1]: DENIED, - }, - [ORIGIN2]: { - [PERMISSION2]: GRANTED, - }, - })); - const manager = new PermissionManager(permissionFile); - manager.isDenied(ORIGIN1, PERMISSION1).should.be.true; - manager.isGranted(ORIGIN2, PERMISSION2).should.be.true; - }); - - it('should allow permissions for trusted URLs', function() { - fs.writeFileSync(permissionFile, JSON.stringify({})); - const manager = new PermissionManager(permissionFile, [ORIGIN1, ORIGIN2]); - manager.isGranted(ORIGIN1, PERMISSION1).should.be.true; - manager.isGranted(ORIGIN2, PERMISSION2).should.be.true; - }); -});