Skip to content

Commit

Permalink
[desktop] update registry entries for windows, depend on install mode
Browse files Browse the repository at this point in the history
* a per-user install should only write into HKEY_CURRENT_USER so other users
can't select an app that's not installed for them as default mail app
* the app now opens the settings page after registering as a mail handler so
the user can select it

#3574
  • Loading branch information
ganthern committed Dec 9, 2021
1 parent d22fc91 commit 8af1041
Show file tree
Hide file tree
Showing 7 changed files with 333 additions and 105 deletions.
19 changes: 19 additions & 0 deletions buildSrc/windows-installer.nsh
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
; electron builder nsis code:
; https://github.com/electron-userland/electron-builder/tree/master/packages/app-builder-lib/templates/nsis

!macro disableAutoUpdates
${GetParameters} $R0
ClearErrors
Expand All @@ -7,10 +10,26 @@
${EndIf}
!macroend

!macro saveInstallMode
; to detect whether _this_ install was installed per-user or per-machine,
; we put a file next to the executable on a per-user install.
; this should prevent confusion with multiple installs or custom paths
; that could occur if we wrote to a common location in the registry or file system
${If} $installMode == 'CurrentUser'
ClearErrors
FileOpen $0 $INSTDIR\per_user w
IfErrors done
FileWrite $0 ""
FileClose $0
done:
${EndIf}
!macroend

!macro deleteUpdateFile
Delete $INSTDIR\resources\app-update.yml
!macroend

!macro customInstall
!insertMacro disableAutoUpdates
!insertMacro saveInstallMode
!macroend
5 changes: 3 additions & 2 deletions flow/electron.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ declare module 'electron' {
declare export var shell: {
// Open the given external protocol URL in the desktop's default manner.
// (For example, mailto: URLs in the user's default mail agent).
openExternal(url: string): void;
openExternal(url: string): Promise<void>;
showItemInFolder(fullPath: string): void;
// Open the given file in the desktop's default manner.
openPath(fullPath: string): Promise<string>;
Expand Down Expand Up @@ -321,7 +321,7 @@ declare module 'electron' {
once(AppEvent, (Event, ...Array<any>) => any): App,
emit(AppEvent): App,
removeListener(AppEvent, Function): App,
requestSingleInstanceLock(): void,
requestSingleInstanceLock(): boolean,
quit(): void,
exit(code: number): void,
relaunch({args: Array<string>, execPath?: string}): void,
Expand Down Expand Up @@ -354,6 +354,7 @@ declare module 'electron' {
args?: string
}): void;
getAppPath(): string;
getName(): string;
getPath(name: 'home'
| 'appData' //Per-user application data directory
| 'userData' // directory for your app's configuration files, by default it is appData + app name.
Expand Down
40 changes: 27 additions & 13 deletions src/desktop/DesktopUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@ import {exec, spawn} from 'child_process'
import {promisify} from 'util'
import type {Rectangle} from "electron"
import {app} from 'electron'
import {defer} from '@tutao/tutanota-utils'
import {noOp} from "@tutao/tutanota-utils"
import {defer, delay, noOp, uint8ArrayToHex} from '@tutao/tutanota-utils'
import {log} from "./DesktopLog"
import {uint8ArrayToHex} from "@tutao/tutanota-utils"
import {delay} from "@tutao/tutanota-utils"
import {DesktopCryptoFacade} from "./DesktopCryptoFacade"
import {swapFilename} from "./PathUtils"
import {fileExists, swapFilename} from "./PathUtils"
import url from "url"

export class DesktopUtils {
Expand All @@ -30,6 +27,11 @@ export class DesktopUtils {
return Promise.resolve(app.isDefaultProtocolClient("mailto"))
}
checkIsPerUserInstall(): Promise<boolean> {
const markerPath = swapFilename(process.execPath, "per_user")
return fileExists(markerPath)
}
/**
* open and close a file to make sure it exists
* @param path: the file to touch
Expand Down Expand Up @@ -224,26 +226,38 @@ export class DesktopUtils {
}
async _registerOnWin(): Promise<void> {
// any app that wants to use tutanota over MAPI needs to know which dll to load.
// additionally, the DLL needs to know
// * which tutanota executable to start (per-user/per-machine/snapshot/test/release)
// * where to log (this depends on the current user -> %USERPROFILE%)
// * where to put tmp files (also user-dependent)
// all these must be set in the registry
const execPath = process.execPath
const dllPath = swapFilename(execPath, "mapirs.dll")
const logPath = path.join(app.getPath('userData'), 'logs')
const tmpPath = this.getTutanotaTempPath('attach')
const dllPath = swapFilename(execPath, 'mapirs.dll')
// we may be a per-machine installation that's used by multiple users, so the dll will replace %USERPROFILE%
// with the value of the USERPROFILE env var.
const appData = path.join("%USERPROFILE%", 'AppData')
const logPath = path.join(appData, 'Roaming', app.getName(), 'logs')
const tmpPath = path.join(appData, "Local", "Temp", this._topLevelDownloadDir, "attach")
const isLocal = await this.checkIsPerUserInstall()
const tmpRegScript = (await import('./reg-templater.js')).registerKeys(
execPath,
dllPath,
logPath,
tmpPath
tmpPath,
isLocal
)
return this._executeRegistryScript(tmpRegScript)
.then(() => {
app.setAsDefaultProtocolClient('mailto')
})
.then(() => app.setAsDefaultProtocolClient('mailto'))
.then(() => this._electron.shell.openExternal('ms-settings:defaultapps').catch())
}

async _unregisterOnWin(): Promise<void> {
app.removeAsDefaultProtocolClient('mailto')
const tmpRegScript = (await import('./reg-templater.js')).unregisterKeys()
const isLocal = await this.checkIsPerUserInstall()
const tmpRegScript = (await import('./reg-templater.js')).unregisterKeys(isLocal)
return this._executeRegistryScript(tmpRegScript)
.then(() => this._electron.shell.openExternal('ms-settings:defaultapps').catch())
}

/**
Expand Down
153 changes: 153 additions & 0 deletions src/desktop/integration/RegistryScriptGenerator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// @flow

/*
* This file helps keep registry operations for application and removal in sync by generating
* an application registry script and a removal script from the same template.
*
* Windows Registry Scripts look like this:
* ```
* Windows Registry Editor Version 5.00
*
* [HKCU\SOFTWARE\CLIENTS\MAIL]
* @="default_value"
*
* [-HKLM\SOFTWARE\CLIENTS\MAIL\Mozilla Thunderbird]
*
* [HKCU\SOFTWARE\CLIENTS\MAIL\Section]
* @="default_value"
* "name"="named_value"
* "value_to_delete"=-
*
* [HKCU\SOFTWARE\CLIENTS\MAIL\Section2]
* @=-
* ```
*
* * there is a header line followed by a list of sections
* * each section starts with a path in square brackets []. if the path is prefixed with a dash (-),
* that path and all subkeys will be removed.
* * sections can contain default and named value assignments of the form (NAME|@)=(VALUE|-).
* if the value is a dash (-) the name will be removed or the default value will be unset.
*
* The script generator uses JavaScript arrays of RegistryValueTemplates as templates.
* During application, RegistryValueTemplates will recursively written to the registry at their respective root path.
* The removal script preserves the root keys. this means that subkeys that were created in a root key will be recursively deleted,
* but string values that are assigned directly to a root key or a name in that subkey will only be nulled. Example:
*
* ```
* const template = [{
* root: "HKLM\\SOFTWARE\\CLIENTS\\MAIL",
* value: {"named": "val", subkey: {"": "default_value_2", "DLLPath": "C:\\dll\\path\\t.dll",}, "": "default_value_1"}
* }]
* ```
*
* Will result in the application script
* ```
* Windows Registry Editor Version 5.00
* [HKLM\SOFTWARE\CLIENTS\MAIL]
* "named"="val"
* @="default_value_1"
*
* [HKLM\SOFTWARE\CLIENTS\MAIL\subkey]
* @="default_value_2"
* "DLLPath"="C:\dll\path\t.dll"
* ```
*
* and the removal script
*
* ```
* Windows Registry Editor Version 5.00
*
* [HKLM\SOFTWARE\CLIENTS\MAIL]
* "named"=-
* @=-
*
* [-HKLM\SOFTWARE\CLIENTS\MAIL\subkey]
* ```
*
* Note that "HKLM\SOFTWARE\CLIENTS\MAIL\subkey" was removed entirely while the values
* directly assigned to "HKLM\SOFTWARE\CLIENTS\MAIL" are only nulled, because that path was given as a root.
*
* Current Limitations:
* * only string values are supported
* * application can only write, removal will only remove
* */

export type RegistryTemplateDefinition = Array<RegistryValueTemplate>
export type RegistryValueTemplate = {value: RegistrySubKey, root: string}
export type RegistrySubKey = {[string]: RegistryValue}
export type RegistryValue = RegistrySubKey | string
type OperationBuffer = {[string]: Array<string>}

const header_line = "Windows Registry Editor Version 5.00"
const quote = s => `"${s}"`
const keyLine = path => `[${path}]`
const valueLine = (path, value) => `${path === "" ? "@" : quote(path)}=${value == null ? "-" : quote(value)}`

/**
* value expander for the script generators. if a value is not a string, it's another section
* that gets expanded recursively.
*
* remove is used to create value/key removal lines
*/
function expandValue(path: string, key: string, value: RegistryValue, buf: OperationBuffer, remove?: boolean): OperationBuffer {
if (typeof value === "string") {
buf[path].push(valueLine(key, remove ? null : value))
} else {
buf = expandSection(`${path}\\${key}`, value, buf, remove)
}
return buf
}

/**
* section expander for the script generator
*/
function expandSection(path: string, value: RegistrySubKey, buf: OperationBuffer, remove?: boolean): OperationBuffer {
if (buf[path] == null) buf[path] = []
for (const key in value) {
if (typeof value[key] !== "string" && remove) {
buf[`-${path}\\${key}`] = []
} else {
expandValue(path, key, value[key], buf, remove)
}
}
return buf
}

/**
* converts a map of registry paths to value setters into an executable registry script
* @param {OperationBuffer} buf List of operations the need to be done
* @returns {string} a windows registry script that can be imported by regex.exe to apply the operations
*/
function bufToScript(buf: OperationBuffer): string {
const lines = [header_line]
for (const key in buf) {
const next = buf[key]
if (next.length < 1 && !key.startsWith("-")) continue
lines.push("", keyLine(key))
lines.push(...next)
}
return lines.join("\r\n").trim()
}

/**
* the application and removal script generators are very similar in structure, this function abstracts over that.
*/
function scriptBuilder(remove: boolean, template: Array<RegistryValueTemplate>): string {
const buf = template.reduce((prev, {root, value}) => expandSection(root, value, prev, remove), {})
return bufToScript(buf)
}

/**
* create a windows registry script that can be executed to apply the given template
*/
export function applyScriptBuilder(template: Array<RegistryValueTemplate>): string {
return scriptBuilder(false, template)
}

/**
* create a windows registry script that can be executed to remove the values that have been
* created by executing the script generated from the template by applyScriptBuilder
*/
export function removeScriptBuilder(template: Array<RegistryValueTemplate>): string {
return scriptBuilder(true, template)
}

0 comments on commit 8af1041

Please sign in to comment.