From 080e4bf7270c76d52c5a69a2094d20482082117e Mon Sep 17 00:00:00 2001 From: Devin Binnie <52460000+devinbinnie@users.noreply.github.com> Date: Thu, 18 Jul 2024 16:01:44 -0400 Subject: [PATCH] [MM-59543] Disallow use of file: protocol in the app, remove all references to it, add mattermost-desktop: protocol to read local files (#3095) --- package-lock.json | 8 ++--- package.json | 2 +- scripts/afterpack.js | 1 + src/app/serverViewState.test.js | 6 +--- src/app/serverViewState.ts | 8 ++--- src/common/Validator.ts | 1 + src/common/communication.ts | 1 - src/main/app/initialize.test.js | 4 +++ src/main/app/initialize.ts | 26 ++++++++++++++- src/main/app/intercom.test.js | 7 ++-- src/main/app/intercom.ts | 6 ++-- src/main/authManager.test.js | 1 - src/main/authManager.ts | 6 ++-- src/main/certificateManager.test.js | 1 - src/main/certificateManager.ts | 4 +-- src/main/contextMenu.ts | 2 +- src/main/downloadsManager.test.js | 4 +-- src/main/downloadsManager.ts | 24 +++++++++----- src/main/menus/app.ts | 4 +-- src/main/menus/tray.ts | 4 +-- src/main/preload/internalAPI.js | 5 --- src/main/utils.test.js | 24 -------------- src/main/utils.ts | 31 ++---------------- .../views/downloadsDropdownMenuView.test.js | 1 - src/main/views/downloadsDropdownMenuView.ts | 4 +-- src/main/views/downloadsDropdownView.test.js | 1 - src/main/views/downloadsDropdownView.ts | 12 ++----- src/main/views/loadingScreen.ts | 4 +-- src/main/views/serverDropdownView.test.js | 1 - src/main/views/serverDropdownView.ts | 4 +-- src/main/views/viewManager.ts | 5 ++- src/main/windows/mainWindow.test.js | 1 - src/main/windows/mainWindow.ts | 4 +-- .../assets}/StippleMask.jpg | Bin .../DownloadsDropdown/Thumbnail.tsx | 21 +++--------- .../LoadingScreen/LoadingBackground.tsx | 4 ++- src/renderer/index.html | 2 +- src/types/downloads.ts | 1 + src/types/external/file-types.d.ts | 1 + src/types/window.ts | 3 -- webpack.config.renderer.js | 2 +- 41 files changed, 99 insertions(+), 152 deletions(-) rename src/{assets/loader => renderer/assets}/StippleMask.jpg (100%) diff --git a/package-lock.json b/package-lock.json index 2a9dd358..3916d938 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "@babel/preset-env": "7.24.0", "@babel/preset-react": "7.23.3", "@babel/preset-typescript": "7.23.3", - "@electron/fuses": "1.6.0", + "@electron/fuses": "1.8.0", "@mattermost/desktop-api": "file:api-types", "@mattermost/eslint-plugin": "1.1.0-0", "@types/auto-launch": "5.0.5", @@ -2050,9 +2050,9 @@ } }, "node_modules/@electron/fuses": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.6.0.tgz", - "integrity": "sha512-UnZgLfVO1jf7QoYVEEB27CCP1JjT5plhbWU1U8ji1OaXnvNe5UT6KPuRJ3Z12mwa5ZBAASU2tgxVuI06/2x6nQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", "dev": true, "dependencies": { "chalk": "^4.1.1", diff --git a/package.json b/package.json index 8fcc7828..4d6a5279 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "@babel/preset-env": "7.24.0", "@babel/preset-react": "7.23.3", "@babel/preset-typescript": "7.23.3", - "@electron/fuses": "1.6.0", + "@electron/fuses": "1.8.0", "@mattermost/desktop-api": "file:api-types", "@mattermost/eslint-plugin": "1.1.0-0", "@types/auto-launch": "5.0.5", diff --git a/scripts/afterpack.js b/scripts/afterpack.js index 10321ca4..f5857c3b 100644 --- a/scripts/afterpack.js +++ b/scripts/afterpack.js @@ -43,6 +43,7 @@ exports.default = async function afterPack(context) { version: FuseVersion.V1, [FuseV1Options.RunAsNode]: false, // Disables ELECTRON_RUN_AS_NODE [FuseV1Options.EnableNodeCliInspectArguments]: false, // Disables --inspect + [FuseV1Options.GrantFileProtocolExtraPrivileges]: false, [FuseV1Options.EnableCookieEncryption]: true, [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, // Disables NODE_OPTIONS and NODE_EXTRA_CA_CERTS // Can only verify on macOS right now, electron-builder doesn't support Windows ASAR integrity verification diff --git a/src/app/serverViewState.test.js b/src/app/serverViewState.test.js index 41343e88..d0fb4299 100644 --- a/src/app/serverViewState.test.js +++ b/src/app/serverViewState.test.js @@ -7,7 +7,7 @@ import {URLValidationStatus} from 'common/utils/constants'; import {getDefaultViewsForConfigServer} from 'common/views/View'; import PermissionsManager from 'main/permissionsManager'; import {ServerInfo} from 'main/server/serverInfo'; -import {getLocalURLString, getLocalPreload} from 'main/utils'; +import {getLocalPreload} from 'main/utils'; import ModalManager from 'main/views/modalManager'; import ViewManager from 'main/views/viewManager'; import MainWindow from 'main/windows/mainWindow'; @@ -50,7 +50,6 @@ jest.mock('main/views/modalManager', () => ({ })); jest.mock('main/utils', () => ({ getLocalPreload: jest.fn(), - getLocalURLString: jest.fn(), })); jest.mock('main/windows/mainWindow', () => ({ get: jest.fn(), @@ -169,7 +168,6 @@ describe('app/serverViewState', () => { let serversCopy; beforeEach(() => { - getLocalURLString.mockReturnValue('/some/index.html'); getLocalPreload.mockReturnValue('/some/preload.js'); MainWindow.get.mockReturnValue({}); @@ -226,7 +224,6 @@ describe('app/serverViewState', () => { let serversCopy; beforeEach(() => { - getLocalURLString.mockReturnValue('/some/index.html'); getLocalPreload.mockReturnValue('/some/preload.js'); MainWindow.get.mockReturnValue({}); @@ -311,7 +308,6 @@ describe('app/serverViewState', () => { let serversCopy; beforeEach(() => { - getLocalURLString.mockReturnValue('/some/index.html'); getLocalPreload.mockReturnValue('/some/preload.js'); MainWindow.get.mockReturnValue({}); diff --git a/src/app/serverViewState.ts b/src/app/serverViewState.ts index eb34342c..6cff3392 100644 --- a/src/app/serverViewState.ts +++ b/src/app/serverViewState.ts @@ -28,7 +28,7 @@ import {URLValidationStatus} from 'common/utils/constants'; import {isValidURI, isValidURL, parseURL} from 'common/utils/url'; import PermissionsManager from 'main/permissionsManager'; import {ServerInfo} from 'main/server/serverInfo'; -import {getLocalPreload, getLocalURLString} from 'main/utils'; +import {getLocalPreload} from 'main/utils'; import ModalManager from 'main/views/modalManager'; import ViewManager from 'main/views/viewManager'; import MainWindow from 'main/windows/mainWindow'; @@ -133,7 +133,7 @@ export class ServerViewState { const modalPromise = ModalManager.addModal( 'newServer', - getLocalURLString('newServer.html'), + 'mattermost-desktop://renderer/newServer.html', getLocalPreload('internalAPI.js'), null, mainWindow, @@ -165,7 +165,7 @@ export class ServerViewState { const modalPromise = ModalManager.addModal( 'editServer', - getLocalURLString('editServer.html'), + 'mattermost-desktop://renderer/editServer.html', getLocalPreload('internalAPI.js'), {server: server.toUniqueServer(), permissions: PermissionsManager.getForServer(server) ?? {}}, mainWindow); @@ -195,7 +195,7 @@ export class ServerViewState { const modalPromise = ModalManager.addModal( 'removeServer', - getLocalURLString('removeServer.html'), + 'mattermost-desktop://renderer/removeServer.html', getLocalPreload('internalAPI.js'), server.name, mainWindow, diff --git a/src/common/Validator.ts b/src/common/Validator.ts index 7075d321..6a6acf4f 100644 --- a/src/common/Validator.ts +++ b/src/common/Validator.ts @@ -60,6 +60,7 @@ const downloadsSchema = Joi.object().pattern( receivedBytes: Joi.number().min(0), totalBytes: Joi.number().min(0), bookmark: Joi.string(), + thumbnailData: Joi.string(), }); const configDataSchemaV0 = Joi.object({ diff --git a/src/common/communication.ts b/src/common/communication.ts index 2bc7b1fe..f65e3519 100644 --- a/src/common/communication.ts +++ b/src/common/communication.ts @@ -146,7 +146,6 @@ export const DOWNLOADS_DROPDOWN_OPEN_FILE = 'downloads-dropdown-open-file'; export const REQUEST_HAS_DOWNLOADS = 'request-has-downloads'; export const DOWNLOADS_DROPDOWN_FOCUSED = 'downloads-dropdown-focused'; export const RECEIVE_DOWNLOADS_DROPDOWN_SIZE = 'receive-downloads-dropdown-size'; -export const GET_DOWNLOADED_IMAGE_THUMBNAIL_LOCATION = 'get-downloaded-image-thumbnail-location'; export const OPEN_DOWNLOADS_DROPDOWN_MENU = 'open-downloads-dropdown-menu'; export const CLOSE_DOWNLOADS_DROPDOWN_MENU = 'close-downloads-dropdown-menu'; diff --git a/src/main/app/initialize.test.js b/src/main/app/initialize.test.js index f8ff46c6..d5dc8fe0 100644 --- a/src/main/app/initialize.test.js +++ b/src/main/app/initialize.test.js @@ -69,6 +69,10 @@ jest.mock('electron', () => ({ on: jest.fn(), }, }, + protocol: { + registerSchemesAsPrivileged: jest.fn(), + handle: jest.fn(), + }, })); jest.mock('main/i18nManager', () => ({ diff --git a/src/main/app/initialize.ts b/src/main/app/initialize.ts index a3fb9740..387df0d8 100644 --- a/src/main/app/initialize.ts +++ b/src/main/app/initialize.ts @@ -2,8 +2,9 @@ // See LICENSE.txt for license information. import path from 'path'; +import {pathToFileURL} from 'url'; -import {app, ipcMain, nativeTheme, session} from 'electron'; +import {app, ipcMain, nativeTheme, net, protocol, session} from 'electron'; import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-extension-installer'; import isDev from 'electron-is-dev'; @@ -36,6 +37,7 @@ import { import Config from 'common/config'; import {Logger} from 'common/log'; import ServerManager from 'common/servers/serverManager'; +import {parseURL} from 'common/utils/url'; import AllowProtocolDialog from 'main/allowProtocolDialog'; import AppVersionManager from 'main/AppVersionManager'; import AuthManager from 'main/authManager'; @@ -254,6 +256,10 @@ function initializeBeforeAppReady() { nativeTheme.on('updated', handleUpdateTheme); handleUpdateTheme(); } + + protocol.registerSchemesAsPrivileged([ + {scheme: 'mattermost-desktop', privileges: {standard: true}}, + ]); } function initializeInterCommunicationEventListeners() { @@ -291,6 +297,24 @@ function initializeInterCommunicationEventListeners() { } async function initializeAfterAppReady() { + protocol.handle('mattermost-desktop', (request: Request) => { + const url = parseURL(request.url); + if (!url) { + return new Response('bad', {status: 400}); + } + + // Including this snippet from the handler docs to check for path traversal + // https://www.electronjs.org/docs/latest/api/protocol#protocolhandlescheme-handler + const pathToServe = path.join(app.getAppPath(), 'renderer', url.pathname); + const relativePath = path.relative(app.getAppPath(), pathToServe); + const isSafe = relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath); + if (!isSafe) { + return new Response('bad', {status: 400}); + } + + return net.fetch(pathToFileURL(pathToServe).toString()); + }); + ServerManager.reloadFromConfig(); updateServerInfos(ServerManager.getAllServers()); ServerManager.on(SERVERS_URL_MODIFIED, (serverIds?: string[]) => { diff --git a/src/main/app/intercom.test.js b/src/main/app/intercom.test.js index b8c3f7f4..c186a1fa 100644 --- a/src/main/app/intercom.test.js +++ b/src/main/app/intercom.test.js @@ -4,7 +4,7 @@ import {app} from 'electron'; import ServerManager from 'common/servers/serverManager'; -import {getLocalURLString, getLocalPreload} from 'main/utils'; +import {getLocalPreload} from 'main/utils'; import ModalManager from 'main/views/modalManager'; import MainWindow from 'main/windows/mainWindow'; @@ -38,7 +38,6 @@ jest.mock('common/servers/serverManager', () => ({ })); jest.mock('main/utils', () => ({ getLocalPreload: jest.fn(), - getLocalURLString: jest.fn(), })); jest.mock('main/views/viewManager', () => ({})); jest.mock('main/views/modalManager', () => ({ @@ -53,7 +52,6 @@ jest.mock('./app', () => ({})); describe('main/app/intercom', () => { describe('handleWelcomeScreenModal', () => { beforeEach(() => { - getLocalURLString.mockReturnValue('/some/index.html'); getLocalPreload.mockReturnValue('/some/preload.js'); MainWindow.get.mockReturnValue({}); @@ -65,13 +63,12 @@ describe('main/app/intercom', () => { ModalManager.addModal.mockReturnValue(promise); handleWelcomeScreenModal(); - expect(ModalManager.addModal).toHaveBeenCalledWith('welcomeScreen', '/some/index.html', '/some/preload.js', null, {}, true); + expect(ModalManager.addModal).toHaveBeenCalledWith('welcomeScreen', 'mattermost-desktop://renderer/welcomeScreen.html', '/some/preload.js', null, {}, true); }); }); describe('handleMainWindowIsShown', () => { it('MM-48079 should not show onboarding screen or server screen if GPO server is pre-configured', () => { - getLocalURLString.mockReturnValue('/some/index.html'); getLocalPreload.mockReturnValue('/some/preload.js'); MainWindow.get.mockReturnValue({ isVisible: () => true, diff --git a/src/main/app/intercom.ts b/src/main/app/intercom.ts index 4fb3ee4f..8acfac87 100644 --- a/src/main/app/intercom.ts +++ b/src/main/app/intercom.ts @@ -9,7 +9,7 @@ import {Logger} from 'common/log'; import ServerManager from 'common/servers/serverManager'; import {ping} from 'common/utils/requests'; import NotificationManager from 'main/notifications'; -import {getLocalPreload, getLocalURLString} from 'main/utils'; +import {getLocalPreload} from 'main/utils'; import ModalManager from 'main/views/modalManager'; import MainWindow from 'main/windows/mainWindow'; @@ -88,7 +88,7 @@ export function handleMainWindowIsShown() { export function handleWelcomeScreenModal() { log.debug('handleWelcomeScreenModal'); - const html = getLocalURLString('welcomeScreen.html'); + const html = 'mattermost-desktop://renderer/welcomeScreen.html'; const preload = getLocalPreload('internalAPI.js'); @@ -169,7 +169,7 @@ export function handleShowSettingsModal() { ModalManager.addModal( 'settingsModal', - getLocalURLString('settings.html'), + 'mattermost-desktop://renderer/settings.html', getLocalPreload('internalAPI.js'), null, mainWindow, diff --git a/src/main/authManager.test.js b/src/main/authManager.test.js index 45669643..c408e58e 100644 --- a/src/main/authManager.test.js +++ b/src/main/authManager.test.js @@ -51,7 +51,6 @@ jest.mock('main/views/modalManager', () => ({ jest.mock('main/utils', () => ({ getLocalPreload: (file) => file, - getLocalURLString: (file) => file, })); describe('main/authManager', () => { diff --git a/src/main/authManager.ts b/src/main/authManager.ts index 2141b56c..a0a8d892 100644 --- a/src/main/authManager.ts +++ b/src/main/authManager.ts @@ -6,7 +6,7 @@ import {Logger} from 'common/log'; import {BASIC_AUTH_PERMISSION} from 'common/permissions'; import {isCustomLoginURL, isTrustedURL, parseURL} from 'common/utils/url'; import TrustedOriginsStore from 'main/trustedOrigins'; -import {getLocalURLString, getLocalPreload} from 'main/utils'; +import {getLocalPreload} from 'main/utils'; import modalManager from 'main/views/modalManager'; import ViewManager from 'main/views/viewManager'; import MainWindow from 'main/windows/mainWindow'; @@ -16,8 +16,8 @@ import type {PermissionType} from 'types/trustedOrigin'; const log = new Logger('AuthManager'); const preload = getLocalPreload('internalAPI.js'); -const loginModalHtml = getLocalURLString('loginModal.html'); -const permissionModalHtml = getLocalURLString('permissionModal.html'); +const loginModalHtml = 'mattermost-desktop://renderer/loginModal.html'; +const permissionModalHtml = 'mattermost-desktop://renderer/permissionModal.html'; type LoginModalResult = { username: string; diff --git a/src/main/certificateManager.test.js b/src/main/certificateManager.test.js index 5dbaa038..66129b07 100644 --- a/src/main/certificateManager.test.js +++ b/src/main/certificateManager.test.js @@ -16,7 +16,6 @@ jest.mock('main/views/modalManager', () => ({ jest.mock('main/utils', () => ({ getLocalPreload: (file) => file, - getLocalURLString: (file) => file, })); describe('main/certificateManager', () => { diff --git a/src/main/certificateManager.ts b/src/main/certificateManager.ts index 51f796ad..1387d447 100644 --- a/src/main/certificateManager.ts +++ b/src/main/certificateManager.ts @@ -7,13 +7,13 @@ import {Logger} from 'common/log'; import type {CertificateModalData} from 'types/certificate'; -import {getLocalURLString, getLocalPreload} from './utils'; +import {getLocalPreload} from './utils'; import modalManager from './views/modalManager'; import MainWindow from './windows/mainWindow'; const log = new Logger('CertificateManager'); const preload = getLocalPreload('internalAPI.js'); -const html = getLocalURLString('certificateModal.html'); +const html = 'mattermost-desktop://renderer/certificateModal.html'; type CertificateModalResult = { cert: Certificate; diff --git a/src/main/contextMenu.ts b/src/main/contextMenu.ts index d584a075..1429cf52 100644 --- a/src/main/contextMenu.ts +++ b/src/main/contextMenu.ts @@ -14,7 +14,7 @@ const defaultMenuOptions = { let isInternalSrc; try { const srcurl = parseURL(p.srcURL); - isInternalSrc = srcurl?.protocol === 'file:'; + isInternalSrc = srcurl?.protocol === 'mattermost-desktop:'; } catch (err) { isInternalSrc = false; } diff --git a/src/main/downloadsManager.test.js b/src/main/downloadsManager.test.js index 6445d2f0..fc16e4e8 100644 --- a/src/main/downloadsManager.test.js +++ b/src/main/downloadsManager.test.js @@ -148,11 +148,11 @@ describe('main/downloadsManager', () => { it('should mark "completed" files that were deleted as "deleted"', () => { expect(new DownloadsManager(JSON.stringify(downloadsJson))).toHaveProperty('downloads', {...downloadsJson, 'file1.txt': {...downloadsJson['file1.txt'], state: 'deleted'}}); }); - it('should handle a new download', () => { + it('should handle a new download', async () => { const dl = new DownloadsManager({}); path.parse.mockImplementation(() => ({base: 'file.txt'})); dl.willDownloadURLs.set('http://some-url.com/some-text.txt', {filePath: locationMock}); - dl.handleNewDownload({preventDefault: jest.fn()}, item, {id: 0, getURL: jest.fn(), downloadURL: jest.fn()}); + await dl.handleNewDownload({preventDefault: jest.fn()}, item, {id: 0, getURL: jest.fn(), downloadURL: jest.fn()}); expect(dl).toHaveProperty('downloads', {'file.txt': { addedAt: nowSeconds * 1000, filename: 'file.txt', diff --git a/src/main/downloadsManager.ts b/src/main/downloadsManager.ts index 9a315697..51f9f8fc 100644 --- a/src/main/downloadsManager.ts +++ b/src/main/downloadsManager.ts @@ -4,7 +4,7 @@ import fs from 'fs'; import path from 'path'; import type {DownloadItem, Event, WebContents, FileFilter, IpcMainInvokeEvent} from 'electron'; -import {ipcMain, dialog, shell, Menu, app} from 'electron'; +import {ipcMain, dialog, shell, Menu, app, nativeImage} from 'electron'; import type {ProgressInfo, UpdateInfo} from 'electron-updater'; import { @@ -123,7 +123,7 @@ export class DownloadsManager extends JsonFileManager { item.setSavePath(info.filePath); } - this.upsertFileToDownloads(item, 'progressing'); + await this.upsertFileToDownloads(item, 'progressing'); this.progressingItems.set(this.getFileId(item), item); this.handleDownloadItemEvents(item, webContents); this.openDownloadsDropdown(); @@ -501,10 +501,10 @@ export class DownloadsManager extends JsonFileManager { } }; - private upsertFileToDownloads = (item: DownloadItem, state: DownloadItemState, overridePath?: string) => { + private upsertFileToDownloads = async (item: DownloadItem, state: DownloadItemState, overridePath?: string) => { const fileId = this.getFileId(item); log.debug('upsertFileToDownloads', {fileId}); - const formattedItem = this.formatDownloadItem(item, state, overridePath); + const formattedItem = await this.formatDownloadItem(item, state, overridePath); this.save(fileId, formattedItem); this.checkIfMaxFilesReached(); }; @@ -545,10 +545,10 @@ export class DownloadsManager extends JsonFileManager { /** * DownloadItem event handlers */ - private updatedEventController = (updatedEvent: Event, state: DownloadItemUpdatedEventState, item: DownloadItem) => { + private updatedEventController = async (updatedEvent: Event, state: DownloadItemUpdatedEventState, item: DownloadItem) => { log.debug('updatedEventController', {state}); - this.upsertFileToDownloads(item, state); + await this.upsertFileToDownloads(item, state); if (state === 'interrupted') { this.fileSizes.delete(item.getFilename()); @@ -557,7 +557,7 @@ export class DownloadsManager extends JsonFileManager { this.shouldShowBadge(); }; - private doneEventController = (doneEvent: Event, state: DownloadItemDoneEventState, item: DownloadItem, webContents: WebContents) => { + private doneEventController = async (doneEvent: Event, state: DownloadItemDoneEventState, item: DownloadItem, webContents: WebContents) => { log.debug('doneEventController', {state}); if (state === 'completed' && !this.open) { @@ -571,7 +571,7 @@ export class DownloadsManager extends JsonFileManager { func(); } - this.upsertFileToDownloads(item, state, bookmark?.originalPath); + await this.upsertFileToDownloads(item, state, bookmark?.originalPath); this.fileSizes.delete(item.getFilename()); this.progressingItems.delete(this.getFileId(item)); this.shouldAutoClose(); @@ -628,11 +628,16 @@ export class DownloadsManager extends JsonFileManager { /** * Internal utils */ - private formatDownloadItem = (item: DownloadItem, state: DownloadItemState, overridePath?: string): DownloadedItem => { + private formatDownloadItem = async (item: DownloadItem, state: DownloadItemState, overridePath?: string): Promise => { const totalBytes = this.getFileSize(item); const receivedBytes = item.getReceivedBytes(); const progress = getPercentage(receivedBytes, totalBytes); + let thumbnailData; + if (state === 'completed' && item.getMimeType().toLowerCase().startsWith('image/')) { + thumbnailData = (await nativeImage.createThumbnailFromPath(overridePath ?? item.getSavePath(), {height: 32, width: 32})).toDataURL(); + } + return { addedAt: doubleSecToMs(item.getStartTime()), filename: this.getFileId(item), @@ -644,6 +649,7 @@ export class DownloadsManager extends JsonFileManager { totalBytes, type: DownloadItemTypeEnum.FILE, bookmark: this.getBookmark(item), + thumbnailData, }; }; diff --git a/src/main/menus/app.ts b/src/main/menus/app.ts index a097f30f..818a0e70 100644 --- a/src/main/menus/app.ts +++ b/src/main/menus/app.ts @@ -18,7 +18,7 @@ import type {UpdateManager} from 'main/autoUpdater'; import Diagnostics from 'main/diagnostics'; import downloadsManager from 'main/downloadsManager'; import {localizeMessage} from 'main/i18nManager'; -import {getLocalPreload, getLocalURLString} from 'main/utils'; +import {getLocalPreload} from 'main/utils'; import ModalManager from 'main/views/modalManager'; import ViewManager from 'main/views/viewManager'; import CallsWidgetWindow from 'main/windows/callsWidgetWindow'; @@ -57,7 +57,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) { ModalManager.addModal( 'settingsModal', - getLocalURLString('settings.html'), + 'mattermost-desktop://renderer/settings.html', getLocalPreload('internalAPI.js'), null, mainWindow, diff --git a/src/main/menus/tray.ts b/src/main/menus/tray.ts index 383fe9fc..0cac0c52 100644 --- a/src/main/menus/tray.ts +++ b/src/main/menus/tray.ts @@ -9,7 +9,7 @@ import {Menu} from 'electron'; import ServerViewState from 'app/serverViewState'; import ServerManager from 'common/servers/serverManager'; import {localizeMessage} from 'main/i18nManager'; -import {getLocalPreload, getLocalURLString} from 'main/utils'; +import {getLocalPreload} from 'main/utils'; import ModalManager from 'main/views/modalManager'; import MainWindow from 'main/windows/mainWindow'; @@ -35,7 +35,7 @@ export function createTemplate() { ModalManager.addModal( 'settingsModal', - getLocalURLString('settings.html'), + 'mattermost-desktop://renderer/settings.html', getLocalPreload('internalAPI.js'), null, mainWindow, diff --git a/src/main/preload/internalAPI.js b/src/main/preload/internalAPI.js index 935a9321..d33494ec 100644 --- a/src/main/preload/internalAPI.js +++ b/src/main/preload/internalAPI.js @@ -72,7 +72,6 @@ import { START_UPDATE_DOWNLOAD, START_UPGRADE, TOGGLE_DOWNLOADS_DROPDOWN_MENU, - GET_DOWNLOADED_IMAGE_THUMBNAIL_LOCATION, DOWNLOADS_DROPDOWN_OPEN_FILE, MODAL_CANCEL, MODAL_RESULT, @@ -110,10 +109,6 @@ contextBridge.exposeInMainWorld('timers', { setImmediate, }); -contextBridge.exposeInMainWorld('mas', { - getThumbnailLocation: (location) => ipcRenderer.invoke(GET_DOWNLOADED_IMAGE_THUMBNAIL_LOCATION, location), -}); - contextBridge.exposeInMainWorld('desktop', { quit: (reason, stack) => ipcRenderer.send(QUIT, reason, stack), openAppMenu: () => ipcRenderer.send(OPEN_APP_MENU), diff --git a/src/main/utils.test.js b/src/main/utils.test.js index 57a39c24..f6dd57ff 100644 --- a/src/main/utils.test.js +++ b/src/main/utils.test.js @@ -3,7 +3,6 @@ 'use strict'; import {BACK_BAR_HEIGHT, TAB_BAR_HEIGHT} from 'common/utils/constants'; -import {runMode} from 'common/utils/util'; import * as Utils from './utils'; @@ -82,29 +81,6 @@ describe('main/utils', () => { }); }); - describe('getLocalURLString', () => { - it('should return URL relative to current run directory', () => { - runMode.mockImplementation(() => 'development'); - expect(Utils.getLocalURLString('index.html')).toStrictEqual('file:///path/to/app/dist/renderer/index.html'); - }); - - it('should return URL relative to current run directory in production', () => { - runMode.mockImplementation(() => 'production'); - expect(Utils.getLocalURLString('index.html')).toStrictEqual('file:///path/to/app/renderer/index.html'); - }); - - it('should include query string when specified', () => { - const queryMap = new Map([['key', 'value']]); - runMode.mockImplementation(() => 'development'); - expect(Utils.getLocalURLString('index.html', queryMap)).toStrictEqual('file:///path/to/app/dist/renderer/index.html?key=value'); - }); - - it('should return URL relative to current run directory when using main process', () => { - runMode.mockImplementation(() => 'development'); - expect(Utils.getLocalURLString('index.html', null, true)).toStrictEqual('file:///path/to/app/dist/index.html'); - }); - }); - describe('shouldHaveBackBar', () => { it('should have back bar for custom logins', () => { expect(Utils.shouldHaveBackBar(new URL('https://server-1.com'), new URL('https://server-1.com/login/sso/saml'))).toBe(true); diff --git a/src/main/utils.ts b/src/main/utils.ts index 5fcfa148..ea6b29dd 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -11,9 +11,8 @@ const exec = promisify(execOriginal); import type {BrowserWindow} from 'electron'; import {app} from 'electron'; -import {BACK_BAR_HEIGHT, customLoginRegexPaths, PRODUCTION, TAB_BAR_HEIGHT} from 'common/utils/constants'; +import {BACK_BAR_HEIGHT, customLoginRegexPaths, TAB_BAR_HEIGHT} from 'common/utils/constants'; import {isAdminUrl, isPluginUrl, isTeamUrl, isUrlType, parseURL} from 'common/utils/url'; -import Utils from 'common/utils/util'; import type {Args} from 'types/args'; @@ -85,34 +84,8 @@ export function shouldHaveBackBar(serverUrl: URL, inputURL: URL) { return !isTeamUrl(serverUrl, inputURL) && !isAdminUrl(serverUrl, inputURL) && !isPluginUrl(serverUrl, inputURL); } -export function getLocalURLString(urlPath: string, query?: Map, isMain?: boolean) { - let pathname; - const processPath = isMain ? '' : '/renderer'; - const mode = Utils.runMode(); - const protocol = 'file'; - const hostname = ''; - const port = ''; - if (mode === PRODUCTION) { - pathname = path.join(app.getAppPath(), `${processPath}/${urlPath}`); - } else { - pathname = path.resolve(__dirname, `../../dist/${processPath}/${urlPath}`); // TODO: find a better way to work with webpack on this - } - const localUrl = new URL(`${protocol}://${hostname}${port}`); - localUrl.pathname = pathname; - if (query) { - query.forEach((value: string, key: string) => { - localUrl.searchParams.append(encodeURIComponent(key), encodeURIComponent(value)); - }); - } - - return localUrl.href; -} - export function getLocalPreload(file: string) { - if (Utils.runMode() === PRODUCTION) { - return path.join(app.getAppPath(), `${file}`); - } - return path.resolve(__dirname, `../../dist/${file}`); + return path.join(app.getAppPath(), file); } export function composeUserAgent() { diff --git a/src/main/views/downloadsDropdownMenuView.test.js b/src/main/views/downloadsDropdownMenuView.test.js index a3297d90..cec506f7 100644 --- a/src/main/views/downloadsDropdownMenuView.test.js +++ b/src/main/views/downloadsDropdownMenuView.test.js @@ -12,7 +12,6 @@ import {DownloadsDropdownMenuView} from './downloadsDropdownMenuView'; jest.mock('main/utils', () => ({ getLocalPreload: (file) => file, - getLocalURLString: (file) => file, })); jest.mock('electron', () => { class NotificationMock { diff --git a/src/main/views/downloadsDropdownMenuView.ts b/src/main/views/downloadsDropdownMenuView.ts index b95c8a0b..1b4aed78 100644 --- a/src/main/views/downloadsDropdownMenuView.ts +++ b/src/main/views/downloadsDropdownMenuView.ts @@ -27,7 +27,7 @@ import { TAB_BAR_HEIGHT, } from 'common/utils/constants'; import downloadsManager from 'main/downloadsManager'; -import {getLocalPreload, getLocalURLString} from 'main/utils'; +import {getLocalPreload} from 'main/utils'; import MainWindow from 'main/windows/mainWindow'; import type {CoordinatesToJsonType, DownloadedItem, DownloadsMenuOpenEventPayload} from 'types/downloads'; @@ -75,7 +75,7 @@ export class DownloadsDropdownMenuView { // @ts-ignore transparent: true, }}); - this.view.webContents.loadURL(getLocalURLString('downloadsDropdownMenu.html')); + this.view.webContents.loadURL('mattermost-desktop://renderer/downloadsDropdownMenu.html'); MainWindow.get()?.addBrowserView(this.view); }; diff --git a/src/main/views/downloadsDropdownView.test.js b/src/main/views/downloadsDropdownView.test.js index 900aa9af..a4278887 100644 --- a/src/main/views/downloadsDropdownView.test.js +++ b/src/main/views/downloadsDropdownView.test.js @@ -12,7 +12,6 @@ import {DownloadsDropdownView} from './downloadsDropdownView'; jest.mock('main/utils', () => ({ getLocalPreload: (file) => file, - getLocalURLString: (file) => file, })); jest.mock('fs', () => ({ existsSync: jest.fn().mockReturnValue(false), diff --git a/src/main/views/downloadsDropdownView.ts b/src/main/views/downloadsDropdownView.ts index 552711ac..1b771514 100644 --- a/src/main/views/downloadsDropdownView.ts +++ b/src/main/views/downloadsDropdownView.ts @@ -1,7 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import type {IpcMainEvent, IpcMainInvokeEvent} from 'electron'; +import type {IpcMainEvent} from 'electron'; import {BrowserView, ipcMain} from 'electron'; import { @@ -13,7 +13,6 @@ import { REQUEST_DOWNLOADS_DROPDOWN_INFO, UPDATE_DOWNLOADS_DROPDOWN, UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM, - GET_DOWNLOADED_IMAGE_THUMBNAIL_LOCATION, DOWNLOADS_DROPDOWN_OPEN_FILE, MAIN_WINDOW_CREATED, MAIN_WINDOW_RESIZED, @@ -22,7 +21,7 @@ import Config from 'common/config'; import {Logger} from 'common/log'; import {TAB_BAR_HEIGHT, DOWNLOADS_DROPDOWN_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT, DOWNLOADS_DROPDOWN_FULL_WIDTH} from 'common/utils/constants'; import downloadsManager from 'main/downloadsManager'; -import {getLocalPreload, getLocalURLString} from 'main/utils'; +import {getLocalPreload} from 'main/utils'; import MainWindow from 'main/windows/mainWindow'; import type {DownloadedItem} from 'types/downloads'; @@ -47,7 +46,6 @@ export class DownloadsDropdownView { ipcMain.on(DOWNLOADS_DROPDOWN_OPEN_FILE, this.openFile); ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN, this.updateDownloadsDropdown); ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM, this.updateDownloadsDropdownMenuItem); - ipcMain.handle(GET_DOWNLOADED_IMAGE_THUMBNAIL_LOCATION, this.getDownloadImageThumbnailLocation); } init = () => { @@ -67,7 +65,7 @@ export class DownloadsDropdownView { transparent: true, }}); - this.view.webContents.loadURL(getLocalURLString('downloadsDropdown.html')); + this.view.webContents.loadURL('mattermost-desktop://renderer/downloadsDropdown.html'); this.view.webContents.session.webRequest.onHeadersReceived(downloadsManager.webRequestOnHeadersReceivedHandler); MainWindow.get()?.addBrowserView(this.view); }; @@ -183,10 +181,6 @@ export class DownloadsDropdownView { this.view?.setBounds(this.bounds); } }; - - private getDownloadImageThumbnailLocation = (event: IpcMainInvokeEvent, location: string) => { - return location; - }; } const downloadsDropdownView = new DownloadsDropdownView(); diff --git a/src/main/views/loadingScreen.ts b/src/main/views/loadingScreen.ts index ec224977..74d6ad23 100644 --- a/src/main/views/loadingScreen.ts +++ b/src/main/views/loadingScreen.ts @@ -5,7 +5,7 @@ import {BrowserView, app, ipcMain} from 'electron'; import {DARK_MODE_CHANGE, LOADING_SCREEN_ANIMATION_FINISHED, MAIN_WINDOW_RESIZED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication'; import {Logger} from 'common/log'; -import {getLocalPreload, getLocalURLString, getWindowBoundaries} from 'main/utils'; +import {getLocalPreload, getWindowBoundaries} from 'main/utils'; import MainWindow from 'main/windows/mainWindow'; enum LoadingScreenState { @@ -85,7 +85,7 @@ export class LoadingScreen { // @ts-ignore transparent: true, }}); - const localURL = getLocalURLString('loadingScreen.html'); + const localURL = 'mattermost-desktop://renderer/loadingScreen.html'; this.view.webContents.loadURL(localURL); }; diff --git a/src/main/views/serverDropdownView.test.js b/src/main/views/serverDropdownView.test.js index 11253417..91d88890 100644 --- a/src/main/views/serverDropdownView.test.js +++ b/src/main/views/serverDropdownView.test.js @@ -12,7 +12,6 @@ jest.mock('app/serverViewState', () => ({})); jest.mock('main/utils', () => ({ getLocalPreload: (file) => file, - getLocalURLString: (file) => file, })); jest.mock('electron', () => ({ diff --git a/src/main/views/serverDropdownView.ts b/src/main/views/serverDropdownView.ts index bce477b7..345ab1b0 100644 --- a/src/main/views/serverDropdownView.ts +++ b/src/main/views/serverDropdownView.ts @@ -22,7 +22,7 @@ import Config from 'common/config'; import {Logger} from 'common/log'; import ServerManager from 'common/servers/serverManager'; import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHADOW_WIDTH} from 'common/utils/constants'; -import {getLocalPreload, getLocalURLString} from 'main/utils'; +import {getLocalPreload} from 'main/utils'; import type {UniqueServer} from 'types/config'; @@ -83,7 +83,7 @@ export class ServerDropdownView { // @ts-ignore transparent: true, }}); - this.view.webContents.loadURL(getLocalURLString('dropdown.html')); + this.view.webContents.loadURL('mattermost-desktop://renderer/dropdown.html'); this.setOrderedServers(); this.windowBounds = MainWindow.getBounds(); diff --git a/src/main/views/viewManager.ts b/src/main/views/viewManager.ts index 936e5afd..7e173d62 100644 --- a/src/main/views/viewManager.ts +++ b/src/main/views/viewManager.ts @@ -52,7 +52,7 @@ import LoadingScreen from './loadingScreen'; import {MattermostBrowserView} from './MattermostBrowserView'; import modalManager from './modalManager'; -import {getLocalURLString, getLocalPreload, getAdjustedWindowBoundaries, shouldHaveBackBar} from '../utils'; +import {getLocalPreload, getAdjustedWindowBoundaries, shouldHaveBackBar} from '../utils'; const log = new Logger('ViewManager'); const URL_VIEW_DURATION = 10 * SECOND; @@ -354,8 +354,7 @@ export class ViewManager { // @ts-ignore transparent: true, }}); - const query = new Map([['url', urlString]]); - const localURL = getLocalURLString('urlView.html', query); + const localURL = `mattermost-desktop://renderer/urlView.html?url=${encodeURIComponent(urlString)}`; urlView.webContents.loadURL(localURL); MainWindow.get()?.addBrowserView(urlView); const boundaries = this.views.get(this.currentView || '')?.getBounds() ?? MainWindow.getBounds(); diff --git a/src/main/windows/mainWindow.test.js b/src/main/windows/mainWindow.test.js index df489479..059b15a6 100644 --- a/src/main/windows/mainWindow.test.js +++ b/src/main/windows/mainWindow.test.js @@ -68,7 +68,6 @@ jest.mock('../contextMenu', () => jest.fn()); jest.mock('../utils', () => ({ isInsideRectangle: jest.fn(), getLocalPreload: jest.fn(), - getLocalURLString: jest.fn(), })); jest.mock('main/i18nManager', () => ({ diff --git a/src/main/windows/mainWindow.ts b/src/main/windows/mainWindow.ts index 7c59ceef..2fae5556 100644 --- a/src/main/windows/mainWindow.ts +++ b/src/main/windows/mainWindow.ts @@ -37,7 +37,7 @@ import {localizeMessage} from 'main/i18nManager'; import type {SavedWindowState} from 'types/mainWindow'; import ContextMenu from '../contextMenu'; -import {getLocalPreload, getLocalURLString, isInsideRectangle} from '../utils'; +import {getLocalPreload, isInsideRectangle} from '../utils'; const log = new Logger('MainWindow'); const ALT_MENU_KEYS = ['Alt+F', 'Alt+E', 'Alt+V', 'Alt+H', 'Alt+W', 'Alt+P']; @@ -152,7 +152,7 @@ export class MainWindow extends EventEmitter { const contextMenu = new ContextMenu({}, this.win); contextMenu.reload(); - const localURL = getLocalURLString('index.html'); + const localURL = 'mattermost-desktop://renderer/index.html'; this.win.loadURL(localURL).catch( (reason) => { log.error('failed to load', reason); diff --git a/src/assets/loader/StippleMask.jpg b/src/renderer/assets/StippleMask.jpg similarity index 100% rename from src/assets/loader/StippleMask.jpg rename to src/renderer/assets/StippleMask.jpg diff --git a/src/renderer/components/DownloadsDropdown/Thumbnail.tsx b/src/renderer/components/DownloadsDropdown/Thumbnail.tsx index ed651f1b..b908230f 100644 --- a/src/renderer/components/DownloadsDropdown/Thumbnail.tsx +++ b/src/renderer/components/DownloadsDropdown/Thumbnail.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useEffect, useState} from 'react'; +import React from 'react'; import {CheckCircleIcon, CloseCircleIcon} from '@mattermost/compass-icons/components'; @@ -17,11 +17,7 @@ const iconSize = 14; const colorGreen = '#3DB887'; const colorRed = '#D24B4E'; -const isWin = window.process.platform === 'win32'; - const Thumbnail = ({item}: OwnProps) => { - const [imageUrl, setImageUrl] = useState(); - const showBadge = (state: DownloadedItem['state']) => { switch (state) { case 'completed': @@ -45,27 +41,18 @@ const Thumbnail = ({item}: OwnProps) => { } }; - useEffect(() => { - const fetchThumbnail = async () => { - const imageUrl = await window.mas.getThumbnailLocation(item.location); - setImageUrl(imageUrl); - }; - - fetchThumbnail(); - }, [item]); - const showImagePreview = isImageFile(item) && item.state === 'completed'; - if (showImagePreview && !imageUrl) { + if (showImagePreview && !item.thumbnailData) { return null; } return (
- {showImagePreview && imageUrl ? + {showImagePreview && item.thumbnailData ?
: diff --git a/src/renderer/components/LoadingScreen/LoadingBackground.tsx b/src/renderer/components/LoadingScreen/LoadingBackground.tsx index 3a99dedf..393dc0ea 100644 --- a/src/renderer/components/LoadingScreen/LoadingBackground.tsx +++ b/src/renderer/components/LoadingScreen/LoadingBackground.tsx @@ -3,6 +3,8 @@ import React from 'react'; +import StippleMask from 'renderer/assets/StippleMask.jpg'; + function LoadingBackground() { return (
@@ -36,7 +38,7 @@ function LoadingBackground() { - + <%= htmlWebpackPlugin.options.title %> diff --git a/src/types/downloads.ts b/src/types/downloads.ts index c69dce6d..b5b9b608 100644 --- a/src/types/downloads.ts +++ b/src/types/downloads.ts @@ -18,6 +18,7 @@ export type DownloadedItem = { receivedBytes: number; totalBytes: number; bookmark?: string; + thumbnailData?: string; } export type DownloadedItems = Record; diff --git a/src/types/external/file-types.d.ts b/src/types/external/file-types.d.ts index 3bb48df7..bfac6698 100644 --- a/src/types/external/file-types.d.ts +++ b/src/types/external/file-types.d.ts @@ -4,3 +4,4 @@ declare module '*.mp3'; declare module '*.svg'; declare module '*.lazy.css'; +declare module '*.jpg'; diff --git a/src/types/window.ts b/src/types/window.ts index e56dd817..bbeeae09 100644 --- a/src/types/window.ts +++ b/src/types/window.ts @@ -27,9 +27,6 @@ declare global { timers: { setImmediate: typeof setImmediate; }; - mas: { - getThumbnailLocation: (location: string) => Promise; - }; desktop: { quit: (reason: string, stack: string) => void; openAppMenu: () => void; diff --git a/webpack.config.renderer.js b/webpack.config.renderer.js index aab7c1f3..1181243a 100644 --- a/webpack.config.renderer.js +++ b/webpack.config.renderer.js @@ -158,7 +158,7 @@ module.exports = merge(base, { test: /\.mp3$/, type: 'asset/inline', }, { - test: /\.(svg|gif)$/, + test: /\.(svg|gif|jpg)$/, type: 'asset/resource', }, { test: /\.(eot|ttf|woff|woff2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,