Get rid of `electron.remote`
This commit is contained in:
parent
246583d274
commit
76d8b5e375
|
@ -3299,30 +3299,6 @@ Signal Desktop makes use of the following open source projects.
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
## tmp
|
|
||||||
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2014 KARASZI István
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
||||||
## typeface-inter
|
## typeface-inter
|
||||||
|
|
||||||
Copyright (c) 2016-2018 The Inter Project Authors (me@rsms.me)
|
Copyright (c) 2016-2018 The Inter Project Authors (me@rsms.me)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
getStickersPath,
|
getStickersPath,
|
||||||
getTempPath,
|
getTempPath,
|
||||||
getDraftPath,
|
getDraftPath,
|
||||||
} from './attachments';
|
} from '../ts/util/attachments';
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
|
|
|
@ -1,43 +1,24 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
// Copyright 2018-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { randomBytes } from 'crypto';
|
import { join, relative } from 'path';
|
||||||
import { basename, join, normalize, relative } from 'path';
|
|
||||||
import { app, dialog, shell, remote } from 'electron';
|
|
||||||
|
|
||||||
import fastGlob from 'fast-glob';
|
import fastGlob from 'fast-glob';
|
||||||
import glob from 'glob';
|
import glob from 'glob';
|
||||||
import pify from 'pify';
|
import pify from 'pify';
|
||||||
import fse from 'fs-extra';
|
import fse from 'fs-extra';
|
||||||
import { map, isTypedArray, isString } from 'lodash';
|
import { map } from 'lodash';
|
||||||
import normalizePath from 'normalize-path';
|
import normalizePath from 'normalize-path';
|
||||||
import getGuid from 'uuid/v4';
|
|
||||||
|
|
||||||
import { isPathInside } from '../ts/util/isPathInside';
|
import {
|
||||||
import { isWindows } from '../ts/OS';
|
getPath,
|
||||||
import { writeWindowsZoneIdentifier } from '../ts/util/windowsZoneIdentifier';
|
getStickersPath,
|
||||||
|
getDraftPath,
|
||||||
|
getTempPath,
|
||||||
|
createDeleter,
|
||||||
|
} from '../ts/util/attachments';
|
||||||
|
|
||||||
type FSAttrType = {
|
export * from '../ts/util/attachments';
|
||||||
set: (path: string, attribute: string, value: string) => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
let xattr: FSAttrType | undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
// eslint-disable-next-line global-require, import/no-extraneous-dependencies, import/no-unresolved
|
|
||||||
xattr = require('fs-xattr');
|
|
||||||
} catch (e) {
|
|
||||||
console.log('x-attr dependency did not load successfully');
|
|
||||||
}
|
|
||||||
|
|
||||||
const PATH = 'attachments.noindex';
|
|
||||||
const AVATAR_PATH = 'avatars.noindex';
|
|
||||||
const STICKER_PATH = 'stickers.noindex';
|
|
||||||
const TEMP_PATH = 'temp';
|
|
||||||
const DRAFT_PATH = 'drafts.noindex';
|
|
||||||
|
|
||||||
const getApp = () => app || remote.app;
|
|
||||||
|
|
||||||
export const getAllAttachments = async (
|
export const getAllAttachments = async (
|
||||||
userDataPath: string
|
userDataPath: string
|
||||||
|
@ -79,274 +60,11 @@ export const getBuiltInImages = async (): Promise<ReadonlyArray<string>> => {
|
||||||
return map(files, file => relative(dir, file));
|
return map(files, file => relative(dir, file));
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPathGetter = (subpath: string) => (
|
|
||||||
userDataPath: string
|
|
||||||
): string => {
|
|
||||||
if (!isString(userDataPath)) {
|
|
||||||
throw new TypeError("'userDataPath' must be a string");
|
|
||||||
}
|
|
||||||
return join(userDataPath, subpath);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAvatarsPath = createPathGetter(AVATAR_PATH);
|
|
||||||
export const getDraftPath = createPathGetter(DRAFT_PATH);
|
|
||||||
export const getPath = createPathGetter(PATH);
|
|
||||||
export const getStickersPath = createPathGetter(STICKER_PATH);
|
|
||||||
export const getTempPath = createPathGetter(TEMP_PATH);
|
|
||||||
|
|
||||||
export const clearTempPath = (userDataPath: string): Promise<void> => {
|
export const clearTempPath = (userDataPath: string): Promise<void> => {
|
||||||
const tempPath = getTempPath(userDataPath);
|
const tempPath = getTempPath(userDataPath);
|
||||||
return fse.emptyDir(tempPath);
|
return fse.emptyDir(tempPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createReader = (
|
|
||||||
root: string
|
|
||||||
): ((relativePath: string) => Promise<Uint8Array>) => {
|
|
||||||
if (!isString(root)) {
|
|
||||||
throw new TypeError("'root' must be a path");
|
|
||||||
}
|
|
||||||
|
|
||||||
return async (relativePath: string): Promise<Uint8Array> => {
|
|
||||||
if (!isString(relativePath)) {
|
|
||||||
throw new TypeError("'relativePath' must be a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
const absolutePath = join(root, relativePath);
|
|
||||||
const normalized = normalize(absolutePath);
|
|
||||||
if (!isPathInside(normalized, root)) {
|
|
||||||
throw new Error('Invalid relative path');
|
|
||||||
}
|
|
||||||
return fse.readFile(normalized);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createDoesExist = (
|
|
||||||
root: string
|
|
||||||
): ((relativePath: string) => Promise<boolean>) => {
|
|
||||||
if (!isString(root)) {
|
|
||||||
throw new TypeError("'root' must be a path");
|
|
||||||
}
|
|
||||||
|
|
||||||
return async (relativePath: string): Promise<boolean> => {
|
|
||||||
if (!isString(relativePath)) {
|
|
||||||
throw new TypeError("'relativePath' must be a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
const absolutePath = join(root, relativePath);
|
|
||||||
const normalized = normalize(absolutePath);
|
|
||||||
if (!isPathInside(normalized, root)) {
|
|
||||||
throw new Error('Invalid relative path');
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await fse.access(normalized, fse.constants.F_OK);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const copyIntoAttachmentsDirectory = (
|
|
||||||
root: string
|
|
||||||
): ((sourcePath: string) => Promise<{ path: string; size: number }>) => {
|
|
||||||
if (!isString(root)) {
|
|
||||||
throw new TypeError("'root' must be a path");
|
|
||||||
}
|
|
||||||
|
|
||||||
const userDataPath = getApp().getPath('userData');
|
|
||||||
|
|
||||||
return async (
|
|
||||||
sourcePath: string
|
|
||||||
): Promise<{ path: string; size: number }> => {
|
|
||||||
if (!isString(sourcePath)) {
|
|
||||||
throw new TypeError('sourcePath must be a string');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isPathInside(sourcePath, userDataPath)) {
|
|
||||||
throw new Error(
|
|
||||||
"'sourcePath' must be relative to the user config directory"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = createName();
|
|
||||||
const relativePath = getRelativePath(name);
|
|
||||||
const absolutePath = join(root, relativePath);
|
|
||||||
const normalized = normalize(absolutePath);
|
|
||||||
if (!isPathInside(normalized, root)) {
|
|
||||||
throw new Error('Invalid relative path');
|
|
||||||
}
|
|
||||||
|
|
||||||
await fse.ensureFile(normalized);
|
|
||||||
await fse.copy(sourcePath, normalized);
|
|
||||||
const { size } = await fse.stat(normalized);
|
|
||||||
|
|
||||||
return {
|
|
||||||
path: relativePath,
|
|
||||||
size,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
async function writeWithAttributes(
|
|
||||||
target: string,
|
|
||||||
data: Uint8Array
|
|
||||||
): Promise<void> {
|
|
||||||
await fse.writeFile(target, Buffer.from(data));
|
|
||||||
|
|
||||||
if (process.platform === 'darwin' && xattr) {
|
|
||||||
// kLSQuarantineTypeInstantMessageAttachment
|
|
||||||
const type = '0003';
|
|
||||||
|
|
||||||
// Hexadecimal seconds since epoch
|
|
||||||
const timestamp = Math.trunc(Date.now() / 1000).toString(16);
|
|
||||||
|
|
||||||
const appName = 'Signal';
|
|
||||||
const guid = getGuid();
|
|
||||||
|
|
||||||
// https://ilostmynotes.blogspot.com/2012/06/gatekeeper-xprotect-and-quarantine.html
|
|
||||||
const attrValue = `${type};${timestamp};${appName};${guid}`;
|
|
||||||
|
|
||||||
await xattr.set(target, 'com.apple.quarantine', attrValue);
|
|
||||||
} else if (isWindows()) {
|
|
||||||
// This operation may fail (see the function's comments), which is not a show-stopper.
|
|
||||||
try {
|
|
||||||
await writeWindowsZoneIdentifier(target);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('Failed to write Windows Zone.Identifier file; continuing');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openFileInDownloads = async (name: string): Promise<void> => {
|
|
||||||
const shellToUse = shell || remote.shell;
|
|
||||||
const appToUse = getApp();
|
|
||||||
|
|
||||||
const downloadsPath =
|
|
||||||
appToUse.getPath('downloads') || appToUse.getPath('home');
|
|
||||||
const target = join(downloadsPath, name);
|
|
||||||
|
|
||||||
const normalized = normalize(target);
|
|
||||||
if (!isPathInside(normalized, downloadsPath)) {
|
|
||||||
throw new Error('Invalid filename!');
|
|
||||||
}
|
|
||||||
|
|
||||||
shellToUse.showItemInFolder(normalized);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const saveAttachmentToDisk = async ({
|
|
||||||
data,
|
|
||||||
name,
|
|
||||||
}: {
|
|
||||||
data: Uint8Array;
|
|
||||||
name: string;
|
|
||||||
}): Promise<null | { fullPath: string; name: string }> => {
|
|
||||||
const dialogToUse = dialog || remote.dialog;
|
|
||||||
const browserWindow = remote.getCurrentWindow();
|
|
||||||
|
|
||||||
const { canceled, filePath } = await dialogToUse.showSaveDialog(
|
|
||||||
browserWindow,
|
|
||||||
{
|
|
||||||
defaultPath: name,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (canceled || !filePath) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeWithAttributes(filePath, data);
|
|
||||||
|
|
||||||
const fileBasename = basename(filePath);
|
|
||||||
|
|
||||||
return {
|
|
||||||
fullPath: filePath,
|
|
||||||
name: fileBasename,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const openFileInFolder = async (target: string): Promise<void> => {
|
|
||||||
const shellToUse = shell || remote.shell;
|
|
||||||
|
|
||||||
shellToUse.showItemInFolder(target);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createWriterForNew = (
|
|
||||||
root: string
|
|
||||||
): ((bytes: Uint8Array) => Promise<string>) => {
|
|
||||||
if (!isString(root)) {
|
|
||||||
throw new TypeError("'root' must be a path");
|
|
||||||
}
|
|
||||||
|
|
||||||
return async (bytes: Uint8Array) => {
|
|
||||||
if (!isTypedArray(bytes)) {
|
|
||||||
throw new TypeError("'bytes' must be a typed array");
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = createName();
|
|
||||||
const relativePath = getRelativePath(name);
|
|
||||||
return createWriterForExisting(root)({
|
|
||||||
data: bytes,
|
|
||||||
path: relativePath,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createWriterForExisting = (
|
|
||||||
root: string
|
|
||||||
): ((options: { data: Uint8Array; path: string }) => Promise<string>) => {
|
|
||||||
if (!isString(root)) {
|
|
||||||
throw new TypeError("'root' must be a path");
|
|
||||||
}
|
|
||||||
|
|
||||||
return async ({
|
|
||||||
data: bytes,
|
|
||||||
path: relativePath,
|
|
||||||
}: {
|
|
||||||
data: Uint8Array;
|
|
||||||
path: string;
|
|
||||||
}): Promise<string> => {
|
|
||||||
if (!isString(relativePath)) {
|
|
||||||
throw new TypeError("'relativePath' must be a path");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isTypedArray(bytes)) {
|
|
||||||
throw new TypeError("'arrayBuffer' must be an array buffer");
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = Buffer.from(bytes);
|
|
||||||
const absolutePath = join(root, relativePath);
|
|
||||||
const normalized = normalize(absolutePath);
|
|
||||||
if (!isPathInside(normalized, root)) {
|
|
||||||
throw new Error('Invalid relative path');
|
|
||||||
}
|
|
||||||
|
|
||||||
await fse.ensureFile(normalized);
|
|
||||||
await fse.writeFile(normalized, buffer);
|
|
||||||
return relativePath;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createDeleter = (
|
|
||||||
root: string
|
|
||||||
): ((relativePath: string) => Promise<void>) => {
|
|
||||||
if (!isString(root)) {
|
|
||||||
throw new TypeError("'root' must be a path");
|
|
||||||
}
|
|
||||||
|
|
||||||
return async (relativePath: string): Promise<void> => {
|
|
||||||
if (!isString(relativePath)) {
|
|
||||||
throw new TypeError("'relativePath' must be a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
const absolutePath = join(root, relativePath);
|
|
||||||
const normalized = normalize(absolutePath);
|
|
||||||
if (!isPathInside(normalized, root)) {
|
|
||||||
throw new Error('Invalid relative path');
|
|
||||||
}
|
|
||||||
await fse.remove(absolutePath);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteAll = async ({
|
export const deleteAll = async ({
|
||||||
userDataPath,
|
userDataPath,
|
||||||
attachments,
|
attachments,
|
||||||
|
@ -400,28 +118,3 @@ export const deleteAllDraftAttachments = async ({
|
||||||
|
|
||||||
console.log(`deleteAllDraftAttachments: deleted ${attachments.length} files`);
|
console.log(`deleteAllDraftAttachments: deleted ${attachments.length} files`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createName = (): string => {
|
|
||||||
const buffer = randomBytes(32);
|
|
||||||
return buffer.toString('hex');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getRelativePath = (name: string): string => {
|
|
||||||
if (!isString(name)) {
|
|
||||||
throw new TypeError("'name' must be a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefix = name.slice(0, 2);
|
|
||||||
return join(prefix, name);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createAbsolutePathGetter = (rootPath: string) => (
|
|
||||||
relativePath: string
|
|
||||||
): string => {
|
|
||||||
const absolutePath = join(rootPath, relativePath);
|
|
||||||
const normalized = normalize(absolutePath);
|
|
||||||
if (!isPathInside(normalized, rootPath)) {
|
|
||||||
throw new Error('Invalid relative path');
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
};
|
|
||||||
|
|
31
app/main.ts
31
app/main.ts
|
@ -310,6 +310,9 @@ function prepareUrl(
|
||||||
serverPublicParams: config.get<string>('serverPublicParams'),
|
serverPublicParams: config.get<string>('serverPublicParams'),
|
||||||
serverTrustRoot: config.get<string>('serverTrustRoot'),
|
serverTrustRoot: config.get<string>('serverTrustRoot'),
|
||||||
appStartInitialSpellcheckSetting,
|
appStartInitialSpellcheckSetting,
|
||||||
|
userDataPath: app.getPath('userData'),
|
||||||
|
downloadsPath: app.getPath('downloads'),
|
||||||
|
homePath: app.getPath('home'),
|
||||||
...moreKeys,
|
...moreKeys,
|
||||||
}).href;
|
}).href;
|
||||||
}
|
}
|
||||||
|
@ -2063,3 +2066,31 @@ async function ensureFilePermissions(onlyFiles?: Array<string>) {
|
||||||
|
|
||||||
getLogger().info(`Finish ensuring permissions in ${Date.now() - start}ms`);
|
getLogger().info(`Finish ensuring permissions in ${Date.now() - start}ms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ipc.handle('get-auto-launch', async () => {
|
||||||
|
return app.getLoginItemSettings().openAtLogin;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipc.handle('set-auto-launch', async (_event, value) => {
|
||||||
|
app.setLoginItemSettings({ openAtLogin: Boolean(value) });
|
||||||
|
});
|
||||||
|
|
||||||
|
ipc.on('show-message-box', (_event, { type, message }) => {
|
||||||
|
dialog.showMessageBox({ type, message });
|
||||||
|
});
|
||||||
|
|
||||||
|
ipc.on('show-item-in-folder', (_event, folder) => {
|
||||||
|
shell.showItemInFolder(folder);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipc.handle('show-save-dialog', async (_event, { defaultPath }) => {
|
||||||
|
if (!mainWindow) {
|
||||||
|
getLogger().warn('show-save-dialog: no main window');
|
||||||
|
|
||||||
|
return { canceled: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return dialog.showSaveDialog(mainWindow, {
|
||||||
|
defaultPath,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -17,7 +17,7 @@ const {
|
||||||
sample,
|
sample,
|
||||||
} = require('lodash');
|
} = require('lodash');
|
||||||
|
|
||||||
const Attachments = require('../../app/attachments');
|
const Attachments = require('../../ts/windows/attachments');
|
||||||
const Message = require('./types/message');
|
const Message = require('./types/message');
|
||||||
const { sleep } = require('../../ts/util/sleep');
|
const { sleep } = require('../../ts/util/sleep');
|
||||||
|
|
||||||
|
|
|
@ -161,7 +161,6 @@
|
||||||
"sharp": "0.28.1",
|
"sharp": "0.28.1",
|
||||||
"split2": "4.0.0",
|
"split2": "4.0.0",
|
||||||
"testcheck": "1.0.0-rc.2",
|
"testcheck": "1.0.0-rc.2",
|
||||||
"tmp": "0.0.33",
|
|
||||||
"typeface-inter": "3.10.0",
|
"typeface-inter": "3.10.0",
|
||||||
"underscore": "1.12.1",
|
"underscore": "1.12.1",
|
||||||
"uuid": "3.3.2",
|
"uuid": "3.3.2",
|
||||||
|
|
13
preload.js
13
preload.js
|
@ -21,9 +21,6 @@ try {
|
||||||
const { getEnvironment, Environment } = require('./ts/environment');
|
const { getEnvironment, Environment } = require('./ts/environment');
|
||||||
const ipc = electron.ipcRenderer;
|
const ipc = electron.ipcRenderer;
|
||||||
|
|
||||||
const { remote } = electron;
|
|
||||||
const { app } = remote;
|
|
||||||
|
|
||||||
const config = require('url').parse(window.location.toString(), true).query;
|
const config = require('url').parse(window.location.toString(), true).query;
|
||||||
|
|
||||||
const log = require('./ts/logging/log');
|
const log = require('./ts/logging/log');
|
||||||
|
@ -72,9 +69,11 @@ try {
|
||||||
window.getServerPublicParams = () => config.serverPublicParams;
|
window.getServerPublicParams = () => config.serverPublicParams;
|
||||||
window.getSfuUrl = () => config.sfuUrl;
|
window.getSfuUrl = () => config.sfuUrl;
|
||||||
window.isBehindProxy = () => Boolean(config.proxyUrl);
|
window.isBehindProxy = () => Boolean(config.proxyUrl);
|
||||||
window.getAutoLaunch = () => app.getLoginItemSettings().openAtLogin;
|
window.getAutoLaunch = () => {
|
||||||
|
return ipc.invoke('get-auto-launch');
|
||||||
|
};
|
||||||
window.setAutoLaunch = value => {
|
window.setAutoLaunch = value => {
|
||||||
app.setLoginItemSettings({ openAtLogin: Boolean(value) });
|
return ipc.invoke('set-auto-launch', value);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.isBeforeVersion = (toCheck, baseVersion) => {
|
window.isBeforeVersion = (toCheck, baseVersion) => {
|
||||||
|
@ -405,7 +404,7 @@ try {
|
||||||
window.PQueue = require('p-queue').default;
|
window.PQueue = require('p-queue').default;
|
||||||
|
|
||||||
const Signal = require('./js/modules/signal');
|
const Signal = require('./js/modules/signal');
|
||||||
const Attachments = require('./app/attachments');
|
const Attachments = require('./ts/windows/attachments');
|
||||||
|
|
||||||
const { locale } = config;
|
const { locale } = config;
|
||||||
window.i18n = SignalContext.i18n;
|
window.i18n = SignalContext.i18n;
|
||||||
|
@ -418,7 +417,7 @@ try {
|
||||||
});
|
});
|
||||||
window.moment.locale(locale);
|
window.moment.locale(locale);
|
||||||
|
|
||||||
const userDataPath = app.getPath('userData');
|
const userDataPath = SignalContext.getPath('userData');
|
||||||
window.baseAttachmentsPath = Attachments.getPath(userDataPath);
|
window.baseAttachmentsPath = Attachments.getPath(userDataPath);
|
||||||
window.baseStickersPath = Attachments.getStickersPath(userDataPath);
|
window.baseStickersPath = Attachments.getStickersPath(userDataPath);
|
||||||
window.baseTempPath = Attachments.getTempPath(userDataPath);
|
window.baseTempPath = Attachments.getTempPath(userDataPath);
|
||||||
|
|
|
@ -27,7 +27,6 @@ window.test = {
|
||||||
fastGlob,
|
fastGlob,
|
||||||
normalizePath: require('normalize-path'),
|
normalizePath: require('normalize-path'),
|
||||||
fse: require('fs-extra'),
|
fse: require('fs-extra'),
|
||||||
tmp: require('tmp'),
|
|
||||||
path: require('path'),
|
path: require('path'),
|
||||||
basePath: __dirname,
|
basePath: __dirname,
|
||||||
attachmentsPath: window.Signal.Migrations.attachmentsPath,
|
attachmentsPath: window.Signal.Migrations.attachmentsPath,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
/* global window */
|
/* global window */
|
||||||
const { ipcRenderer: ipc, remote } = require('electron');
|
const { ipcRenderer: ipc } = require('electron');
|
||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
const pify = require('pify');
|
const pify = require('pify');
|
||||||
const { readFile } = require('fs');
|
const { readFile } = require('fs');
|
||||||
|
@ -23,8 +23,6 @@ const { SignalService: Proto } = require('../ts/protobuf');
|
||||||
const { getEnvironment } = require('../ts/environment');
|
const { getEnvironment } = require('../ts/environment');
|
||||||
const { createSetting } = require('../ts/util/preload');
|
const { createSetting } = require('../ts/util/preload');
|
||||||
|
|
||||||
const { dialog } = remote;
|
|
||||||
|
|
||||||
const STICKER_SIZE = 512;
|
const STICKER_SIZE = 512;
|
||||||
const MIN_STICKER_DIMENSION = 10;
|
const MIN_STICKER_DIMENSION = 10;
|
||||||
const MAX_STICKER_DIMENSION = STICKER_SIZE;
|
const MAX_STICKER_DIMENSION = STICKER_SIZE;
|
||||||
|
@ -171,7 +169,7 @@ window.encryptAndUpload = async (
|
||||||
'StickerCreator--Authentication--error'
|
'StickerCreator--Authentication--error'
|
||||||
];
|
];
|
||||||
|
|
||||||
dialog.showMessageBox({
|
ipc.send('show-message-box', {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
// Copyright 2018-2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
const fs = require('fs');
|
import { assert } from 'chai';
|
||||||
const fse = require('fs-extra');
|
import path from 'path';
|
||||||
const path = require('path');
|
import fs from 'fs';
|
||||||
const tmp = require('tmp');
|
import os from 'os';
|
||||||
const { assert } = require('chai');
|
import fse from 'fs-extra';
|
||||||
const { app } = require('electron');
|
import * as Attachments from '../../windows/attachments';
|
||||||
|
import * as Bytes from '../../Bytes';
|
||||||
const Attachments = require('../../app/attachments');
|
|
||||||
const Bytes = require('../../ts/Bytes');
|
|
||||||
|
|
||||||
const PREFIX_LENGTH = 2;
|
const PREFIX_LENGTH = 2;
|
||||||
const NUM_SEPARATORS = 1;
|
const NUM_SEPARATORS = 1;
|
||||||
|
@ -17,45 +15,128 @@ const NAME_LENGTH = 64;
|
||||||
const PATH_LENGTH = PREFIX_LENGTH + NUM_SEPARATORS + NAME_LENGTH;
|
const PATH_LENGTH = PREFIX_LENGTH + NUM_SEPARATORS + NAME_LENGTH;
|
||||||
|
|
||||||
describe('Attachments', () => {
|
describe('Attachments', () => {
|
||||||
describe('createWriterForNew', () => {
|
const USER_DATA = window.SignalContext.getPath('userData');
|
||||||
let tempRootDirectory = null;
|
|
||||||
|
let tempRootDirectory: string;
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
tempRootDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'Signal'));
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await fse.remove(tempRootDirectory);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createReader', () => {
|
||||||
|
it('should read file from disk', async () => {
|
||||||
|
const tempDirectory = path.join(
|
||||||
|
tempRootDirectory,
|
||||||
|
'Attachments_createReader'
|
||||||
|
);
|
||||||
|
|
||||||
|
const relativePath = Attachments.getRelativePath(
|
||||||
|
Attachments.createName()
|
||||||
|
);
|
||||||
|
const fullPath = path.join(tempDirectory, relativePath);
|
||||||
|
const input = Bytes.fromString('test string');
|
||||||
|
|
||||||
|
const inputBuffer = Buffer.from(input);
|
||||||
|
await fse.ensureFile(fullPath);
|
||||||
|
await fse.writeFile(fullPath, inputBuffer);
|
||||||
|
const output = await Attachments.createReader(tempDirectory)(
|
||||||
|
relativePath
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(input, output);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if relative path goes higher than root', async () => {
|
||||||
|
const tempDirectory = path.join(
|
||||||
|
tempRootDirectory,
|
||||||
|
'Attachments_createReader'
|
||||||
|
);
|
||||||
|
|
||||||
|
const relativePath = '../../parent';
|
||||||
|
|
||||||
|
await assert.isRejected(
|
||||||
|
Attachments.createReader(tempDirectory)(relativePath),
|
||||||
|
'Invalid relative path'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('copyIntoAttachmentsDirectory', () => {
|
||||||
|
let filesToRemove: Array<string>;
|
||||||
|
|
||||||
|
const getFakeAttachmentsDirectory = () => {
|
||||||
|
const result = path.join(
|
||||||
|
USER_DATA,
|
||||||
|
`fake-attachments-${Date.now()}-${Math.random()
|
||||||
|
.toString()
|
||||||
|
.substring(2)}`
|
||||||
|
);
|
||||||
|
filesToRemove.push(result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// These tests use the `userData` path. In `electron-mocha`, these are temporary
|
||||||
|
// directories; no need to be concerned about messing with the "real" directory.
|
||||||
before(() => {
|
before(() => {
|
||||||
tempRootDirectory = tmp.dirSync().name;
|
filesToRemove = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
after(async () => {
|
after(async () => {
|
||||||
await fse.remove(tempRootDirectory);
|
await Promise.all(filesToRemove.map(toRemove => fse.remove(toRemove)));
|
||||||
|
filesToRemove = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should write file to disk and return path', async () => {
|
it('throws if passed a non-string', () => {
|
||||||
const input = Bytes.fromString('test string');
|
assert.throws(() => {
|
||||||
const tempDirectory = path.join(
|
Attachments.copyIntoAttachmentsDirectory((1234 as unknown) as string);
|
||||||
tempRootDirectory,
|
}, TypeError);
|
||||||
'Attachments_createWriterForNew'
|
assert.throws(() => {
|
||||||
|
Attachments.copyIntoAttachmentsDirectory((null as unknown) as string);
|
||||||
|
}, TypeError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a function that rejects if the source path is not a string', async () => {
|
||||||
|
const copier = Attachments.copyIntoAttachmentsDirectory(
|
||||||
|
await getFakeAttachmentsDirectory()
|
||||||
|
);
|
||||||
|
await assert.isRejected(copier((123 as unknown) as string));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a function that rejects if the source path is not in the user config directory', async () => {
|
||||||
|
const copier = Attachments.copyIntoAttachmentsDirectory(
|
||||||
|
await getFakeAttachmentsDirectory()
|
||||||
|
);
|
||||||
|
await assert.isRejected(
|
||||||
|
copier(path.join(tempRootDirectory, 'hello.txt')),
|
||||||
|
"'sourcePath' must be relative to the user config directory"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a function that copies the source path into the attachments directory and returns its path and size', async () => {
|
||||||
|
const attachmentsPath = await getFakeAttachmentsDirectory();
|
||||||
|
const someOtherPath = path.join(USER_DATA, 'somethingElse');
|
||||||
|
await fse.outputFile(someOtherPath, 'hello world');
|
||||||
|
filesToRemove.push(someOtherPath);
|
||||||
|
|
||||||
|
const copier = Attachments.copyIntoAttachmentsDirectory(attachmentsPath);
|
||||||
|
const { path: relativePath, size } = await copier(someOtherPath);
|
||||||
|
|
||||||
|
const absolutePath = path.join(attachmentsPath, relativePath);
|
||||||
|
assert.notEqual(someOtherPath, absolutePath);
|
||||||
|
assert.strictEqual(
|
||||||
|
await fs.promises.readFile(absolutePath, 'utf8'),
|
||||||
|
'hello world'
|
||||||
);
|
);
|
||||||
|
|
||||||
const outputPath = await Attachments.createWriterForNew(tempDirectory)(
|
assert.strictEqual(size, 'hello world'.length);
|
||||||
input
|
|
||||||
);
|
|
||||||
const output = await fse.readFile(path.join(tempDirectory, outputPath));
|
|
||||||
|
|
||||||
assert.lengthOf(outputPath, PATH_LENGTH);
|
|
||||||
|
|
||||||
const inputBuffer = Buffer.from(input);
|
|
||||||
assert.deepEqual(inputBuffer, output);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createWriterForExisting', () => {
|
describe('createWriterForExisting', () => {
|
||||||
let tempRootDirectory = null;
|
|
||||||
before(() => {
|
|
||||||
tempRootDirectory = tmp.dirSync().name;
|
|
||||||
});
|
|
||||||
|
|
||||||
after(async () => {
|
|
||||||
await fse.remove(tempRootDirectory);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should write file to disk on given path and return path', async () => {
|
it('should write file to disk on given path and return path', async () => {
|
||||||
const input = Bytes.fromString('test string');
|
const input = Bytes.fromString('test string');
|
||||||
const tempDirectory = path.join(
|
const tempDirectory = path.join(
|
||||||
|
@ -104,205 +185,23 @@ describe('Attachments', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createReader', () => {
|
describe('createWriterForNew', () => {
|
||||||
let tempRootDirectory = null;
|
it('should write file to disk and return path', async () => {
|
||||||
before(() => {
|
const input = Bytes.fromString('test string');
|
||||||
tempRootDirectory = tmp.dirSync().name;
|
|
||||||
});
|
|
||||||
|
|
||||||
after(async () => {
|
|
||||||
await fse.remove(tempRootDirectory);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should read file from disk', async () => {
|
|
||||||
const tempDirectory = path.join(
|
const tempDirectory = path.join(
|
||||||
tempRootDirectory,
|
tempRootDirectory,
|
||||||
'Attachments_createReader'
|
'Attachments_createWriterForNew'
|
||||||
);
|
);
|
||||||
|
|
||||||
const relativePath = Attachments.getRelativePath(
|
const outputPath = await Attachments.createWriterForNew(tempDirectory)(
|
||||||
Attachments.createName()
|
input
|
||||||
);
|
);
|
||||||
const fullPath = path.join(tempDirectory, relativePath);
|
const output = await fse.readFile(path.join(tempDirectory, outputPath));
|
||||||
const input = Bytes.fromString('test string');
|
|
||||||
|
assert.lengthOf(outputPath, PATH_LENGTH);
|
||||||
|
|
||||||
const inputBuffer = Buffer.from(input);
|
const inputBuffer = Buffer.from(input);
|
||||||
await fse.ensureFile(fullPath);
|
assert.deepEqual(inputBuffer, output);
|
||||||
await fse.writeFile(fullPath, inputBuffer);
|
|
||||||
const output = await Attachments.createReader(tempDirectory)(
|
|
||||||
relativePath
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.deepEqual(input, output);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws if relative path goes higher than root', async () => {
|
|
||||||
const tempDirectory = path.join(
|
|
||||||
tempRootDirectory,
|
|
||||||
'Attachments_createReader'
|
|
||||||
);
|
|
||||||
|
|
||||||
const relativePath = '../../parent';
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Attachments.createReader(tempDirectory)(relativePath);
|
|
||||||
} catch (error) {
|
|
||||||
assert.strictEqual(error.message, 'Invalid relative path');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Expected an error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('copyIntoAttachmentsDirectory', () => {
|
|
||||||
// These tests use the `userData` path. In `electron-mocha`, these are temporary
|
|
||||||
// directories; no need to be concerned about messing with the "real" directory.
|
|
||||||
before(function thisNeeded() {
|
|
||||||
this.filesToRemove = [];
|
|
||||||
this.getFakeAttachmentsDirectory = () => {
|
|
||||||
const result = path.join(
|
|
||||||
app.getPath('userData'),
|
|
||||||
`fake-attachments-${Date.now()}-${Math.random()
|
|
||||||
.toString()
|
|
||||||
.substring(2)}`
|
|
||||||
);
|
|
||||||
this.filesToRemove.push(result);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
this.getTempFile = () => {
|
|
||||||
const result = tmp.fileSync().name;
|
|
||||||
this.filesToRemove.push(result);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
after(async function thisNeeded() {
|
|
||||||
await Promise.all(
|
|
||||||
this.filesToRemove.map(toRemove => fse.remove(toRemove))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws if passed a non-string', () => {
|
|
||||||
assert.throws(() => {
|
|
||||||
Attachments.copyIntoAttachmentsDirectory(1234);
|
|
||||||
}, TypeError);
|
|
||||||
assert.throws(() => {
|
|
||||||
Attachments.copyIntoAttachmentsDirectory(null);
|
|
||||||
}, TypeError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns a function that rejects if the source path is not a string', async function thisNeeded() {
|
|
||||||
const copier = Attachments.copyIntoAttachmentsDirectory(
|
|
||||||
await this.getFakeAttachmentsDirectory()
|
|
||||||
);
|
|
||||||
return copier(123)
|
|
||||||
.then(() => {
|
|
||||||
assert.fail('This should never be run');
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
assert.instanceOf(err, TypeError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns a function that rejects if the source path is not in the user config directory', async function thisNeeded() {
|
|
||||||
const copier = Attachments.copyIntoAttachmentsDirectory(
|
|
||||||
await this.getFakeAttachmentsDirectory()
|
|
||||||
);
|
|
||||||
return copier(this.getTempFile())
|
|
||||||
.then(() => {
|
|
||||||
assert.fail('This should never be run');
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
assert.instanceOf(err, Error);
|
|
||||||
assert.strictEqual(
|
|
||||||
err.message,
|
|
||||||
"'sourcePath' must be relative to the user config directory"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns a function that copies the source path into the attachments directory and returns its path and size', async function thisNeeded() {
|
|
||||||
const attachmentsPath = await this.getFakeAttachmentsDirectory();
|
|
||||||
const someOtherPath = path.join(app.getPath('userData'), 'somethingElse');
|
|
||||||
await fse.outputFile(someOtherPath, 'hello world');
|
|
||||||
this.filesToRemove.push(someOtherPath);
|
|
||||||
|
|
||||||
const copier = Attachments.copyIntoAttachmentsDirectory(attachmentsPath);
|
|
||||||
const { path: relativePath, size } = await copier(someOtherPath);
|
|
||||||
|
|
||||||
const absolutePath = path.join(attachmentsPath, relativePath);
|
|
||||||
assert.notEqual(someOtherPath, absolutePath);
|
|
||||||
assert.strictEqual(
|
|
||||||
await fs.promises.readFile(absolutePath, 'utf8'),
|
|
||||||
'hello world'
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.strictEqual(size, 'hello world'.length);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('createDeleter', () => {
|
|
||||||
let tempRootDirectory = null;
|
|
||||||
before(() => {
|
|
||||||
tempRootDirectory = tmp.dirSync().name;
|
|
||||||
});
|
|
||||||
|
|
||||||
after(async () => {
|
|
||||||
await fse.remove(tempRootDirectory);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete file from disk', async () => {
|
|
||||||
const tempDirectory = path.join(
|
|
||||||
tempRootDirectory,
|
|
||||||
'Attachments_createDeleter'
|
|
||||||
);
|
|
||||||
|
|
||||||
const relativePath = Attachments.getRelativePath(
|
|
||||||
Attachments.createName()
|
|
||||||
);
|
|
||||||
const fullPath = path.join(tempDirectory, relativePath);
|
|
||||||
const input = Bytes.fromString('test string');
|
|
||||||
|
|
||||||
const inputBuffer = Buffer.from(input);
|
|
||||||
await fse.ensureFile(fullPath);
|
|
||||||
await fse.writeFile(fullPath, inputBuffer);
|
|
||||||
await Attachments.createDeleter(tempDirectory)(relativePath);
|
|
||||||
|
|
||||||
const existsFile = await fse.exists(fullPath);
|
|
||||||
assert.isFalse(existsFile);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws if relative path goes higher than root', async () => {
|
|
||||||
const tempDirectory = path.join(
|
|
||||||
tempRootDirectory,
|
|
||||||
'Attachments_createDeleter'
|
|
||||||
);
|
|
||||||
|
|
||||||
const relativePath = '../../parent';
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Attachments.createDeleter(tempDirectory)(relativePath);
|
|
||||||
} catch (error) {
|
|
||||||
assert.strictEqual(error.message, 'Invalid relative path');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Expected an error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('createName', () => {
|
|
||||||
it('should return random file name with correct length', () => {
|
|
||||||
assert.lengthOf(Attachments.createName(), NAME_LENGTH);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getRelativePath', () => {
|
|
||||||
it('should return correct path', () => {
|
|
||||||
const name =
|
|
||||||
'608ce3bc536edbf7637a6aeb6040bdfec49349140c0dd43e97c7ce263b15ff7e';
|
|
||||||
assert.lengthOf(Attachments.getRelativePath(name), PATH_LENGTH);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -336,4 +235,59 @@ describe('Attachments', () => {
|
||||||
throw new Error('Expected an error');
|
throw new Error('Expected an error');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('createName', () => {
|
||||||
|
it('should return random file name with correct length', () => {
|
||||||
|
assert.lengthOf(Attachments.createName(), NAME_LENGTH);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRelativePath', () => {
|
||||||
|
it('should return correct path', () => {
|
||||||
|
const name =
|
||||||
|
'608ce3bc536edbf7637a6aeb6040bdfec49349140c0dd43e97c7ce263b15ff7e';
|
||||||
|
assert.lengthOf(Attachments.getRelativePath(name), PATH_LENGTH);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createDeleter', () => {
|
||||||
|
it('should delete file from disk', async () => {
|
||||||
|
const tempDirectory = path.join(
|
||||||
|
tempRootDirectory,
|
||||||
|
'Attachments_createDeleter'
|
||||||
|
);
|
||||||
|
|
||||||
|
const relativePath = Attachments.getRelativePath(
|
||||||
|
Attachments.createName()
|
||||||
|
);
|
||||||
|
const fullPath = path.join(tempDirectory, relativePath);
|
||||||
|
const input = Bytes.fromString('test string');
|
||||||
|
|
||||||
|
const inputBuffer = Buffer.from(input);
|
||||||
|
await fse.ensureFile(fullPath);
|
||||||
|
await fse.writeFile(fullPath, inputBuffer);
|
||||||
|
await Attachments.createDeleter(tempDirectory)(relativePath);
|
||||||
|
|
||||||
|
const existsFile = await fse.pathExists(fullPath);
|
||||||
|
assert.isFalse(existsFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if relative path goes higher than root', async () => {
|
||||||
|
const tempDirectory = path.join(
|
||||||
|
tempRootDirectory,
|
||||||
|
'Attachments_createDeleter'
|
||||||
|
);
|
||||||
|
|
||||||
|
const relativePath = '../../parent';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Attachments.createDeleter(tempDirectory)(relativePath);
|
||||||
|
} catch (error) {
|
||||||
|
assert.strictEqual(error.message, 'Invalid relative path');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Expected an error');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
|
@ -26,7 +26,7 @@ import rimraf from 'rimraf';
|
||||||
import type { BrowserWindow } from 'electron';
|
import type { BrowserWindow } from 'electron';
|
||||||
import { app, ipcMain } from 'electron';
|
import { app, ipcMain } from 'electron';
|
||||||
|
|
||||||
import { getTempPath } from '../../app/attachments';
|
import { getTempPath } from '../util/attachments';
|
||||||
import { DialogType } from '../types/Dialogs';
|
import { DialogType } from '../types/Dialogs';
|
||||||
import { getUserAgent } from '../util/getUserAgent';
|
import { getUserAgent } from '../util/getUserAgent';
|
||||||
import { isAlpha, isBeta } from '../util/version';
|
import { isAlpha, isBeta } from '../util/version';
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { isString } from 'lodash';
|
||||||
|
import { join, normalize } from 'path';
|
||||||
|
import fse from 'fs-extra';
|
||||||
|
|
||||||
|
import { isPathInside } from './isPathInside';
|
||||||
|
|
||||||
|
const PATH = 'attachments.noindex';
|
||||||
|
const AVATAR_PATH = 'avatars.noindex';
|
||||||
|
const STICKER_PATH = 'stickers.noindex';
|
||||||
|
const TEMP_PATH = 'temp';
|
||||||
|
const DRAFT_PATH = 'drafts.noindex';
|
||||||
|
|
||||||
|
const createPathGetter = (subpath: string) => (
|
||||||
|
userDataPath: string
|
||||||
|
): string => {
|
||||||
|
if (!isString(userDataPath)) {
|
||||||
|
throw new TypeError("'userDataPath' must be a string");
|
||||||
|
}
|
||||||
|
return join(userDataPath, subpath);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAvatarsPath = createPathGetter(AVATAR_PATH);
|
||||||
|
export const getDraftPath = createPathGetter(DRAFT_PATH);
|
||||||
|
export const getPath = createPathGetter(PATH);
|
||||||
|
export const getStickersPath = createPathGetter(STICKER_PATH);
|
||||||
|
export const getTempPath = createPathGetter(TEMP_PATH);
|
||||||
|
|
||||||
|
export const createDeleter = (
|
||||||
|
root: string
|
||||||
|
): ((relativePath: string) => Promise<void>) => {
|
||||||
|
if (!isString(root)) {
|
||||||
|
throw new TypeError("'root' must be a path");
|
||||||
|
}
|
||||||
|
|
||||||
|
return async (relativePath: string): Promise<void> => {
|
||||||
|
if (!isString(relativePath)) {
|
||||||
|
throw new TypeError("'relativePath' must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
const absolutePath = join(root, relativePath);
|
||||||
|
const normalized = normalize(absolutePath);
|
||||||
|
if (!isPathInside(normalized, root)) {
|
||||||
|
throw new Error('Invalid relative path');
|
||||||
|
}
|
||||||
|
await fse.remove(absolutePath);
|
||||||
|
};
|
||||||
|
};
|
|
@ -114,7 +114,7 @@ export type IPCEventsCallbacksType = {
|
||||||
type ValuesWithGetters = Omit<
|
type ValuesWithGetters = Omit<
|
||||||
IPCEventsValuesType,
|
IPCEventsValuesType,
|
||||||
// Optional
|
// Optional
|
||||||
'mediaPermissions' | 'mediaCameraPermissions'
|
'mediaPermissions' | 'mediaCameraPermissions' | 'autoLaunch'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type ValuesWithSetters = Omit<
|
type ValuesWithSetters = Omit<
|
||||||
|
@ -146,6 +146,7 @@ export type IPCEventsGettersType = {
|
||||||
} & {
|
} & {
|
||||||
getMediaPermissions?: () => Promise<boolean>;
|
getMediaPermissions?: () => Promise<boolean>;
|
||||||
getMediaCameraPermissions?: () => Promise<boolean>;
|
getMediaCameraPermissions?: () => Promise<boolean>;
|
||||||
|
getAutoLaunch?: () => Promise<boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IPCEventsSettersType = {
|
export type IPCEventsSettersType = {
|
||||||
|
@ -330,7 +331,7 @@ export function createIPCEvents(
|
||||||
|
|
||||||
getAutoLaunch: () => window.getAutoLaunch(),
|
getAutoLaunch: () => window.getAutoLaunch(),
|
||||||
setAutoLaunch: async (value: boolean) => {
|
setAutoLaunch: async (value: boolean) => {
|
||||||
window.setAutoLaunch(value);
|
return window.setAutoLaunch(value);
|
||||||
},
|
},
|
||||||
|
|
||||||
isPhoneNumberSharingEnabled: () => isPhoneNumberSharingEnabled(),
|
isPhoneNumberSharingEnabled: () => isPhoneNumberSharingEnabled(),
|
||||||
|
|
|
@ -170,8 +170,8 @@ declare global {
|
||||||
imageToBlurHash: typeof imageToBlurHash;
|
imageToBlurHash: typeof imageToBlurHash;
|
||||||
loadImage: any;
|
loadImage: any;
|
||||||
isBehindProxy: () => boolean;
|
isBehindProxy: () => boolean;
|
||||||
getAutoLaunch: () => boolean;
|
getAutoLaunch: () => Promise<boolean>;
|
||||||
setAutoLaunch: (value: boolean) => void;
|
setAutoLaunch: (value: boolean) => Promise<void>;
|
||||||
|
|
||||||
PQueue: typeof PQueue;
|
PQueue: typeof PQueue;
|
||||||
PQueueType: PQueue;
|
PQueueType: PQueue;
|
||||||
|
|
|
@ -0,0 +1,268 @@
|
||||||
|
// Copyright 2018-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
import { isString, isTypedArray } from 'lodash';
|
||||||
|
import { join, normalize, basename } from 'path';
|
||||||
|
import fse from 'fs-extra';
|
||||||
|
import getGuid from 'uuid/v4';
|
||||||
|
|
||||||
|
import { getRandomBytes } from '../Crypto';
|
||||||
|
import * as Bytes from '../Bytes';
|
||||||
|
|
||||||
|
import { isPathInside } from '../util/isPathInside';
|
||||||
|
import { writeWindowsZoneIdentifier } from '../util/windowsZoneIdentifier';
|
||||||
|
import { isWindows } from '../OS';
|
||||||
|
|
||||||
|
export * from '../util/attachments';
|
||||||
|
|
||||||
|
type FSAttrType = {
|
||||||
|
set: (path: string, attribute: string, value: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let xattr: FSAttrType | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
// eslint-disable-next-line global-require, import/no-extraneous-dependencies, import/no-unresolved
|
||||||
|
xattr = require('fs-xattr');
|
||||||
|
} catch (e) {
|
||||||
|
window.SignalContext.log?.info('x-attr dependency did not load successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createReader = (
|
||||||
|
root: string
|
||||||
|
): ((relativePath: string) => Promise<Uint8Array>) => {
|
||||||
|
if (!isString(root)) {
|
||||||
|
throw new TypeError("'root' must be a path");
|
||||||
|
}
|
||||||
|
|
||||||
|
return async (relativePath: string): Promise<Uint8Array> => {
|
||||||
|
if (!isString(relativePath)) {
|
||||||
|
throw new TypeError("'relativePath' must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
const absolutePath = join(root, relativePath);
|
||||||
|
const normalized = normalize(absolutePath);
|
||||||
|
if (!isPathInside(normalized, root)) {
|
||||||
|
throw new Error('Invalid relative path');
|
||||||
|
}
|
||||||
|
return fse.readFile(normalized);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRelativePath = (name: string): string => {
|
||||||
|
if (!isString(name)) {
|
||||||
|
throw new TypeError("'name' must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = name.slice(0, 2);
|
||||||
|
return join(prefix, name);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createName = (): string => {
|
||||||
|
const buffer = getRandomBytes(32);
|
||||||
|
return Bytes.toHex(buffer);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const copyIntoAttachmentsDirectory = (
|
||||||
|
root: string
|
||||||
|
): ((sourcePath: string) => Promise<{ path: string; size: number }>) => {
|
||||||
|
if (!isString(root)) {
|
||||||
|
throw new TypeError("'root' must be a path");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userDataPath = window.SignalContext.getPath('userData');
|
||||||
|
|
||||||
|
return async (
|
||||||
|
sourcePath: string
|
||||||
|
): Promise<{ path: string; size: number }> => {
|
||||||
|
if (!isString(sourcePath)) {
|
||||||
|
throw new TypeError('sourcePath must be a string');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPathInside(sourcePath, userDataPath)) {
|
||||||
|
throw new Error(
|
||||||
|
"'sourcePath' must be relative to the user config directory"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = createName();
|
||||||
|
const relativePath = getRelativePath(name);
|
||||||
|
const absolutePath = join(root, relativePath);
|
||||||
|
const normalized = normalize(absolutePath);
|
||||||
|
if (!isPathInside(normalized, root)) {
|
||||||
|
throw new Error('Invalid relative path');
|
||||||
|
}
|
||||||
|
|
||||||
|
await fse.ensureFile(normalized);
|
||||||
|
await fse.copy(sourcePath, normalized);
|
||||||
|
const { size } = await fse.stat(normalized);
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: relativePath,
|
||||||
|
size,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createWriterForNew = (
|
||||||
|
root: string
|
||||||
|
): ((bytes: Uint8Array) => Promise<string>) => {
|
||||||
|
if (!isString(root)) {
|
||||||
|
throw new TypeError("'root' must be a path");
|
||||||
|
}
|
||||||
|
|
||||||
|
return async (bytes: Uint8Array) => {
|
||||||
|
if (!isTypedArray(bytes)) {
|
||||||
|
throw new TypeError("'bytes' must be a typed array");
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = createName();
|
||||||
|
const relativePath = getRelativePath(name);
|
||||||
|
return createWriterForExisting(root)({
|
||||||
|
data: bytes,
|
||||||
|
path: relativePath,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createWriterForExisting = (
|
||||||
|
root: string
|
||||||
|
): ((options: { data: Uint8Array; path: string }) => Promise<string>) => {
|
||||||
|
if (!isString(root)) {
|
||||||
|
throw new TypeError("'root' must be a path");
|
||||||
|
}
|
||||||
|
|
||||||
|
return async ({
|
||||||
|
data: bytes,
|
||||||
|
path: relativePath,
|
||||||
|
}: {
|
||||||
|
data: Uint8Array;
|
||||||
|
path: string;
|
||||||
|
}): Promise<string> => {
|
||||||
|
if (!isString(relativePath)) {
|
||||||
|
throw new TypeError("'relativePath' must be a path");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isTypedArray(bytes)) {
|
||||||
|
throw new TypeError("'arrayBuffer' must be an array buffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = Buffer.from(bytes);
|
||||||
|
const absolutePath = join(root, relativePath);
|
||||||
|
const normalized = normalize(absolutePath);
|
||||||
|
if (!isPathInside(normalized, root)) {
|
||||||
|
throw new Error('Invalid relative path');
|
||||||
|
}
|
||||||
|
|
||||||
|
await fse.ensureFile(normalized);
|
||||||
|
await fse.writeFile(normalized, buffer);
|
||||||
|
return relativePath;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createAbsolutePathGetter = (rootPath: string) => (
|
||||||
|
relativePath: string
|
||||||
|
): string => {
|
||||||
|
const absolutePath = join(rootPath, relativePath);
|
||||||
|
const normalized = normalize(absolutePath);
|
||||||
|
if (!isPathInside(normalized, rootPath)) {
|
||||||
|
throw new Error('Invalid relative path');
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createDoesExist = (
|
||||||
|
root: string
|
||||||
|
): ((relativePath: string) => Promise<boolean>) => {
|
||||||
|
if (!isString(root)) {
|
||||||
|
throw new TypeError("'root' must be a path");
|
||||||
|
}
|
||||||
|
|
||||||
|
return async (relativePath: string): Promise<boolean> => {
|
||||||
|
if (!isString(relativePath)) {
|
||||||
|
throw new TypeError("'relativePath' must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
const absolutePath = join(root, relativePath);
|
||||||
|
const normalized = normalize(absolutePath);
|
||||||
|
if (!isPathInside(normalized, root)) {
|
||||||
|
throw new Error('Invalid relative path');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await fse.access(normalized, fse.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const openFileInFolder = async (target: string): Promise<void> => {
|
||||||
|
ipcRenderer.send('show-item-in-folder', target);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showSaveDialog = (
|
||||||
|
defaultPath: string
|
||||||
|
): Promise<{
|
||||||
|
canceled: boolean;
|
||||||
|
filePath?: string;
|
||||||
|
}> => {
|
||||||
|
return ipcRenderer.invoke('show-save-dialog', { defaultPath });
|
||||||
|
};
|
||||||
|
|
||||||
|
async function writeWithAttributes(
|
||||||
|
target: string,
|
||||||
|
data: Uint8Array
|
||||||
|
): Promise<void> {
|
||||||
|
await fse.writeFile(target, Buffer.from(data));
|
||||||
|
|
||||||
|
if (process.platform === 'darwin' && xattr) {
|
||||||
|
// kLSQuarantineTypeInstantMessageAttachment
|
||||||
|
const type = '0003';
|
||||||
|
|
||||||
|
// Hexadecimal seconds since epoch
|
||||||
|
const timestamp = Math.trunc(Date.now() / 1000).toString(16);
|
||||||
|
|
||||||
|
const appName = 'Signal';
|
||||||
|
const guid = getGuid();
|
||||||
|
|
||||||
|
// https://ilostmynotes.blogspot.com/2012/06/gatekeeper-xprotect-and-quarantine.html
|
||||||
|
const attrValue = `${type};${timestamp};${appName};${guid}`;
|
||||||
|
|
||||||
|
await xattr.set(target, 'com.apple.quarantine', attrValue);
|
||||||
|
} else if (isWindows()) {
|
||||||
|
// This operation may fail (see the function's comments), which is not a show-stopper.
|
||||||
|
try {
|
||||||
|
await writeWindowsZoneIdentifier(target);
|
||||||
|
} catch (err) {
|
||||||
|
window.SignalContext.log?.warn(
|
||||||
|
'Failed to write Windows Zone.Identifier file; continuing'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const saveAttachmentToDisk = async ({
|
||||||
|
data,
|
||||||
|
name,
|
||||||
|
}: {
|
||||||
|
data: Uint8Array;
|
||||||
|
name: string;
|
||||||
|
}): Promise<null | { fullPath: string; name: string }> => {
|
||||||
|
const { canceled, filePath } = await showSaveDialog(name);
|
||||||
|
|
||||||
|
if (canceled || !filePath) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeWithAttributes(filePath, data);
|
||||||
|
|
||||||
|
const fileBasename = basename(filePath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fullPath: filePath,
|
||||||
|
name: fileBasename,
|
||||||
|
};
|
||||||
|
};
|
|
@ -53,6 +53,7 @@ export type SignalContextType = {
|
||||||
getEnvironment: () => string;
|
getEnvironment: () => string;
|
||||||
getNodeVersion: () => string;
|
getNodeVersion: () => string;
|
||||||
getVersion: () => string;
|
getVersion: () => string;
|
||||||
|
getPath: (name: 'userData' | 'home' | 'downloads') => string;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
log: LoggerType;
|
log: LoggerType;
|
||||||
renderWindow?: () => void;
|
renderWindow?: () => void;
|
||||||
|
@ -71,6 +72,9 @@ export const SignalContext: SignalContextType = {
|
||||||
getEnvironment,
|
getEnvironment,
|
||||||
getNodeVersion: (): string => String(config.node_version),
|
getNodeVersion: (): string => String(config.node_version),
|
||||||
getVersion: (): string => String(config.version),
|
getVersion: (): string => String(config.version),
|
||||||
|
getPath: (name: 'userData' | 'home' | 'downloads'): string => {
|
||||||
|
return String(config[`${name}Path`]);
|
||||||
|
},
|
||||||
i18n: setupI18n(locale, localeMessages),
|
i18n: setupI18n(locale, localeMessages),
|
||||||
log: window.SignalContext.log,
|
log: window.SignalContext.log,
|
||||||
nativeThemeListener: createNativeThemeListener(ipcRenderer, window),
|
nativeThemeListener: createNativeThemeListener(ipcRenderer, window),
|
||||||
|
|
Loading…
Reference in New Issue