Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[desktop] update registry entries for windows, depend on install mode #3715

Merged
merged 2 commits into from
Dec 21, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 17 additions & 13 deletions src/desktop/DesktopUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {log} from "./DesktopLog"
import {DesktopCryptoFacade} from "./DesktopCryptoFacade"
import {fileExists, swapFilename} from "./PathUtils"
import url from "url"
import {registerKeys, unregisterKeys} from "./reg-templater"

export class DesktopUtils {

Expand Down Expand Up @@ -240,24 +241,27 @@ export class DesktopUtils {
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,
isLocal
)
return this._executeRegistryScript(tmpRegScript)
.then(() => app.setAsDefaultProtocolClient('mailto'))
.then(() => this._electron.shell.openExternal('ms-settings:defaultapps').catch())
const tmpRegScript = registerKeys({execPath, dllPath, logPath, tmpPath}, isLocal)
await this._executeRegistryScript(tmpRegScript)
app.setAsDefaultProtocolClient('mailto')
await this._openDefaultAppsSettings()
}

async _unregisterOnWin(): Promise<void> {
app.removeAsDefaultProtocolClient('mailto')
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())
const tmpRegScript = unregisterKeys(isLocal)
await this._executeRegistryScript(tmpRegScript)
await this._openDefaultAppsSettings()
}

async _openDefaultAppsSettings(): Promise<void> {
try {
await this._electron.shell.openExternal('ms-settings:defaultapps')
} catch (e) {
// ignoring, this is just a convenience for the user
console.error("failed to open default apps settings page:", e.message)
}
}

/**
Expand Down
54 changes: 34 additions & 20 deletions src/desktop/integration/RegistryScriptGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,19 @@
*
* * 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.
* that path and all its subkeys will be removed.
* * sections can contain
* * default value assignments of the form @="VALUE"
* * default value nullifications of the form @=-
* * named value assignments of the form "NAME"="VALUE"
* * named value deletions of the form "NAME"=-
*
* 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
* To generate this application script:
* ```
* Windows Registry Editor Version 5.00
* [HKLM\SOFTWARE\CLIENTS\MAIL]
Expand All @@ -52,7 +48,7 @@
* "DLLPath"="C:\dll\path\t.dll"
* ```
*
* and the removal script
* and this removal script
*
* ```
* Windows Registry Editor Version 5.00
Expand All @@ -64,24 +60,42 @@
* [-HKLM\SOFTWARE\CLIENTS\MAIL\subkey]
* ```
*
* Note that "HKLM\SOFTWARE\CLIENTS\MAIL\subkey" was removed entirely while the values
* We would use this template. Note the values with empty keys which get expanded to @=<value> assignments.
*
* ```
* const template = [{
* root: "HKLM\\SOFTWARE\\CLIENTS\\MAIL",
* value: {"named": "val", subkey: {"": "default_value_2", "DLLPath": "C:\\dll\\path\\t.dll",}, "": "default_value_1"}
* }]
* ```
*
* Also 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 RegistryTemplateDefinition = $ReadOnlyArray<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)}`

function quote(s: string): string {
return `"${s}"`
}

function keyLine(path: string): string {
return `[${path}]`
}

function valueLine(path: string, value: ?string): string {
return `${path === "" ? "@" : quote(path)}=${value == null ? "-" : quote(value)}`
}

/**
* value expander for the script generators. if a value is not a string, it's another section
Expand Down Expand Up @@ -132,22 +146,22 @@ function bufToScript(buf: OperationBuffer): string {
/**
* the application and removal script generators are very similar in structure, this function abstracts over that.
*/
function scriptBuilder(remove: boolean, template: Array<RegistryValueTemplate>): string {
function scriptBuilder(remove: boolean, template: RegistryTemplateDefinition): 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 {
export function applyScriptBuilder(template: RegistryTemplateDefinition): 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 {
export function removeScriptBuilder(template: RegistryTemplateDefinition): string {
return scriptBuilder(true, template)
}
73 changes: 47 additions & 26 deletions src/desktop/reg-templater.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,26 @@ const esc = s => s.replace(/\\/g, '\\\\')
const hklm = "HKEY_LOCAL_MACHINE"
const hkcu = "HKEY_CURRENT_USER"
const hkcr = "HKEY_CLASSES_ROOT"
const sw_clients_mail = r => `${r}\\SOFTWARE\\Clients\\Mail`
const sw_cls = r => `${r}\\SOFTWARE\\CLASSES`
const sw = r => `${r}\\SOFTWARE`
const sw_wow = r => `${r}\\SOFTWARE\\Wow6432Node`
const sw_reg_apps = r => `${r}\\SOFTWARE\\RegisteredApplications`
const sw_wow_reg_apps = r => `${r}\\SOFTWARE\\Wow6432Node\\RegisteredApplications`

/**
* get a registry template specific to tutanota desktop
* https://docs.microsoft.com/en-us/windows/win32/msi/installation-context#registry-redirection
* execPath: path for the dll to this executable
* dllPath: path for other apps to the dll
* logPath: path for the dll to log mapi activity to
* tmpPath: path for the dll to put temporary attachment files
*/
function getTemplate(
export type RegistryPaths = {
execPath: string,
dllPath: string,
logPath: string,
tmpPath: string,
local: boolean,
): RegistryTemplateDefinition {
tmpPath: string
}

/**
* get a registry template specific to tutanota desktop
* https://docs.microsoft.com/en-us/windows/win32/msi/installation-context#registry-redirection
*/
function getTemplate(opts: RegistryPaths, local: boolean): RegistryTemplateDefinition {
const {execPath, dllPath, logPath, tmpPath} = opts
const client_template = {
tutanota: {
"": "tutanota",
Expand Down Expand Up @@ -64,27 +66,46 @@ function getTemplate(

const r = local ? hkcu : hklm
return [
{root: sw_clients_mail(r), value: client_template},
{root: sw_cls(r), value: {mailto: mailto_template, "tutanota.Mailto": mailto_template}},
{root: hkcr, value: {mailto: mailto_template, "tutanota.Mailto": mailto_template}},
{root: sw_reg_apps(r), value: {"tutanota": "SOFTWARE\\\\tutao\\\\tutanota\\\\Capabilities"}},
{root: sw_wow_reg_apps(r), value: {"tutanota": "SOFTWARE\\\\Wow6432Node\\\\tutao\\\\tutanota\\\\Capabilities"}},
{root: sw(r), value: capabilities_template},
{root: sw_wow(r), value: capabilities_template}
{
root: `${r}\\SOFTWARE\\Clients\\Mail`,
value: client_template
},
{
root: `${r}\\SOFTWARE\\CLASSES`,
value: {mailto: mailto_template, "tutanota.Mailto": mailto_template}
},
{
root: hkcr,
value: {mailto: mailto_template, "tutanota.Mailto": mailto_template}
},
{
root: `${r}\\SOFTWARE\\RegisteredApplications`,
value: {"tutanota": "SOFTWARE\\\\tutao\\\\tutanota\\\\Capabilities"}
},
{
root: `${r}\\SOFTWARE\\Wow6432Node\\RegisteredApplications`,
value: {"tutanota": "SOFTWARE\\\\Wow6432Node\\\\tutao\\\\tutanota\\\\Capabilities"}
},
{
root: `${r}\\SOFTWARE`,
value: capabilities_template
},
{
root: `${r}\\SOFTWARE\\Wow6432Node`,
value: capabilities_template
}
]
}

/**
* produce a tmp windows registry script to register an executable as a mailto handler
* @param execPath path to the executable that should be registered
* @param dllPath path to the mapi dll that handles "Send as Mail..." requests
* @param logPath path to the directory the mapi dll should put logs in
* @param tmpPath path to the tmp dir that's managed by tutanota
* @param opts {RegistryPaths}
* @param local set to true if the app was installed per-user
* @returns {string} registry script
*/
export function registerKeys(execPath: string, dllPath: string, logPath: string, tmpPath: string, local: boolean): string {
const template = getTemplate(esc(execPath), esc(dllPath), esc(logPath), esc(tmpPath), local)
export function registerKeys(opts: RegistryPaths, local: boolean): string {
ganthern marked this conversation as resolved.
Show resolved Hide resolved
const {execPath, dllPath, logPath, tmpPath} = opts
const template = getTemplate({execPath: esc(execPath), dllPath: esc(dllPath), logPath: esc(logPath), tmpPath: esc(tmpPath)}, local)
return applyScriptBuilder(template)
}

Expand All @@ -94,6 +115,6 @@ export function registerKeys(execPath: string, dllPath: string, logPath: string,
*/
export function unregisterKeys(local: boolean): string {
// the removal script generator doesn't care about values
const template = getTemplate("execPath", "dllPath", "logPath", "tmpPath", local)
const template = getTemplate({execPath: "execPath", dllPath: "dllPath", logPath: "logPath", tmpPath: "tmpPath"}, local)
return removeScriptBuilder(template)
}
16 changes: 8 additions & 8 deletions test/client/desktop/integration/RegistryScriptGeneratorTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,40 @@ o.spec("RegistryScriptGenerator Test", function () {
const templates = [
{
name: "empty template",
t: [],
template: [],
apply: "Windows Registry Editor Version 5.00",
remove: "Windows Registry Editor Version 5.00"
},
{
name: "naked value",
t: [{root: 'HKLM', value: {"tuta": "some_value"}}],
template: [{root: 'HKLM', value: {"tuta": "some_value"}}],
apply: 'Windows Registry Editor Version 5.00\r\n\r\n[HKLM]\r\n"tuta"="some_value"',
remove: 'Windows Registry Editor Version 5.00\r\n\r\n[HKLM]\r\n"tuta"=-'
},
{
name: "with subkeys",
t: [{root: 'HKLM', value: {tuta: {'': "subkey_value"}}}],
template: [{root: 'HKLM', value: {tuta: {'': "subkey_value"}}}],
apply: 'Windows Registry Editor Version 5.00\r\n\r\n[HKLM\\tuta]\r\n@="subkey_value"',
remove: 'Windows Registry Editor Version 5.00\r\n\r\n[-HKLM\\tuta]'
},
{
name: "mixed",
t: [{root: "HKLM", value: {direct: "direct_val", sub: {"key": "sub_val"}}}],
template: [{root: "HKLM", value: {direct: "direct_val", sub: {"key": "sub_val"}}}],
apply: 'Windows Registry Editor Version 5.00\r\n\r\n[HKLM]\r\n"direct"="direct_val"\r\n\r\n[HKLM\\sub]\r\n"key"="sub_val"',
remove: 'Windows Registry Editor Version 5.00\r\n\r\n[HKLM]\r\n"direct"=-\r\n\r\n[-HKLM\\sub]',
},
{
name: "deeper subkey",
t: [{root: "HKCU", value: {a: {deep: {path: {subkey: {'': "hello."}}}}}}],
template: [{root: "HKCU", value: {a: {deep: {path: {subkey: {'': "hello."}}}}}}],
apply: 'Windows Registry Editor Version 5.00\r\n\r\n[HKCU\\a\\deep\\path\\subkey]\r\n@="hello."',
remove: 'Windows Registry Editor Version 5.00\r\n\r\n[-HKCU\\a]',
},
]

for (const {t, apply, remove, name} of templates) {
for (const {template, apply, remove, name} of templates) {
o(name, function () {
o(applyScriptBuilder(t)).equals(apply)
o(removeScriptBuilder(t)).equals(remove)
o(applyScriptBuilder(template)).equals(apply)
o(removeScriptBuilder(template)).equals(remove)
})
}
})