diff --git a/demo/index.html b/demo/index.html index b0975352..d1818a2b 100644 --- a/demo/index.html +++ b/demo/index.html @@ -16,7 +16,7 @@

- +

diff --git a/js/aboutApp/index.js b/js/aboutApp/index.js index 3181bb92..117f86a6 100644 --- a/js/aboutApp/index.js +++ b/js/aboutApp/index.js @@ -6,6 +6,7 @@ const path = require('path'); const fs = require('fs'); const log = require('../log.js'); const logLevels = require('../enums/logLevels.js'); +const buildNumber = require('../../package.json').buildNumber; let aboutWindow; @@ -78,6 +79,10 @@ function openAboutWindow(windowName) { aboutWindow.show(); }); + aboutWindow.webContents.on('did-finish-load', () => { + aboutWindow.webContents.send('buildNumber', buildNumber || '0'); + }); + aboutWindow.on('close', () => { destroyWindow(); }); diff --git a/js/aboutApp/renderer.js b/js/aboutApp/renderer.js index 808c4337..dcb35981 100644 --- a/js/aboutApp/renderer.js +++ b/js/aboutApp/renderer.js @@ -1,5 +1,5 @@ 'use strict'; -const { remote } = require('electron'); +const { remote, ipcRenderer } = require('electron'); renderDom(); @@ -9,13 +9,19 @@ renderDom(); function renderDom() { document.addEventListener('DOMContentLoaded', function () { const applicationName = remote.app.getName() || 'Symphony'; - const version = remote.app.getVersion(); let appName = document.getElementById('app-name'); - let versionText = document.getElementById('version'); let copyright = document.getElementById('copyright'); appName.innerHTML = applicationName; - versionText.innerHTML = version ? `Version ${version} (${version})` : null; copyright.innerHTML = `Copyright © ${new Date().getFullYear()} ${applicationName}` }); } + +ipcRenderer.on('buildNumber', (event, buildNumber) => { + let versionText = document.getElementById('version'); + const version = remote.app.getVersion(); + + if (versionText) { + versionText.innerHTML = version ? `Version ${version} (${version}.${buildNumber})` : 'N/A'; + } +}); \ No newline at end of file diff --git a/js/basicAuth/basic-auth.html b/js/basicAuth/basic-auth.html new file mode 100644 index 00000000..b81e7e80 --- /dev/null +++ b/js/basicAuth/basic-auth.html @@ -0,0 +1,91 @@ + + + + + Authentication Request + + + +

+ Please provide your login credentials for: + hostname +
+ + + + + + + + + + + +
User name: + +
Password: + +
+ +
+
+ + \ No newline at end of file diff --git a/js/basicAuth/index.js b/js/basicAuth/index.js new file mode 100644 index 00000000..a9443695 --- /dev/null +++ b/js/basicAuth/index.js @@ -0,0 +1,126 @@ +'use strict'; + +const electron = require('electron'); +const BrowserWindow = electron.BrowserWindow; +const ipc = electron.ipcMain; +const path = require('path'); +const fs = require('fs'); +const log = require('../log.js'); +const logLevels = require('../enums/logLevels.js'); + +let basicAuthWindow; + +const local = {}; + +let windowConfig = { + width: 360, + height: 270, + show: false, + modal: true, + autoHideMenuBar: true, + titleBarStyle: true, + resizable: false, + webPreferences: { + preload: path.join(__dirname, 'renderer.js'), + sandbox: true, + nodeIntegration: false + } +}; + +/** + * method to get the HTML template path + * @returns {string} + */ +function getTemplatePath() { + let templatePath = path.join(__dirname, 'basic-auth.html'); + try { + fs.statSync(templatePath).isFile(); + } catch (err) { + log.send(logLevels.ERROR, 'basic-auth: Could not find template ("' + templatePath + '").'); + } + return 'file://' + templatePath; +} + +/** + * Opens the basic auth window for authentication + * @param {String} windowName - name of the window upon which this window should show + * @param {String} hostname - name of the website that requires authentication + * @param {Function} callback + */ +function openBasicAuthWindow(windowName, hostname, callback) { + + // Register callback function + if (typeof callback === 'function') { + local.authCallback = callback; + } + + // This prevents creating multiple instances of the + // basic auth window + if (basicAuthWindow) { + if (basicAuthWindow.isMinimized()) { + basicAuthWindow.restore(); + } + basicAuthWindow.focus(); + return; + } + let allWindows = BrowserWindow.getAllWindows(); + allWindows = allWindows.find((window) => { return window.winName === windowName }); + + // if we couldn't find any window matching the window name + // it will render as a new window + if (allWindows) { + windowConfig.parent = allWindows; + } + + basicAuthWindow = new BrowserWindow(windowConfig); + basicAuthWindow.setVisibleOnAllWorkspaces(true); + basicAuthWindow.loadURL(getTemplatePath()); + + // sets the AlwaysOnTop property for the basic auth window + // if the main window's AlwaysOnTop is true + let focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow && focusedWindow.isAlwaysOnTop()) { + basicAuthWindow.setAlwaysOnTop(true); + } + + basicAuthWindow.once('ready-to-show', () => { + basicAuthWindow.show(); + }); + + basicAuthWindow.webContents.on('did-finish-load', () => { + basicAuthWindow.webContents.send('hostname', hostname); + }); + + basicAuthWindow.on('close', () => { + destroyWindow(); + }); + + basicAuthWindow.on('closed', () => { + destroyWindow(); + }); +} + +ipc.on('login', (event, args) => { + if (typeof args === 'object' && typeof local.authCallback === 'function') { + local.authCallback(args.username, args.password); + basicAuthWindow.close(); + } +}); + +ipc.on('close-basic-auth', () => { + if (basicAuthWindow) { + basicAuthWindow.close(); + } +}); + +/** + * Destroys a window + */ +function destroyWindow() { + basicAuthWindow = null; +} + + +module.exports = { + openBasicAuthWindow: openBasicAuthWindow +}; \ No newline at end of file diff --git a/js/basicAuth/renderer.js b/js/basicAuth/renderer.js new file mode 100644 index 00000000..3e99dac7 --- /dev/null +++ b/js/basicAuth/renderer.js @@ -0,0 +1,55 @@ +'use strict'; +const electron = require('electron'); +const ipc = electron.ipcRenderer; + +renderDom(); + +/** + * Method that renders application data + */ +function renderDom() { + document.addEventListener('DOMContentLoaded', function () { + loadContent(); + }); +} + +function loadContent() { + let basicAuth = document.getElementById('basicAuth'); + let cancel = document.getElementById('cancel'); + + if (basicAuth) { + basicAuth.onsubmit = (e) => { + e.preventDefault(); + submitForm(); + }; + } + + if (cancel) { + cancel.addEventListener('click', () => { + ipc.send('close-basic-auth'); + }); + } +} + +/** + * Method that gets invoked on submitting the form + */ +function submitForm() { + let username = document.getElementById('username').value; + let password = document.getElementById('password').value; + + if (username && password) { + ipc.send('login', { username, password }); + } +} + +/** + * Updates the hosts name + */ +ipc.on('hostname', (event, host) => { + let hostname = document.getElementById('hostname'); + + if (hostname){ + hostname.innerHTML = host || 'unknown'; + } +}); \ No newline at end of file diff --git a/js/config.js b/js/config.js index ba92566f..36a75e8d 100644 --- a/js/config.js +++ b/js/config.js @@ -6,6 +6,8 @@ const path = require('path'); const fs = require('fs'); const AppDirectory = require('appdirectory'); const omit = require('lodash.omit'); +const pick = require('lodash.pick'); +const difference = require('lodash.difference'); const isDevEnv = require('./utils/misc.js').isDevEnv; const isMac = require('./utils/misc.js').isMac; @@ -314,6 +316,64 @@ function updateUserConfigMac() { }); } +/** + * Method that tries to grab multiple config field from user config + * if field doesn't exist tries reading from global config + * + * @param {Array} fieldNames - array of config filed names + * @returns {Promise} - object all the config data from user and global config + */ +function getMultipleConfigField(fieldNames) { + return new Promise((resolve, reject) => { + let userConfigData; + + if (!fieldNames && fieldNames.length < 0) { + reject('cannot read config file, invalid fields'); + return; + } + + // reads user config data + readUserConfig().then((config) => { + userConfigData = pick(config, fieldNames); + let userConfigKeys = userConfigData ? Object.keys(userConfigData) : undefined; + + /** + * Condition to validate data from user config, + * if all the required fields are not present + * this tries to fetch the remaining fields from global config + */ + if (!userConfigKeys || userConfigKeys.length < fieldNames.length) { + + // remainingConfig - config field that are not present in the user config + let remainingConfig = difference(fieldNames, userConfigKeys); + + if (remainingConfig && Object.keys(remainingConfig).length > 0) { + readGlobalConfig().then((globalConfigData) => { + // assigns the remaining fields from global config to the user config + userConfigData = Object.assign(userConfigData, pick(globalConfigData, remainingConfig)); + resolve(userConfigData); + }).catch((err) => { + reject(err); + }); + } + + } else { + resolve(userConfigData); + } + }).catch(() => { + // This reads global config if there was any + // error while reading user config + readGlobalConfig().then((config) => { + userConfigData = pick(config, fieldNames); + resolve(userConfigData); + }).catch((err) => { + reject(err); + }); + }); + }); +} + + /** * Clears the cached config */ @@ -331,6 +391,7 @@ module.exports = { updateConfigField, updateUserConfigWin, updateUserConfigMac, + getMultipleConfigField, // items below here are only exported for testing, do NOT use! saveUserConfig, diff --git a/js/dialogs/showBasicAuth.js b/js/dialogs/showBasicAuth.js new file mode 100644 index 00000000..36c4b320 --- /dev/null +++ b/js/dialogs/showBasicAuth.js @@ -0,0 +1,25 @@ +'use strict'; + +const electron = require('electron'); + +const basicAuth = require('../basicAuth'); + +/** + * Having a proxy or hosts that requires authentication will allow user to + * enter their credentials 'username' & 'password' + */ +electron.app.on('login', (event, webContents, request, authInfo, callback) => { + + event.preventDefault(); + + // name of the host to display + let hostname = authInfo.host || authInfo.realm; + let browserWin = electron.BrowserWindow.fromWebContents(webContents); + let windowName = browserWin.winName || ''; + + /** + * Opens an electron modal window in which + * user can enter credentials fot the host + */ + basicAuth.openBasicAuthWindow(windowName, hostname, callback); +}); diff --git a/js/main.js b/js/main.js index ed36388f..7a6e855a 100644 --- a/js/main.js +++ b/js/main.js @@ -11,6 +11,7 @@ const urlParser = require('url'); // Local Dependencies const {getConfigField, updateUserConfigWin, updateUserConfigMac} = require('./config.js'); +const {setCheckboxValues} = require('./menus/menuTemplate.js'); const { isMac, isDevEnv } = require('./utils/misc.js'); const protocolHandler = require('./protocolHandler'); const getCmdLineArg = require('./utils/getCmdLineArg.js'); @@ -92,7 +93,7 @@ if (isMac) { * initialization and is ready to create browser windows. * Some APIs can only be used after this event occurs. */ -app.on('ready', setupThenOpenMainWindow); +app.on('ready', readConfigThenOpenMainWindow); /** * Is triggered when all the windows are closed @@ -127,6 +128,20 @@ app.on('open-url', function(event, url) { handleProtocolAction(url); }); +/** + * Reads the config fields that are required for the menu items + * then opens the main window + * + * This is a workaround for the issue where the menu template was returned + * even before the config data was populated + * https://perzoinc.atlassian.net/browse/ELECTRON-154 + */ +function readConfigThenOpenMainWindow() { + setCheckboxValues() + .then(setupThenOpenMainWindow) + .catch(setupThenOpenMainWindow) +} + /** * Sets up the app (to handle various things like config changes, protocol handling etc.) * and opens the main window diff --git a/js/menus/menuTemplate.js b/js/menus/menuTemplate.js index ab822603..1010426a 100644 --- a/js/menus/menuTemplate.js +++ b/js/menus/menuTemplate.js @@ -1,7 +1,7 @@ 'use strict'; const electron = require('electron'); -const { getConfigField, updateConfigField } = require('../config.js'); +const { updateConfigField, getMultipleConfigField } = require('../config.js'); const AutoLaunch = require('auto-launch'); const isMac = require('../utils/misc.js').isMac; const log = require('../log.js'); @@ -13,8 +13,6 @@ let minimizeOnClose = false; let launchOnStartup = false; let isAlwaysOnTop = false; -setCheckboxValues(); - let symphonyAutoLauncher; if (isMac) { @@ -266,39 +264,42 @@ function getTemplate(app) { * based on configuration */ function setCheckboxValues() { - getConfigField('minimizeOnClose').then(function(mClose) { - minimizeOnClose = mClose; - }).catch(function(err) { - let title = 'Error loading configuration'; - log.send(logLevels.ERROR, 'MenuTemplate: error getting config field minimizeOnClose, error: ' + err); - electron.dialog.showErrorBox(title, title + ': ' + err); + return new Promise((resolve) => { + /** + * Method that reads multiple config fields + */ + getMultipleConfigField(['minimizeOnClose', 'launchOnStartup', 'alwaysOnTop', 'notificationSettings']) + .then(function (configData) { + for (let key in configData) { + if (configData.hasOwnProperty(key)) { // eslint-disable-line no-prototype-builtins + switch (key) { + case 'minimizeOnClose': + minimizeOnClose = configData[key]; + break; + case 'launchOnStartup': + launchOnStartup = configData[key]; + break; + case 'alwaysOnTop': + isAlwaysOnTop = configData[key]; + eventEmitter.emit('isAlwaysOnTop', configData[key]); + break; + case 'notificationSettings': + eventEmitter.emit('notificationSettings', configData[key]); + break; + default: + break; + } + } + } + return resolve(); + }) + .catch((err) => { + let title = 'Error loading configuration'; + log.send(logLevels.ERROR, 'MenuTemplate: error reading configuration fields, error: ' + err); + electron.dialog.showErrorBox(title, title + ': ' + err); + return resolve(); + }); }); - - getConfigField('launchOnStartup').then(function(lStartup) { - launchOnStartup = lStartup; - }).catch(function(err) { - let title = 'Error loading configuration'; - log.send(logLevels.ERROR, 'MenuTemplate: error getting config field launchOnStartup, error: ' + err); - electron.dialog.showErrorBox(title, title + ': ' + err); - }); - - getConfigField('alwaysOnTop').then(function(mAlwaysOnTop) { - isAlwaysOnTop = mAlwaysOnTop; - eventEmitter.emit('isAlwaysOnTop', isAlwaysOnTop); - }).catch(function(err) { - let title = 'Error loading configuration'; - log.send(logLevels.ERROR, 'MenuTemplate: error getting config field alwaysOnTop, error: ' + err); - electron.dialog.showErrorBox(title, title + ': ' + err); - }); - - getConfigField('notificationSettings').then(function(notfObject) { - eventEmitter.emit('notificationSettings', notfObject); - }).catch(function(err) { - let title = 'Error loading configuration'; - log.send(logLevels.ERROR, 'MenuTemplate: error getting config field notificationSettings, error: ' + err); - electron.dialog.showErrorBox(title, title + ': ' + err); - }); - } function getMinimizeOnClose() { @@ -307,5 +308,6 @@ function getMinimizeOnClose() { module.exports = { getTemplate: getTemplate, - getMinimizeOnClose: getMinimizeOnClose + getMinimizeOnClose: getMinimizeOnClose, + setCheckboxValues: setCheckboxValues }; \ No newline at end of file diff --git a/js/notify/assets/symphony-logo-black.png b/js/notify/assets/symphony-logo-black.png new file mode 100644 index 00000000..8ba73198 Binary files /dev/null and b/js/notify/assets/symphony-logo-black.png differ diff --git a/js/notify/assets/symphony-logo-white.png b/js/notify/assets/symphony-logo-white.png new file mode 100644 index 00000000..1a484192 Binary files /dev/null and b/js/notify/assets/symphony-logo-white.png differ diff --git a/js/notify/electron-notify-preload.js b/js/notify/electron-notify-preload.js index afdca896..589385f9 100644 --- a/js/notify/electron-notify-preload.js +++ b/js/notify/electron-notify-preload.js @@ -9,6 +9,8 @@ const electron = require('electron'); const ipc = electron.ipcRenderer; +const whiteColorRegExp = new RegExp(/^(?:white|#fff(?:fff)?|rgba?\(\s*255\s*,\s*255\s*,\s*255\s*(?:,\s*1\s*)?\))$/i); + /** * Sets style for a notification * @param config @@ -19,7 +21,9 @@ function setStyle(config) { let container = notiDoc.getElementById('container'); let header = notiDoc.getElementById('header'); let image = notiDoc.getElementById('image'); + let logo = notiDoc.getElementById('symphony-logo'); let title = notiDoc.getElementById('title'); + let pod = notiDoc.getElementById('pod'); let message = notiDoc.getElementById('message'); let close = notiDoc.getElementById('close'); @@ -37,8 +41,12 @@ function setStyle(config) { setStyleOnDomElement(config.defaultStyleImage, image); + setStyleOnDomElement(config.defaultStyleLogo, logo); + setStyleOnDomElement(config.defaultStyleTitle, title); + setStyleOnDomElement(config.defaultStylePod, pod); + setStyleOnDomElement(config.defaultStyleText, message); setStyleOnDomElement(config.defaultStyleClose, close); @@ -75,6 +83,20 @@ function setContents(event, notificationObj) { if (notificationObj.color) { container.style.backgroundColor = notificationObj.color; + let logo = notiDoc.getElementById('symphony-logo'); + + if (notificationObj.color.match(whiteColorRegExp)) { + logo.src = './assets/symphony-logo-black.png'; + } else { + let title = notiDoc.getElementById('title'); + let pod = notiDoc.getElementById('pod'); + let message = notiDoc.getElementById('message'); + + message.style.color = '#ffffff'; + title.style.color = '#ffffff'; + pod.style.color = notificationObj.color; + logo.src = './assets/symphony-logo-white.png'; + } } if (notificationObj.flash) { diff --git a/js/notify/electron-notify.html b/js/notify/electron-notify.html index 05243059..ec8c2c2b 100644 --- a/js/notify/electron-notify.html +++ b/js/notify/electron-notify.html @@ -2,11 +2,17 @@
-