diff --git a/app/main/index.ts b/app/main/index.ts index 74b2cbe2..e677f116 100644 --- a/app/main/index.ts +++ b/app/main/index.ts @@ -38,13 +38,20 @@ const mainURL = 'file://' + path.join(__dirname, '../renderer', 'main.html'); const singleInstanceLock = app.requestSingleInstanceLock(); if (singleInstanceLock) { - app.on('second-instance', () => { + app.on('second-instance', (event, argv) => { if (mainWindow) { if (mainWindow.isMinimized()) { mainWindow.restore(); } mainWindow.show(); + + // URI scheme handler for systems other than macOS. + // For macOS, see 'open-url' event below. + if (process.platform !== 'darwin') { + const deepLinkingUrl = argv.slice(1); + handleDeepLink(deepLinkingUrl[(isDev && process.platform === 'win32') ? 1 : 0]); + } } }); } else { @@ -66,6 +73,11 @@ const toggleApp = (): any => { } }; +function handleDeepLink(url: string): void { + mainWindow.webContents.focus(); + mainWindow.webContents.send('deep-linking-url', url); +} + function createMainWindow(): Electron.BrowserWindow { // Load the previous state with fallback to defaults const mainWindowState: windowStateKeeper.State = windowStateKeeper({ @@ -145,6 +157,20 @@ function createMainWindow(): Electron.BrowserWindow { // Decrease load on GPU (experimental) app.disableHardwareAcceleration(); +if (process.platform === 'win32' && isDev) { + app.setAsDefaultProtocolClient('zulip', process.execPath, [path.resolve(process.argv[1])]); +} else { + app.setAsDefaultProtocolClient('zulip'); +} + +// URI scheme handler for macOS +app.on('open-url', (event, url) => { + event.preventDefault(); + if (mainWindow) { + mainWindow.webContents.send('deep-linking-url', url); + } +}); + // Temporary fix for Electron render colors differently // More info here - https://github.com/electron/electron/issues/10732 app.commandLine.appendSwitch('force-color-profile', 'srgb'); diff --git a/app/package.json b/app/package.json index 228ad7bd..7c404c2f 100644 --- a/app/package.json +++ b/app/package.json @@ -43,7 +43,8 @@ "node-json-db": "0.9.2", "request": "2.85.0", "semver": "5.4.1", - "wurl": "2.5.0" + "wurl": "2.5.0", + "crypto-random-string": "3.1.0" }, "optionalDependencies": { "node-mac-notifier": "1.1.0" diff --git a/app/renderer/js/components/webview.ts b/app/renderer/js/components/webview.ts index 2cf6b30b..249f917b 100644 --- a/app/renderer/js/components/webview.ts +++ b/app/renderer/js/components/webview.ts @@ -254,6 +254,10 @@ class WebView extends BaseComponent { this.$el.openDevTools(); } + loadURL(url: string): void { + this.$el.loadURL(url); + } + back(): void { if (this.$el.canGoBack()) { this.$el.goBack(); diff --git a/app/renderer/js/main.ts b/app/renderer/js/main.ts index ca49ec80..8333162e 100644 --- a/app/renderer/js/main.ts +++ b/app/renderer/js/main.ts @@ -21,6 +21,7 @@ import ReconnectUtil = require('./utils/reconnect-util'); import Logger = require('./utils/logger-util'); import CommonUtil = require('./utils/common-util'); import EnterpriseUtil = require('./utils/enterprise-util'); +import AuthUtil = require('./utils/auth-util'); import Messages = require('./../../resources/messages'); interface FunctionalTabProps { @@ -47,6 +48,7 @@ interface SettingsOptions { autoUpdate: boolean; betaUpdate: boolean; errorReporting: boolean; + loginInApp: boolean; customCSS: boolean; silent: boolean; lastActiveTab: number; @@ -199,6 +201,7 @@ class ServerManagerView { autoUpdate: true, betaUpdate: false, errorReporting: true, + loginInApp: false, customCSS: false, silent: false, lastActiveTab: 0, @@ -808,6 +811,30 @@ class ServerManagerView { }); } + ipcRenderer.on('deep-linking-url', (event: Event, url: string) => { + if (!ConfigUtil.getConfigItem('desktopOtp')) { + return; + } + const urlObject = new URL(decodeURIComponent(url)); + const serverURL = urlObject.searchParams.get('realm'); + let apiKey = urlObject.searchParams.get('otp_encrypted_login_key'); + const desktopOtp = ConfigUtil.getConfigItem('desktopOtp'); + apiKey = AuthUtil.hexToAscii(AuthUtil.xorStrings(apiKey, desktopOtp)); + + // Use this apiKey to login the realm if it exists + if (apiKey === '') { + console.log('Invalid API Key'); + } else { + DomainUtil.getDomains().forEach((domain: any, index: number) => { + if (domain.url.includes(serverURL)) { + this.activateTab(index); + this.tabs[index].webview.loadURL(`${serverURL}/accounts/login/subdomain/${apiKey}`); + } + }); + ConfigUtil.setConfigItem('desktopOtp', null); + } + }); + ipcRenderer.on('show-network-error', (event: Event, index: number) => { this.openNetworkTroubleshooting(index); }); diff --git a/app/renderer/js/pages/preference/general-section.ts b/app/renderer/js/pages/preference/general-section.ts index 3795c086..adb41992 100644 --- a/app/renderer/js/pages/preference/general-section.ts +++ b/app/renderer/js/pages/preference/general-section.ts @@ -93,29 +93,33 @@ class GeneralSection extends BaseSection {
${t.__('Advanced')}
-
-
${t.__('Enable error reporting (requires restart)')}
-
-
-
-
${t.__('Show downloaded files in file manager')}
-
-
-
-
- ${t.__('Add custom CSS')} -
- -
-
-
-
${ConfigUtil.getConfigItem('customCSS')}
-
-
- indeterminate_check_box - ${t.__('Delete')} -
-
+
+
${t.__('Enable error reporting (requires restart)')}
+
+
+
+
${t.__('Force social login in app instead of browser')}
+
+
+
+
${t.__('Show downloaded files in file manager')}
+
+
+
+
+ ${t.__('Add custom CSS')} +
+ +
+
+
+
${ConfigUtil.getConfigItem('customCSS')}
+
+
+ indeterminate_check_box + ${t.__('Delete')} +
+
${t.__('Default download location')} @@ -131,7 +135,6 @@ class GeneralSection extends BaseSection {
${t.__('Ask where to save files before downloading')}
-
${t.__('Reset Application Data')}
@@ -166,6 +169,7 @@ class GeneralSection extends BaseSection { this.updateQuitOnCloseOption(); this.updatePromptDownloadOption(); this.enableErrorReporting(); + this.enableLoginInApp(); // Platform specific settings @@ -367,6 +371,18 @@ class GeneralSection extends BaseSection { }); } + enableLoginInApp(): void { + this.generateSettingOption({ + $element: document.querySelector('#force-login-app .setting-control'), + value: ConfigUtil.getConfigItem('loginInApp', true), + clickHandler: () => { + const newValue = !ConfigUtil.getConfigItem('loginInApp'); + ConfigUtil.setConfigItem('loginInApp', newValue); + this.enableLoginInApp(); + } + }); + } + clearAppDataDialog(): void { const clearAppDataMessage = 'By clicking proceed you will be removing all added accounts and preferences from Zulip. When the application restarts, it will be as if you are starting Zulip for the first time.'; const getAppPath = path.join(app.getPath('appData'), app.getName()); diff --git a/app/renderer/js/preload.ts b/app/renderer/js/preload.ts index 4164b91a..9adf9f58 100644 --- a/app/renderer/js/preload.ts +++ b/app/renderer/js/preload.ts @@ -11,6 +11,8 @@ import SetupSpellChecker from './spellchecker'; import isDev = require('electron-is-dev'); import LinkUtil = require('./utils/link-util'); import params = require('./utils/params-util'); +import AuthUtil = require('./utils/auth-util'); +import ConfigUtil = require('./utils/config-util'); import NetworkError = require('./pages/network'); @@ -79,7 +81,24 @@ process.once('loaded', (): void => { // To prevent failing this script on linux we need to load it after the document loaded document.addEventListener('DOMContentLoaded', (): void => { if (params.isPageParams()) { - // Get the default language of the server + const authMethods = page_params.external_authentication_methods; // eslint-disable-line no-undef + const loginInApp = ConfigUtil.getConfigItem('loginInApp'); + console.log(loginInApp); + if (authMethods && !loginInApp) { + for (const authMethod of authMethods) { + const { button_id_suffix } = authMethod; + const $socialButton = document.querySelector(`button[id$="${button_id_suffix}"]`); + if ($socialButton) { + $socialButton.addEventListener('click', event => { + event.preventDefault(); + const socialLink = $socialButton.closest('form').action; + AuthUtil.openInBrowser(socialLink); + }); + } + } + } + + // Get the default language of the server const serverLanguage = page_params.default_language; // eslint-disable-line no-undef if (serverLanguage) { // Init spellchecker diff --git a/app/renderer/js/utils/auth-util.ts b/app/renderer/js/utils/auth-util.ts new file mode 100644 index 00000000..4cbbf335 --- /dev/null +++ b/app/renderer/js/utils/auth-util.ts @@ -0,0 +1,36 @@ +import { remote } from 'electron'; + +import cryptoRandomString = require('crypto-random-string'); +import ConfigUtil = require('./config-util'); + +const { shell } = remote; + +class AuthUtil { + openInBrowser = (link: string) => { + const otp = cryptoRandomString({length: 64}); + ConfigUtil.setConfigItem('desktopOtp', otp); + shell.openExternal(`${link}?desktop_flow_otp=${otp}`); + }; + + xorStrings = (a: string, b: string): string => { + if (a.length === b.length) { + return a + .split('') + .map((char, i) => (parseInt(a[i], 16) ^ parseInt(b[i], 16)).toString(16)) + .join('') + .toUpperCase(); + } else { + return ''; + } + }; + + hexToAscii = (hex: string) => { + let ascii = ''; + for (let i = 0; i < hex.length; i += 2) { + ascii += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); + } + return ascii; + }; +} + +export = new AuthUtil(); diff --git a/app/translations/en-GB.json b/app/translations/en-GB.json index 6f690c76..02838c4c 100644 --- a/app/translations/en-GB.json +++ b/app/translations/en-GB.json @@ -115,5 +115,9 @@ "View Shortcuts": "View Shortcuts", "Enter Full Screen": "Enter Full Screen", "History Shortcuts": "History Shortcuts", - "Quit when the window is closed": "Quit when the window is closed" + "Quit when the window is closed": "Quit when the window is closed", + "File": "File", + "Network and Proxy Settings": "Network and Proxy Settings", + "Ask where to save files before downloading": "Ask where to save files before downloading", + "Force social login in app instead of browser (not recommended)": "Force social login in app instead of browser (not recommended)" } \ No newline at end of file diff --git a/package.json b/package.json index e702e11f..b06e41ef 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,15 @@ "entitlementsInherit": "build/entitlements.mac.plist", "gatekeeperAssess": false }, + "protocols": [ + { + "name": "zulip", + "role": "Viewer", + "schemes": [ + "zulip" + ] + } + ], "linux": { "category": "Chat;GNOME;GTK;Network;InstantMessaging", "icon": "build/icon.icns", diff --git a/typings.d.ts b/typings.d.ts index 5555c928..f3a3df3f 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -13,10 +13,12 @@ declare module 'fs-extra'; declare module 'wurl'; declare module 'i18n'; declare module 'backoff'; +declare module 'crypto-random-string'; interface PageParamsObject { - realm_uri: string; - default_language: string; + realm_uri: string; + default_language: string; + external_authentication_methods: any; } declare var page_params: PageParamsObject;