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

URL handling #6243

Merged
merged 16 commits into from
Apr 24, 2023
Merged
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/ide-desktop/eslint.config.js
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ const DEFAULT_IMPORT_ONLY_MODULES =
const ALLOWED_DEFAULT_IMPORT_MODULES = `${DEFAULT_IMPORT_ONLY_MODULES}|postcss|react-hot-toast`
const OUR_MODULES = 'enso-content-config|enso-common'
const RELATIVE_MODULES =
'bin\\u002Fproject-manager|bin\\u002Fserver|config\\u002Fparser|authentication|config|debug|file-associations|index|ipc|naming|paths|preload|security'
'bin\\u002Fproject-manager|bin\\u002Fserver|config\\u002Fparser|authentication|config|debug|file-associations|index|ipc|naming|paths|preload|security|url-associations'
const STRING_LITERAL = ':matches(Literal[raw=/^["\']/], TemplateLiteral)'
const JSX = ':matches(JSXElement, JSXFragment)'
const NOT_PASCAL_CASE = '/^(?!_?([A-Z][a-z0-9]*)+$)/'
48 changes: 28 additions & 20 deletions app/ide-desktop/lib/client/src/authentication.ts
Original file line number Diff line number Diff line change
@@ -72,27 +72,21 @@
* {@link URL} to redirect the user to the dashboard, to the page specified in the {@link URL}'s
* `pathname`. */

import * as electron from 'electron'
import opener from 'opener'
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import opener from 'opener'

import * as electron from 'electron'

import * as common from 'enso-common'
import * as contentConfig from 'enso-content-config'
import * as urlAssociations from 'url-associations'

import * as ipc from 'ipc'

const logger = contentConfig.logger

// =================
// === Constants ===
// =================

/** Name of the Electron event that is emitted when a URL is opened in Electron (e.g., when the user
* clicks a link in the dashboard). */
const OPEN_URL_EVENT = 'open-url'

// ========================================
// === Initialize Authentication Module ===
// ========================================
@@ -121,7 +115,11 @@ export function initModule(window: () => electron.BrowserWindow) {
* This functionality is necessary because we don't want to run the OAuth flow in the app. Users
* don't trust Electron apps to handle their credentials. */
function initIpc() {
electron.ipcMain.on(ipc.Channel.openUrlInSystemBrowser, (_event, url: string) => opener(url))
electron.ipcMain.on(ipc.Channel.openUrlInSystemBrowser, (_event, url: string) => {
logger.log(`Opening URL in system browser: '${url}'.`)
urlAssociations.setAsUrlHandler()
opener(url)
})
}

/** Registers a listener that fires a callback for `open-url` events, when the URL is a deep link.
@@ -132,18 +130,28 @@ function initIpc() {
* All URLs that aren't deep links (i.e., URLs that don't use the {@link common.DEEP_LINK_SCHEME}
* protocol) will be ignored by this handler. Non-deep link URLs will be handled by Electron. */
function initOpenUrlListener(window: () => electron.BrowserWindow) {
electron.app.on(OPEN_URL_EVENT, (event, url) => {
const parsedUrl = new URL(url)
/** Prevent Electron from handling the URL at all, because redirects can be dangerous. */
event.preventDefault()
if (parsedUrl.protocol !== `${common.DEEP_LINK_SCHEME}:`) {
logger.error(`${url} is not a deep link, ignoring.`)
} else {
window().webContents.send(ipc.Channel.openDeepLink, url)
}
urlAssociations.registerUrlCallback(url => {
onOpenUrl(url, window)
})
}

/**
* Handles the 'open-url' event by parsing the received URL, checking if it is a deep link, and
* sending it to the appropriate BrowserWindow via IPC.
*
* @param url - The URL to handle.
* @param window - A function that returns the BrowserWindow to send the parsed URL to.
*/
export function onOpenUrl(url: URL, window: () => electron.BrowserWindow) {
logger.log(`Received 'open-url' event for '${url.toString()}'.`)
if (url.protocol !== `${common.DEEP_LINK_SCHEME}:`) {
logger.error(`'${url.toString()}' is not a deep link, ignoring.`)
} else {
logger.log(`'${url.toString()}' is a deep link, sending to renderer.`)
window().webContents.send(ipc.Channel.openDeepLink, url.toString())
}
}

/** Registers a listener that fires a callback for `save-access-token` events.
*
* This listener is used to save given access token to credentials file to be later used by enso backend.
3 changes: 2 additions & 1 deletion app/ide-desktop/lib/client/src/bin/project-manager.ts
Original file line number Diff line number Diff line change
@@ -24,8 +24,9 @@ export function pathOrPanic(args: config.Args): string {
const binExists = fsSync.existsSync(binPath)
if (!binExists) {
throw new Error(`Could not find the project manager binary at ${binPath}.`)
} else {
return binPath
}
return binPath
}

/** Executes the Project Manager with given arguments. */
29 changes: 27 additions & 2 deletions app/ide-desktop/lib/client/src/file-associations.ts
Original file line number Diff line number Diff line change
@@ -14,11 +14,13 @@ import * as electron from 'electron'
import electronIsDev from 'electron-is-dev'

import * as common from 'enso-common'
import * as config from 'enso-content-config'
import * as contentConfig from 'enso-content-config'

import * as clientConfig from './config'
import * as fileAssociations from '../file-associations'
import * as project from './project-management'

const logger = config.logger
const logger = contentConfig.logger

// =================
// === Reexports ===
@@ -146,6 +148,29 @@ export function handleOpenFile(openedFile: string): string {
}
logger.error(e)
electron.dialog.showErrorBox(common.PRODUCT_NAME, message)
// eslint-disable-next-line no-restricted-syntax
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@somebody1234 why we still need such strange linter config in code? I remember we were talking about fixing linter config some time ago and making such annotations not needed anymore. What is the status of this topic? :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the cause of this specific one is bad eslint config, it should be able to be removed once #6267 is merged

throw e
}
}

/** Handle the file to open, if any. See {@link handleOpenFile} for details.
*
* If no file to open is provided, does nothing.
*
* Handles all errors internally.
* @param openedFile - The file to open (null if none).
* @param args - The parsed application arguments.
*/
export function handleFileArguments(openedFile: string | null, args: clientConfig.Args): void {
if (openedFile != null) {
try {
// This makes the IDE open the relevant project. Also, this prevents us from using this
// method after IDE has been fully set up, as the initializing code would have already
// read the value of this argument.
args.groups.startup.options.project.value = handleOpenFile(openedFile)
} catch (e) {
// If we failed to open the file, we should enter the usual welcome screen.
// The `handleOpenFile` function will have already displayed an error message.
}
}
}
48 changes: 32 additions & 16 deletions app/ide-desktop/lib/client/src/index.ts
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ import * as paths from 'paths'
import * as projectManager from 'bin/project-manager'
import * as security from 'security'
import * as server from 'bin/server'
import * as urlAssociations from 'url-associations'
import * as utils from '../../../utils'

const logger = contentConfig.logger
@@ -44,22 +45,12 @@ class App {
isQuitting = false

async run() {
urlAssociations.registerAssociations()
// Register file associations for macOS.
electron.app.on('open-file', fileAssociations.onFileOpened)

const { windowSize, chromeOptions, fileToOpen } = this.processArguments()
if (fileToOpen != null) {
try {
// This makes the IDE open the relevant project. Also, this prevents us from using this
// method after IDE has been fully set up, as the initializing code would have already
// read the value of this argument.
this.args.groups.startup.options.project.value =
fileAssociations.handleOpenFile(fileToOpen)
} catch (e) {
// If we failed to open the file, we should enter the usual welcome screen.
// The `handleOpenFile` function will have already displayed an error message.
}
}
const { windowSize, chromeOptions, fileToOpen, urlToOpen } = this.processArguments()
this.handleItemOpening(fileToOpen, urlToOpen)
if (this.args.options.version.value) {
await this.printVersion()
electron.app.quit()
@@ -91,11 +82,36 @@ class App {
const fileToOpen = fileAssociations.argsDenoteFileOpenAttempt(
fileAssociations.CLIENT_ARGUMENTS
)
const urlToOpen = urlAssociations.argsDenoteUrlOpenAttempt(
fileAssociations.CLIENT_ARGUMENTS
)
// If we are opening a file (i.e. we were spawned with just a path of the file to open as
// the argument), it means that effectively we don't have any non-standard arguments.
// the argument) or URL, it means that effectively we don't have any non-standard arguments.
// We just need to let caller know that we are opening a file.
const argsToParse = fileToOpen ? [] : fileAssociations.CLIENT_ARGUMENTS
return { ...configParser.parseArgs(argsToParse), fileToOpen }
const argsToParse = fileToOpen || urlToOpen ? [] : fileAssociations.CLIENT_ARGUMENTS
return { ...configParser.parseArgs(argsToParse), fileToOpen, urlToOpen }
}

/** This method is invoked when the application was spawned due to being a default application
* for a URL protocol or file extension. */
handleItemOpening(fileToOpen: string | null, urlToOpen: URL | null) {
logger.log('Opening file or URL.', { fileToOpen, urlToOpen })
try {
if (fileToOpen != null) {
// This makes the IDE open the relevant project. Also, this prevents us from using this
// method after IDE has been fully set up, as the initializing code would have already
// read the value of this argument.
this.args.groups.startup.options.project.value =
fileAssociations.handleOpenFile(fileToOpen)
}

if (urlToOpen != null) {
urlAssociations.handleOpenUrl(urlToOpen)
}
} catch (e) {
// If we failed to open the file, we should enter the usual welcome screen.
// The `handleOpenFile` function will have already displayed an error message.
}
}

/** Set Chrome options based on the app configuration. For comprehensive list of available
4 changes: 3 additions & 1 deletion app/ide-desktop/lib/client/src/project-management.ts
Original file line number Diff line number Diff line change
@@ -51,8 +51,9 @@ export function importProjectFromPath(openedPath: string): string {
if (rootPath == null) {
const message = `File '${openedPath}' does not belong to the ${common.PRODUCT_NAME} project.`
throw new Error(message)
} else {
return importDirectory(rootPath)
}
return importDirectory(rootPath)
}
}

@@ -98,6 +99,7 @@ export function importDirectory(rootPath: string): string {
const targetDirectory = generateDirectoryName(rootPath)
if (fsSync.existsSync(targetDirectory)) {
const message = `Project directory already exists: ${targetDirectory}.`
// eslint-disable-next-line no-restricted-syntax
somebody1234 marked this conversation as resolved.
Show resolved Hide resolved
throw new Error(message)
}

Loading