[MM-59543] Disallow use of file: protocol in the app, remove all references to it, add mattermost-desktop: protocol to read local files (#3095)
This commit is contained in:
parent
87b2f12663
commit
080e4bf727
8
package-lock.json
generated
8
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({});
|
||||
|
||||
|
|
|
@ -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<null, Server>(
|
||||
'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<UniqueServerWithPermissions, {server: Server; permissions: Permissions}>(
|
||||
'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<string, boolean>(
|
||||
'removeServer',
|
||||
getLocalURLString('removeServer.html'),
|
||||
'mattermost-desktop://renderer/removeServer.html',
|
||||
getLocalPreload('internalAPI.js'),
|
||||
server.name,
|
||||
mainWindow,
|
||||
|
|
|
@ -60,6 +60,7 @@ const downloadsSchema = Joi.object<DownloadedItems>().pattern(
|
|||
receivedBytes: Joi.number().min(0),
|
||||
totalBytes: Joi.number().min(0),
|
||||
bookmark: Joi.string(),
|
||||
thumbnailData: Joi.string(),
|
||||
});
|
||||
|
||||
const configDataSchemaV0 = Joi.object<ConfigV0>({
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -69,6 +69,10 @@ jest.mock('electron', () => ({
|
|||
on: jest.fn(),
|
||||
},
|
||||
},
|
||||
protocol: {
|
||||
registerSchemesAsPrivileged: jest.fn(),
|
||||
handle: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
|
|
|
@ -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[]) => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -51,7 +51,6 @@ jest.mock('main/views/modalManager', () => ({
|
|||
|
||||
jest.mock('main/utils', () => ({
|
||||
getLocalPreload: (file) => file,
|
||||
getLocalURLString: (file) => file,
|
||||
}));
|
||||
|
||||
describe('main/authManager', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -16,7 +16,6 @@ jest.mock('main/views/modalManager', () => ({
|
|||
|
||||
jest.mock('main/utils', () => ({
|
||||
getLocalPreload: (file) => file,
|
||||
getLocalURLString: (file) => file,
|
||||
}));
|
||||
|
||||
describe('main/certificateManager', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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<DownloadedItems> {
|
|||
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<DownloadedItems> {
|
|||
}
|
||||
};
|
||||
|
||||
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<DownloadedItems> {
|
|||
/**
|
||||
* 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<DownloadedItems> {
|
|||
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<DownloadedItems> {
|
|||
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<DownloadedItems> {
|
|||
/**
|
||||
* Internal utils
|
||||
*/
|
||||
private formatDownloadItem = (item: DownloadItem, state: DownloadItemState, overridePath?: string): DownloadedItem => {
|
||||
private formatDownloadItem = async (item: DownloadItem, state: DownloadItemState, overridePath?: string): Promise<DownloadedItem> => {
|
||||
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<DownloadedItems> {
|
|||
totalBytes,
|
||||
type: DownloadItemTypeEnum.FILE,
|
||||
bookmark: this.getBookmark(item),
|
||||
thumbnailData,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<string, string>, 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() {
|
||||
|
|
|
@ -12,7 +12,6 @@ import {DownloadsDropdownMenuView} from './downloadsDropdownMenuView';
|
|||
|
||||
jest.mock('main/utils', () => ({
|
||||
getLocalPreload: (file) => file,
|
||||
getLocalURLString: (file) => file,
|
||||
}));
|
||||
jest.mock('electron', () => {
|
||||
class NotificationMock {
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ jest.mock('app/serverViewState', () => ({}));
|
|||
|
||||
jest.mock('main/utils', () => ({
|
||||
getLocalPreload: (file) => file,
|
||||
getLocalURLString: (file) => file,
|
||||
}));
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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', () => ({
|
||||
|
|
|
@ -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);
|
||||
|
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
@ -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<string | undefined>();
|
||||
|
||||
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 (
|
||||
<div className='DownloadsDropdown__Thumbnail__Container'>
|
||||
{showImagePreview && imageUrl ?
|
||||
{showImagePreview && item.thumbnailData ?
|
||||
<div
|
||||
className='DownloadsDropdown__Thumbnail preview'
|
||||
style={{
|
||||
backgroundImage: `url("${isWin ? `file:///${imageUrl.replaceAll('\\', '/')}` : imageUrl}")`,
|
||||
backgroundImage: `url("${item.thumbnailData}")`,
|
||||
backgroundSize: 'cover',
|
||||
}}
|
||||
/> :
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import StippleMask from 'renderer/assets/StippleMask.jpg';
|
||||
|
||||
function LoadingBackground() {
|
||||
return (
|
||||
<div className='LoadingScreen__backgound'>
|
||||
|
@ -36,7 +38,7 @@ function LoadingBackground() {
|
|||
<image
|
||||
width='900'
|
||||
height='535'
|
||||
href='../assets/loader/StippleMask.jpg'
|
||||
href={StippleMask}
|
||||
/>
|
||||
</mask>
|
||||
<g
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; media-src data:">
|
||||
<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>
|
||||
|
|
|
@ -18,6 +18,7 @@ export type DownloadedItem = {
|
|||
receivedBytes: number;
|
||||
totalBytes: number;
|
||||
bookmark?: string;
|
||||
thumbnailData?: string;
|
||||
}
|
||||
|
||||
export type DownloadedItems = Record<string, DownloadedItem>;
|
||||
|
|
1
src/types/external/file-types.d.ts
vendored
1
src/types/external/file-types.d.ts
vendored
|
@ -4,3 +4,4 @@
|
|||
declare module '*.mp3';
|
||||
declare module '*.svg';
|
||||
declare module '*.lazy.css';
|
||||
declare module '*.jpg';
|
||||
|
|
|
@ -27,9 +27,6 @@ declare global {
|
|||
timers: {
|
||||
setImmediate: typeof setImmediate;
|
||||
};
|
||||
mas: {
|
||||
getThumbnailLocation: (location: string) => Promise<string>;
|
||||
};
|
||||
desktop: {
|
||||
quit: (reason: string, stack: string) => void;
|
||||
openAppMenu: () => void;
|
||||
|
|
|
@ -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])?$/,
|
||||
|
|
Loading…
Reference in a new issue