[MM-59552] Remove unsafe-inline
from internal CSP, replace with nonce and rework some dynamic styling (#3120)
* Remove unsafe-inline * Fix dynamic dark mode loading * Include nonce generator for CSP for styles * Add nonce provider for react-select * Fix test
This commit is contained in:
parent
e310fa705f
commit
c9f671d82a
3
package-lock.json
generated
3
package-lock.json
generated
|
@ -10,6 +10,7 @@
|
|||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.11.4",
|
||||
"@mattermost/compass-icons": "0.1.45",
|
||||
"auto-launch": "5.0.6",
|
||||
"bootstrap": "4.6.1",
|
||||
|
@ -88,7 +89,7 @@
|
|||
},
|
||||
"api-types": {
|
||||
"name": "@mattermost/desktop-api",
|
||||
"version": "5.9.0-1",
|
||||
"version": "5.10.0-1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
|
|
|
@ -159,6 +159,7 @@
|
|||
"webpack-merge": "5.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.11.4",
|
||||
"@mattermost/compass-icons": "0.1.45",
|
||||
"auto-launch": "5.0.6",
|
||||
"bootstrap": "4.6.1",
|
||||
|
|
|
@ -191,3 +191,5 @@ export const GET_MEDIA_ACCESS_STATUS = 'get-media-access-status';
|
|||
|
||||
// Legacy code remove signal
|
||||
export const LEGACY_OFF = 'legacy-off';
|
||||
|
||||
export const GET_NONCE = 'get-nonce';
|
||||
|
|
|
@ -64,6 +64,9 @@ jest.mock('electron', () => ({
|
|||
},
|
||||
session: {
|
||||
defaultSession: {
|
||||
webRequest: {
|
||||
onHeadersReceived: jest.fn(),
|
||||
},
|
||||
setSpellCheckerDictionaryDownloadURL: jest.fn(),
|
||||
setPermissionRequestHandler: jest.fn(),
|
||||
on: jest.fn(),
|
||||
|
|
|
@ -45,6 +45,7 @@ import {configPath, updatePaths} from 'main/constants';
|
|||
import CriticalErrorHandler from 'main/CriticalErrorHandler';
|
||||
import downloadsManager from 'main/downloadsManager';
|
||||
import i18nManager from 'main/i18nManager';
|
||||
import NonceManager from 'main/nonceManager';
|
||||
import {getDoNotDisturb} from 'main/notifications';
|
||||
import parseArgs from 'main/ParseArgs';
|
||||
import PermissionsManager from 'main/permissionsManager';
|
||||
|
@ -313,6 +314,20 @@ async function initializeAfterAppReady() {
|
|||
|
||||
app.setAppUserModelId('Mattermost.Desktop'); // Use explicit AppUserModelID
|
||||
const defaultSession = session.defaultSession;
|
||||
defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
const url = parseURL(details.url);
|
||||
if (url?.protocol === 'mattermost-desktop:' && url?.pathname.endsWith('html')) {
|
||||
callback({
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
'Content-Security-Policy': [`default-src 'self'; style-src 'self' 'nonce-${NonceManager.create(details.url)}'; media-src data:; img-src 'self' data:`],
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
downloadsManager.webRequestOnHeadersReceivedHandler(details, callback);
|
||||
});
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
defaultSession.on('spellcheck-dictionary-download-failure', (event, lang) => {
|
||||
|
|
34
src/main/nonceManager.ts
Normal file
34
src/main/nonceManager.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ipcMain} from 'electron';
|
||||
import type {IpcMainInvokeEvent} from 'electron';
|
||||
import {v4 as uuid} from 'uuid';
|
||||
|
||||
import {GET_NONCE} from 'common/communication';
|
||||
|
||||
export class NonceManager {
|
||||
private nonces: Map<string, string>;
|
||||
|
||||
constructor() {
|
||||
this.nonces = new Map();
|
||||
|
||||
ipcMain.handle(GET_NONCE, this.handleGetNonce);
|
||||
}
|
||||
|
||||
create = (url: string) => {
|
||||
const nonce = uuid();
|
||||
this.nonces.set(url, nonce);
|
||||
return nonce;
|
||||
};
|
||||
|
||||
private handleGetNonce = (event: IpcMainInvokeEvent) => {
|
||||
const url = event.sender.getURL();
|
||||
const nonce = this.nonces.get(url);
|
||||
this.nonces.delete(url);
|
||||
return nonce;
|
||||
};
|
||||
}
|
||||
|
||||
const nonceManager = new NonceManager();
|
||||
export default nonceManager;
|
|
@ -91,6 +91,7 @@ import {
|
|||
OPEN_WINDOWS_MICROPHONE_PREFERENCES,
|
||||
GET_MEDIA_ACCESS_STATUS,
|
||||
VIEW_FINISHED_RESIZING,
|
||||
GET_NONCE,
|
||||
} from 'common/communication';
|
||||
|
||||
console.log('Preload initialized');
|
||||
|
@ -124,6 +125,7 @@ contextBridge.exposeInMainWorld('desktop', {
|
|||
goBack: () => ipcRenderer.send(HISTORY, -1),
|
||||
checkForUpdates: () => ipcRenderer.send(CHECK_FOR_UPDATES),
|
||||
updateConfiguration: (saveQueueItems) => ipcRenderer.send(UPDATE_CONFIGURATION, saveQueueItems),
|
||||
getNonce: () => ipcRenderer.invoke(GET_NONCE),
|
||||
|
||||
updateServerOrder: (serverOrder) => ipcRenderer.send(UPDATE_SERVER_ORDER, serverOrder),
|
||||
updateTabOrder: (serverId, viewOrder) => ipcRenderer.send(UPDATE_TAB_ORDER, serverId, viewOrder),
|
||||
|
|
|
@ -66,7 +66,6 @@ export class DownloadsDropdownView {
|
|||
}});
|
||||
|
||||
this.view.webContents.loadURL('mattermost-desktop://renderer/downloadsDropdown.html');
|
||||
this.view.webContents.session.webRequest.onHeadersReceived(downloadsManager.webRequestOnHeadersReceivedHandler);
|
||||
MainWindow.get()?.addBrowserView(this.view);
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
|
||||
import 'renderer/css/settings.css';
|
||||
|
||||
import createCache from '@emotion/cache';
|
||||
import {CacheProvider} from '@emotion/react';
|
||||
import type {EmotionCache} from '@emotion/react';
|
||||
import React from 'react';
|
||||
import {FormCheck, Col, FormGroup, FormText, Container, Row, Button, FormControl, Modal} from 'react-bootstrap';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
@ -39,6 +42,7 @@ type State = DeepPartial<CombinedConfig> & {
|
|||
availableLanguages: Array<{label: string; value: string}>;
|
||||
availableSpellcheckerLanguages: Array<{label: string; value: string}>;
|
||||
canUpgrade?: boolean;
|
||||
cache?: EmotionCache;
|
||||
}
|
||||
|
||||
type SavingStateItems = {
|
||||
|
@ -127,6 +131,13 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
availableLanguages.sort((a, b) => a.label.localeCompare(b.label));
|
||||
this.setState({availableLanguages});
|
||||
});
|
||||
|
||||
window.desktop.getNonce().then((nonce) => {
|
||||
this.setState({cache: createCache({
|
||||
key: 'react-select-cache',
|
||||
nonce,
|
||||
})});
|
||||
});
|
||||
}
|
||||
|
||||
getConfig = () => {
|
||||
|
@ -426,6 +437,10 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
render() {
|
||||
const {intl} = this.props;
|
||||
|
||||
if (!this.state.cache) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const settingsPage = {
|
||||
close: {
|
||||
textDecoration: 'none',
|
||||
|
@ -581,22 +596,24 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
</FormText>
|
||||
</FormCheck>
|
||||
{this.state.useSpellChecker &&
|
||||
<ReactSelect
|
||||
inputId='inputSpellCheckerLocalesDropdown'
|
||||
className='SettingsPage__spellCheckerLocalesDropdown'
|
||||
classNamePrefix='SettingsPage__spellCheckerLocalesDropdown'
|
||||
options={this.state.availableSpellcheckerLanguages}
|
||||
isMulti={true}
|
||||
isClearable={false}
|
||||
onChange={this.handleChangeSpellCheckerLocales}
|
||||
value={this.selectedSpellCheckerLocales}
|
||||
placeholder={
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling.preferredLanguages'
|
||||
defaultMessage='Select preferred language(s)'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<CacheProvider value={this.state.cache}>
|
||||
<ReactSelect
|
||||
inputId='inputSpellCheckerLocalesDropdown'
|
||||
className='SettingsPage__spellCheckerLocalesDropdown'
|
||||
classNamePrefix='SettingsPage__spellCheckerLocalesDropdown'
|
||||
options={this.state.availableSpellcheckerLanguages}
|
||||
isMulti={true}
|
||||
isClearable={false}
|
||||
onChange={this.handleChangeSpellCheckerLocales}
|
||||
value={this.selectedSpellCheckerLocales}
|
||||
placeholder={
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling.preferredLanguages'
|
||||
defaultMessage='Select preferred language(s)'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</CacheProvider>
|
||||
}
|
||||
</>,
|
||||
);
|
||||
|
|
|
@ -32,6 +32,10 @@ type Props = {
|
|||
intl: IntlShape;
|
||||
};
|
||||
|
||||
type State = {
|
||||
nonce?: string;
|
||||
}
|
||||
|
||||
function getStyle(style?: DraggingStyle | NotDraggingStyle) {
|
||||
if (style?.transform) {
|
||||
const axisLockX = `${style.transform.slice(0, style.transform.indexOf(','))}, 0px)`;
|
||||
|
@ -43,7 +47,12 @@ function getStyle(style?: DraggingStyle | NotDraggingStyle) {
|
|||
return style;
|
||||
}
|
||||
|
||||
class TabBar extends React.PureComponent<Props> {
|
||||
class TabBar extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
onCloseTab = (id: string) => {
|
||||
return (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
|
@ -51,7 +60,19 @@ class TabBar extends React.PureComponent<Props> {
|
|||
};
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
window.desktop.getNonce().then((nonce) => {
|
||||
this.setState({
|
||||
nonce,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.nonce) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tabs = this.props.tabs.map((tab, index) => {
|
||||
const sessionExpired = this.props.sessionsExpired[tab.id!];
|
||||
const hasUnreads = this.props.unreadCounts[tab.id!];
|
||||
|
@ -145,7 +166,10 @@ class TabBar extends React.PureComponent<Props> {
|
|||
});
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={this.props.onDrop}>
|
||||
<DragDropContext
|
||||
nonce={this.state.nonce}
|
||||
onDragEnd={this.props.onDrop}
|
||||
>
|
||||
<Droppable
|
||||
isDropDisabled={this.props.tabsDisabled}
|
||||
droppableId='tabBar'
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
@import '~bootstrap-dark/src/bootstrap-dark.css';
|
||||
|
||||
body {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__control {
|
||||
background: #242a30;
|
||||
}
|
||||
|
||||
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__menu {
|
||||
background: #242a30;
|
||||
}
|
||||
|
||||
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__option {
|
||||
background: #242a30;
|
||||
}
|
||||
|
||||
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__option:hover {
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
#settingsModal .modal-body {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--light) rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
.Toggle .Toggle___switch {
|
||||
background: rgba(var(--center-channel-bg-rgb), 0.24);
|
||||
}
|
||||
|
||||
.Toggle .Toggle___switch.disabled {
|
||||
background: rgba(var(--center-channel-bg-rgb), 0.08);
|
||||
}
|
38
src/renderer/css/modals-dark.scss
Normal file
38
src/renderer/css/modals-dark.scss
Normal file
|
@ -0,0 +1,38 @@
|
|||
@use 'sass:meta';
|
||||
|
||||
.darkMode {
|
||||
@include meta.load-css('bootstrap-dark/src/bootstrap-dark.css');
|
||||
color: #fff;
|
||||
|
||||
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__control {
|
||||
background: #242a30;
|
||||
}
|
||||
|
||||
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__menu {
|
||||
background: #242a30;
|
||||
}
|
||||
|
||||
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__option {
|
||||
background: #242a30;
|
||||
}
|
||||
|
||||
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__option:hover {
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
#settingsModal .modal-body {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--light) rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
.Toggle___switch {
|
||||
background: rgba(var(--center-channel-bg-rgb), 0.24);
|
||||
}
|
||||
|
||||
.Toggle___switch.disabled {
|
||||
background: rgba(var(--center-channel-bg-rgb), 0.08);
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ type State = {
|
|||
hasGPOServers?: boolean;
|
||||
isAnyDragging: boolean;
|
||||
windowBounds?: Electron.Rectangle;
|
||||
nonce?: string;
|
||||
}
|
||||
|
||||
function getStyle(style?: DraggingStyle | NotDraggingStyle) {
|
||||
|
@ -141,6 +142,9 @@ class ServerDropdown extends React.PureComponent<Record<string, never>, State> {
|
|||
window.desktop.serverDropdown.requestInfo();
|
||||
window.addEventListener('click', this.closeMenu);
|
||||
window.addEventListener('keydown', this.handleKeyboardShortcuts);
|
||||
window.desktop.getNonce().then((nonce) => {
|
||||
this.setState({nonce});
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
|
@ -231,6 +235,10 @@ class ServerDropdown extends React.PureComponent<Record<string, never>, State> {
|
|||
};
|
||||
|
||||
render() {
|
||||
if (!this.state.nonce) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IntlProvider>
|
||||
<div
|
||||
|
@ -256,6 +264,7 @@ class ServerDropdown extends React.PureComponent<Record<string, never>, State> {
|
|||
</div>
|
||||
<hr className='ServerDropdown__divider'/>
|
||||
<DragDropContext
|
||||
nonce={this.state.nonce}
|
||||
onDragStart={this.onDragStart}
|
||||
onDragEnd={this.onDragEnd}
|
||||
>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; media-src data:; img-src 'self' data:">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import darkStyles from 'renderer/css/lazy/modals-dark.lazy.css';
|
||||
import 'renderer/css/modals-dark.scss';
|
||||
|
||||
export default function addDarkModeListener() {
|
||||
const setDarkMode = (darkMode: boolean) => {
|
||||
if (darkMode) {
|
||||
darkStyles.use();
|
||||
document.body.classList.add('darkMode');
|
||||
} else {
|
||||
darkStyles.unuse();
|
||||
document.body.classList.remove('darkMode');
|
||||
}
|
||||
};
|
||||
window.desktop.onDarkModeChange(setDarkMode);
|
||||
|
|
|
@ -44,6 +44,7 @@ declare global {
|
|||
goBack: () => void;
|
||||
checkForUpdates: () => void;
|
||||
updateConfiguration: (saveQueueItems: SaveQueueItem[]) => void;
|
||||
getNonce: () => Promise<string | undefined>;
|
||||
|
||||
updateServerOrder: (serverOrder: string[]) => Promise<void>;
|
||||
updateTabOrder: (serverId: string, viewOrder: string[]) => Promise<void>;
|
||||
|
|
|
@ -152,7 +152,14 @@ module.exports = merge(base, {
|
|||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
'css-loader',
|
||||
'sass-loader',
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
sassOptions: {
|
||||
includePaths: [path.resolve(__dirname, 'node_modules')],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}, {
|
||||
test: /\.mp3$/,
|
||||
|
|
Loading…
Reference in a new issue