diff --git a/CHANGELOG.md b/CHANGELOG.md index eb7521e..434d21b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,48 @@ +Changes in [1.6.5](https://github.com/vector-im/riot-desktop/releases/tag/v1.6.5) (2020-06-16) +============================================================================================== +[Full Changelog](https://github.com/vector-im/riot-desktop/compare/v1.6.4...v1.6.5) + + * No changes since 1.6.4 + +Changes in [1.6.4](https://github.com/vector-im/riot-desktop/releases/tag/v1.6.4) (2020-06-05) +============================================================================================== +[Full Changelog](https://github.com/vector-im/riot-desktop/compare/v1.6.3...v1.6.4) + + * No changes since 1.6.3 + +Changes in [1.6.3](https://github.com/vector-im/riot-desktop/releases/tag/v1.6.3) (2020-06-04) +============================================================================================== +[Full Changelog](https://github.com/vector-im/riot-desktop/compare/v1.6.3-rc.1...v1.6.3) + + * No changes since rc.1 + +Changes in [1.6.3-rc.1](https://github.com/vector-im/riot-desktop/releases/tag/v1.6.3-rc.1) (2020-06-02) +======================================================================================================== +[Full Changelog](https://github.com/vector-im/riot-desktop/compare/v1.6.2...v1.6.3-rc.1) + + * Fix electron context menu copy/save-as + [\#96](https://github.com/vector-im/riot-desktop/pull/96) + * Fixed error in README.md/User-specified config.json + [\#97](https://github.com/vector-im/riot-desktop/pull/97) + * Update Modular hosting link + [\#92](https://github.com/vector-im/riot-desktop/pull/92) + * Enforce sandbox on all spawned BrowserWindow objects + [\#91](https://github.com/vector-im/riot-desktop/pull/91) + * Run before-quit on updates too to flush rageshake + [\#93](https://github.com/vector-im/riot-desktop/pull/93) + * Enable new room list labs flag + [\#87](https://github.com/vector-im/riot-desktop/pull/87) + * Add asar-webapp script + [\#59](https://github.com/vector-im/riot-desktop/pull/59) + * Bump acorn from 6.4.0 to 6.4.1 + [\#50](https://github.com/vector-im/riot-desktop/pull/50) + * Enable font scaling flag for nightly + [\#89](https://github.com/vector-im/riot-desktop/pull/89) + * Enable IRC UI labs flag in nightly + [\#88](https://github.com/vector-im/riot-desktop/pull/88) + * Update help message to fix broken url to electron docs + [\#86](https://github.com/vector-im/riot-desktop/pull/86) + Changes in [1.6.2](https://github.com/vector-im/riot-desktop/releases/tag/v1.6.2) (2020-05-22) ============================================================================================== [Full Changelog](https://github.com/vector-im/riot-desktop/compare/v1.6.1...v1.6.2) diff --git a/README.md b/README.md index 3072cb7..14e643b 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ User-specified config.json + `%APPDATA%\$NAME\config.json` on Windows + `$XDG_CONFIG_HOME\$NAME\config.json` or `~/.config/$NAME/config.json` on Linux -+ `~Library/Application Support/$NAME/config.json` on macOS ++ `~/Library/Application Support/$NAME/config.json` on macOS In the paths above, `$NAME` is typically `Riot`, unless you use `--profile $PROFILE` in which case it becomes `Riot-$PROFILE`. diff --git a/hak/keytar/build.js b/hak/keytar/build.js new file mode 100644 index 0000000..39319ef --- /dev/null +++ b/hak/keytar/build.js @@ -0,0 +1,42 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const path = require('path'); +const childProcess = require('child_process'); + +module.exports = async function(hakEnv, moduleInfo) { + await buildKeytar(hakEnv, moduleInfo); +}; + +async function buildKeytar(hakEnv, moduleInfo) { + const env = hakEnv.makeGypEnv(); + + console.log("Running yarn with env", env); + await new Promise((resolve, reject) => { + const proc = childProcess.spawn( + path.join(moduleInfo.nodeModuleBinDir, 'node-gyp' + (hakEnv.isWin() ? '.cmd' : '')), + ['rebuild'], + { + cwd: moduleInfo.moduleBuildDir, + env, + stdio: 'inherit', + }, + ); + proc.on('exit', (code) => { + code ? reject(code) : resolve(); + }); + }); +} diff --git a/hak/keytar/check.js b/hak/keytar/check.js new file mode 100644 index 0000000..8fcb788 --- /dev/null +++ b/hak/keytar/check.js @@ -0,0 +1,36 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const childProcess = require('child_process'); + +module.exports = async function(hakEnv, moduleInfo) { + const tools = [['python', '--version']]; // node-gyp uses python for reasons beyond comprehension + + for (const tool of tools) { + await new Promise((resolve, reject) => { + const proc = childProcess.spawn(tool[0], tool.slice(1), { + stdio: ['ignore'], + }); + proc.on('exit', (code) => { + if (code !== 0) { + reject("Can't find " + tool); + } else { + resolve(); + } + }); + }); + } +}; diff --git a/hak/keytar/hak.json b/hak/keytar/hak.json new file mode 100644 index 0000000..7597052 --- /dev/null +++ b/hak/keytar/hak.json @@ -0,0 +1,10 @@ +{ + "scripts": { + "check": "check.js", + "build": "build.js" + }, + "copy": "build/Release/keytar.node", + "dependencies": { + "libsecret": "0.20.3" + } +} diff --git a/package.json b/package.json index 0f0ff3d..7c6805e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "riot-desktop", "productName": "Riot", "main": "src/electron-main.js", - "version": "1.6.2", + "version": "1.6.5", "description": "A feature-rich client for Matrix.org", "author": "New Vector Ltd.", "repository": { @@ -50,15 +50,16 @@ "glob": "^7.1.6", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "mkdirp": "^1.0.3", - "needle": "^2.3.2", - "node-pre-gyp": "^0.14.0", + "needle": "^2.5.0", + "node-pre-gyp": "^0.15.0", "npm": "^6.13.7", "rimraf": "^3.0.2", "semver": "^7.1.3", "tar": "^6.0.1" }, "hakDependencies": { - "matrix-seshat": "^1.3.3" + "matrix-seshat": "^1.3.3", + "keytar": "^5.6.0" }, "build": { "appId": "im.riot.app", diff --git a/riot.im/nightly/config.json b/riot.im/nightly/config.json index 0d7a5e8..f40113d 100644 --- a/riot.im/nightly/config.json +++ b/riot.im/nightly/config.json @@ -11,7 +11,7 @@ "https://scalar-staging.vector.im/api", "https://scalar-staging.riot.im/scalar/api" ], - "hosting_signup_link": "https://modular.im/?utm_source=riot-web&utm_medium=web", + "hosting_signup_link": "https://modular.im/services/matrix-hosting-riot?utm_source=riot-web&utm_medium=web", "bug_report_endpoint_url": "https://riot.im/bugreports/submit", "features": { "feature_pinning": "labs", diff --git a/riot.im/release/config.json b/riot.im/release/config.json index 9342151..b92f11e 100644 --- a/riot.im/release/config.json +++ b/riot.im/release/config.json @@ -11,7 +11,7 @@ "https://scalar-staging.vector.im/api", "https://scalar-staging.riot.im/scalar/api" ], - "hosting_signup_link": "https://modular.im/?utm_source=riot-web&utm_medium=web", + "hosting_signup_link": "https://modular.im/services/matrix-hosting-riot?utm_source=riot-web&utm_medium=web", "bug_report_endpoint_url": "https://riot.im/bugreports/submit", "roomDirectory": { "servers": [ diff --git a/src/electron-main.js b/src/electron-main.js index 8843f68..923f594 100644 --- a/src/electron-main.js +++ b/src/electron-main.js @@ -3,6 +3,7 @@ Copyright 2016 Aviral Dasgupta Copyright 2016 OpenMarket Ltd Copyright 2018, 2019 New Vector Ltd Copyright 2017, 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -43,6 +44,18 @@ const Store = require('electron-store'); const fs = require('fs'); const afs = fs.promises; +const crypto = require('crypto'); +let keytar; +try { + keytar = require('keytar'); +} catch (e) { + if (e.code === "MODULE_NOT_FOUND") { + console.log("Keytar isn't installed; secure key storage is disabled."); + } else { + console.warn("Keytar unexpected error:", e); + } +} + let seshatSupported = false; let Seshat; let SeshatRecovery; @@ -241,17 +254,6 @@ ipcMain.on('app_onAction', function(ev, payload) { } }); -autoUpdater.on('update-downloaded', (ev, releaseNotes, releaseName, releaseDate, updateURL) => { - if (!mainWindow) return; - // forward to renderer - mainWindow.webContents.send('update-downloaded', { - releaseNotes, - releaseName, - releaseDate, - updateURL, - }); -}); - ipcMain.on('ipcCall', async function(ev, payload) { if (!mainWindow) return; @@ -365,6 +367,41 @@ ipcMain.on('ipcCall', async function(ev, payload) { recordSSOSession(args[0]); break; + case 'getPickleKey': + try { + ret = await keytar.getPassword("riot.im", `${args[0]}|${args[1]}`); + } catch (e) { + // if an error is thrown (e.g. keytar can't connect to the keychain), + // then return null, which means the default pickle key will be used + ret = null; + } + break; + + case 'createPickleKey': + try { + const randomArray = await new Promise((resolve, reject) => { + crypto.randomBytes(32, (err, buf) => { + if (err) { + reject(err); + } else { + resolve(buf); + } + }); + }); + const pickleKey = randomArray.toString("base64").replace(/=+$/g, ''); + await keytar.setPassword("riot.im", `${args[0]}|${args[1]}`, pickleKey); + ret = pickleKey; + } catch (e) { + ret = null; + } + break; + + case 'destroyPickleKey': + try { + await keytar.deletePassword("riot.im", `${args[0]}|${args[1]}`); + } catch (e) {} + break; + default: mainWindow.webContents.send('ipcReply', { id: payload.id, @@ -615,6 +652,17 @@ protocol.registerSchemesAsPrivileged([{ }, }]); +// Turn the sandbox on for *all* windows we might generate. Doing this means we don't +// have to specify a `sandbox: true` to each BrowserWindow. +// +// This also fixes an issue with window.open where if we only specified the sandbox +// on the main window we'd run into cryptic "ipc_renderer be broke" errors. Turns out +// it's trying to jump the sandbox and make some calls into electron, which it can't +// do when half of it is sandboxed. By turning on the sandbox for everything, the new +// window (no matter how temporary it may be) is also sandboxed, allowing for a clean +// transition into the user's browser. +app.enableSandbox(); + app.on('ready', async () => { try { await setupGlobals(); @@ -725,7 +773,7 @@ app.on('ready', async () => { webPreferences: { preload: preloadScript, nodeIntegration: false, - sandbox: true, + //sandbox: true, // We enable sandboxing from app.enableSandbox() above enableRemoteModule: false, // We don't use this: it's useful for the preload script to // share a context with the main page so we can give select @@ -790,12 +838,15 @@ app.on('activate', () => { mainWindow.show(); }); -app.on('before-quit', () => { +function beforeQuit() { global.appQuitting = true; if (mainWindow) { mainWindow.webContents.send('before-quit'); } -}); +} + +app.on('before-quit', beforeQuit); +app.on('before-quit-for-update', beforeQuit); app.on('second-instance', (ev, commandLine, workingDirectory) => { // If other instance launched with --hidden then skip showing window diff --git a/src/updater.js b/src/updater.js index f5917f8..8f9d0f4 100644 --- a/src/updater.js +++ b/src/updater.js @@ -69,15 +69,31 @@ ipcMain.on('install_update', installUpdate); ipcMain.on('check_updates', pollForUpdates); function ipcChannelSendUpdateStatus(status) { - if (global.mainWindow) { - global.mainWindow.webContents.send('check_updates', status); - } + if (!global.mainWindow) return; + global.mainWindow.webContents.send('check_updates', status); } +// cache the latest update which has been downloaded as electron offers no api to read it +let latestUpdateDownloaded; autoUpdater.on('update-available', function() { ipcChannelSendUpdateStatus(true); }).on('update-not-available', function() { - ipcChannelSendUpdateStatus(false); + if (latestUpdateDownloaded) { + // the only time we will get `update-not-available` if `latestUpdateDownloaded` is already set + // is if the user used the Manual Update check and there is no update newer than the one we + // have downloaded, so show it to them as the latest again. + if (!global.mainWindow) return; + global.mainWindow.webContents.send('update-downloaded', latestUpdateDownloaded); + } else { + ipcChannelSendUpdateStatus(false); + } }).on('error', function(error) { ipcChannelSendUpdateStatus(error.message); }); + +autoUpdater.on('update-downloaded', (ev, releaseNotes, releaseName, releaseDate, updateURL) => { + if (!global.mainWindow) return; + // forward to renderer + latestUpdateDownloaded = { releaseNotes, releaseName, releaseDate, updateURL }; + global.mainWindow.webContents.send('update-downloaded', latestUpdateDownloaded); +}); diff --git a/src/webcontents-handler.js b/src/webcontents-handler.js index 8030646..00f971c 100644 --- a/src/webcontents-handler.js +++ b/src/webcontents-handler.js @@ -35,10 +35,27 @@ function onWindowOrNavigate(ev, target) { safeOpenURL(target); } +function writeNativeImage(filePath, img) { + switch (filePath.split('.').pop().toLowerCase()) { + case "jpg": + case "jpeg": + return fs.promises.writeFile(filePath, img.toJPEG(100)); + case "bmp": + return fs.promises.writeFile(filePath, img.toBitmap()); + case "png": + default: + return fs.promises.writeFile(filePath, img.toPNG()); + } +} + + function onLinkContextMenu(ev, params) { let url = params.linkURL || params.srcURL; if (url.startsWith('vector://vector/webapp')) { + // Avoid showing a context menu for app icons + if (params.hasImageContents) return; + // Rewrite URL so that it can be used outside of the app url = "https://riot.im/app/" + url.substring(23); } @@ -53,22 +70,13 @@ function onLinkContextMenu(ev, params) { })); } - let addSaveAs = false; - if (params.mediaType && params.mediaType === 'image' && !url.startsWith('file://')) { + if (params.hasImageContents) { popupMenu.append(new MenuItem({ label: '&Copy image', click() { - if (url.startsWith('data:')) { - clipboard.writeImage(nativeImage.createFromDataURL(url)); - } else { - ev.sender.copyImageAt(params.x, params.y); - } + ev.sender.copyImageAt(params.x, params.y); }, })); - - // We want the link to be ordered below the copy stuff, but don't want to duplicate - // the `if` statement, so use a flag. - addSaveAs = true; } // No point offering to copy a blob: URL either @@ -91,12 +99,14 @@ function onLinkContextMenu(ev, params) { } } - if (addSaveAs) { + // XXX: We cannot easily save a blob from the main process as + // only the renderer can resolve them so don't give the user an option to. + if (params.hasImageContents && !url.startsWith('blob:')) { popupMenu.append(new MenuItem({ label: 'Sa&ve image as...', - click() { + async click() { const targetFileName = params.titleText || "image.png"; - const filePath = dialog.showSaveDialog({ + const {filePath} = await dialog.showSaveDialog({ defaultPath: targetFileName, }); @@ -104,7 +114,7 @@ function onLinkContextMenu(ev, params) { try { if (url.startsWith("data:")) { - fs.writeFileSync(filePath, nativeImage.createFromDataURL(url)); + await writeNativeImage(filePath, nativeImage.createFromDataURL(url)); } else { request.get(url).pipe(fs.createWriteStream(filePath)); } diff --git a/yarn.lock b/yarn.lock index 4e766f2..ec03441 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2863,8 +2863,8 @@ map-age-cleaner@^0.1.1: p-defer "^1.0.0" "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "6.1.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/a4a7097c103da42075f2c70e070fd01fa6fb0d48" + version "6.2.2" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/1c194e81637fb07fe6ad67cda33be0d5d4c10115" dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0" @@ -2958,6 +2958,11 @@ minimist@^1.2.0, minimist@^1.2.3: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.3.tgz#3db5c0765545ab8637be71f333a104a965a9ca3f" integrity sha512-+bMdgqjMN/Z77a6NlY/I3U5LlRDbnmaAk6lDveAPKwSpcPM4tKAuYsvYF8xjhOPXhOYGe/73vVLVez5PW+jqhw== +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + minipass@^2.3.5, minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" @@ -3011,6 +3016,13 @@ mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: dependencies: minimist "0.0.8" +mkdirp@^0.5.3: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + mkdirp@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea" @@ -3053,10 +3065,10 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -needle@^2.2.1, needle@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.3.2.tgz#3342dea100b7160960a450dc8c22160ac712a528" - integrity sha512-DUzITvPVDUy6vczKKYTnWc/pBZ0EnjMJnQ3y+Jo5zfKFimJs7S3HFCxCRZYB9FUZcrzUQr3WsmvZgddMEIZv6w== +needle@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.5.0.tgz#e6fc4b3cc6c25caed7554bd613a5cf0bac8c31c0" + integrity sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA== dependencies: debug "^3.2.6" iconv-lite "^0.4.4" @@ -3093,14 +3105,14 @@ node-gyp@^5.0.2, node-gyp@^5.0.7: tar "^4.4.12" which "^1.3.1" -node-pre-gyp@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz#9a0596533b877289bcad4e143982ca3d904ddc83" - integrity sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA== +node-pre-gyp@^0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz#c2fc383276b74c7ffa842925241553e8b40f1087" + integrity sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA== dependencies: detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" + mkdirp "^0.5.3" + needle "^2.5.0" nopt "^4.0.1" npm-packlist "^1.1.6" npmlog "^4.0.2"