Skip to content

Instantly share code, notes, and snippets.

@Harti
Created March 23, 2018 10:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Harti/47c88e2e1f7453bed70fe0eee5a4260c to your computer and use it in GitHub Desktop.
Save Harti/47c88e2e1f7453bed70fe0eee5a4260c to your computer and use it in GitHub Desktop.
/**
* Class WebAppWindow
*/
class WebAppWindow {
/**
* Constructs a WebAppWindow instance
* @param {String or Object} profile Either the profile ID or a complete profile object
* @param {Function} cbSuccess Callback function to be called on success
* @param {Function} cbFailure Callback function to be called on failure
*/
constructor(profile, cbSuccess, cbFailure) {
this.isHidden = true;
this.nwWindow = null;
this.webappVersion = null;
this.cancelled = false;
this.showWebAppOnLoaded = true;
this.notifier = null;
this.zoomNotification = null;
this.onOpenListenerInstalled = false;
this.cbSuccess = cbSuccess;
this.cbFailure = cbFailure;
this.webAppSettings = {};
switch (typeof profile) {
case 'string':
this.profile = JSON.parse(JSON.stringify(global.settings.profiles[profile]));
break;
case 'object':
this.profile = JSON.parse(JSON.stringify(profile));
break;
default:
this.cbFailure({
message: global.gt.gettext('Failed to load profile.'),
MAPI_E: null
});
break;
}
this.useSSO = this.profile.authType === 'sso';
this.windowOptions = {
'title': 'Kopano DeskApp',
'width': 1366,
'height': 768,
'id': 'webapp_' + this.profile.id,
'show': false,
'inject_js_start': "app/injection/inject_start.js",
'inject_js_end': "app/injection/inject_end.js"
};
}
/**
* Remove all WebApp Cookies
* @param {NWWindow} win The NWWindow that should be used for removing
* @param {String} url The url to clean up the cookies for
*/
static removeWebappCookies(win, url, callback) {
let count = 0;
win.cookies.getAll({
url: url.replace("index.php?logon", "")
},
function(cookies) {
if (cookies.length === 0) {
callback();
}
for (let cookie of cookies) {
console.log("Removing cookie:", cookie.name);
win.cookies.remove({
url: url.replace("index.php?logon", ""),
name: cookie.name
},
(details) => {
count++;
if (count >= cookies.length) {
callback();
}
});
}
}
);
}
/**
* Compares and saves the current webapp version to the previous one.
* If it has changed the cache is cleared.
* @param {String} curVer current webapp version string
*/
static clearCache(curVer) {
var lastVer = global.settings.general.lastWebappVersion || null;
if (!curVer || (curVer === lastVer)) {
return;
}
global.settings.general.lastWebappVersion = curVer;
global.DA.saveSettings();
nw.App.clearCache();
}
/**
* Create a NWWindow and login to webapp
*/
createWindowAndLogin() {
this.visibleWindow = global.DA.winWelcome.window;
WebAppWindow.removeWebappCookies(global.DA.winWelcome, this.getUrl(this.profile.requestOptions.url), () => {this.login()});
}
getUrl(url) {
if(!this.profile.requestOptions.url.match(/^https.*/))
{
return this.profile.requestOptions.url.replace("//", "https://");
}
return url;
}
/**
* Create or reload the window for webapp
*/
createOrReloadWindow() {
let url = this.getUrl(this.profile.requestOptions.url.replace("index.php?logon", ""));
if (this.nwWindow && this.nwWindow.window && this.nwWindow.window.location.href === url) {
this.reload();
return;
}
nw.Window.open(url, this.windowOptions, (win) => {
if (!win) {
return;
}
this.nwWindow = win;
global.DA.winWebApp = win;
this.setWebAppListeners();
});
}
/**
* Validates profile without creating a NWWindow.
*/
validateAccountfromPreferences() {
this.visibleWindow = global.DA.winPreferences.window;
WebAppWindow.removeWebappCookies(global.DA.winWelcome, this.getUrl(this.profile.requestOptions.url), () => {this.login(false)});
}
/**
* Runs a login and checks if it was successful.
*/
login(openWindow = true) {
this.destroySession();
var req = {
id: -1,
headerXZarafaHresult: undefined,
headerXZarafa: undefined,
redirect301: false,
wwwAuthMethods: {},
};
/**
* Remove all webRequest handlers set up by login.
*/
function removeWebRequestHandlers() {
['onBeforeRequest', 'onHeadersReceived', 'onCompleted', 'onAuthRequired', 'onErrorOccurred']
.forEach(function(item) {
chrome.webRequest[item].removeListener(webRequestHandlers[item + 'Handler']);
});
}
/* webRequest eventhandler functions */
let webRequestHandlers = {
/**
* Event handler for the onBeforeRequest event
* Sets the request id that should be watched
* @param {Object} details the details of the request
* @return {BlockingResponse} only if the user aborted the login
*/
onBeforeRequestHandler: (details) => {
if (req.id === -1) {
req.id = details.requestId;
}
if (this.cancelled) {
return {
cancel: true
};
}
},
/**
* Event handler for the onHeadersReceived event
* Determines if the request was successful by analyzing the received headers
* @param {Object} details the details of the request
* @return {BlockingResponse} only if the user aborted the login
*/
onHeadersReceivedHandler: (details) => {
if (details.requestId !== req.id) {
return;
}
if (this.cancelled) {
return {
cancel: true
};
}
for (var i = 0; i < details.responseHeaders.length; i++) {
let header = details.responseHeaders[i];
switch (header.name.toLowerCase()) {
case "x-zarafa-hresult":
req.headerXZarafaHresult = header.value;
break;
case "x-zarafa":
req.headerXZarafa = header.value;
this.webappVersion = header.value;
WebAppWindow.clearCache(header.value);
break;
case "www-authenticate":
req.wwwAuthMethods[header.value.split(' ')[0]] = true;
break;
case 'location':
req.location = header.value;
break;
}
}
console.log("HTTP statuscode: " + details.statusCode);
if (details.statusCode === 301) {
req.redirect301 = true;
this.profile.requestOptions.url = this.getUrl(req.location + 'index.php?logon');
}
req.statusCode = details.statusCode;
},
/**
* Event handler for the onAuthRequired event
* Determines if the request was successful by analyzing the received headers
* @param {Object} details the details of the request
* @param {Function} callbackFn Callback function that returns the auth object
* @return {BlockingResponse} can cancel the request or return auth credentials
*/
onAuthRequiredHandler: (details, callbackFn) => {
if (details.requestId !== req.id) {
return;
}
if (this.cancelled) {
callbackFn({
cancel: true
});
}
if (!details.isProxy && (details.scheme === 'basic' || details.scheme === 'digest')) {
callbackFn({
'authCredentials': {
'username': this.profile.requestOptions.form.username,
'password': DAEncryption.decrypt(this.profile.requestOptions.form.password)
}
});
} else {
if (details.statusCode === 401) {
req.isProxy = details.isProxy;
this.onError('AUTHREQUIRED', req);
}
}
},
/**
* Event handler for the onCompleted event
* Checks if the login was successful and calls onError or onSuccess accordingly
* Then removes all webRequest handlers
* @param {Object} details the details of the request
*/
onCompletedHandler: (details) => {
if (details.requestId !== req.id) {
return;
}
if (!this.cancelled) {
/* In some cases the headers are returned here */
for (var i = 0; i < details.responseHeaders.length; i++) {
let header = details.responseHeaders[i];
switch (header.name.toLowerCase()) {
case "x-zarafa-hresult":
req.headerXZarafaHresult = header.value;
break;
case "x-zarafa":
req.headerXZarafa = header.value;
this.webappVersion = header.value;
WebAppWindow.clearCache(header.value);
break;
}
}
if (req.headerXZarafa && !req.headerXZarafaHresult) {
if (openWindow) {
this.createOrReloadWindow();
}
this.onSuccess(req);
} else {
if (req.redirect301) {
this.profile.requestOptions.url = this.getUrl(req.location + 'index.php?logon');
this.login(openWindow);
} else {
req.openWindow = openWindow;
this.onError('COMPLETED', req);
}
}
}
removeWebRequestHandlers();
},
/**
* Event handler for the onError event
* If this event is triggered something failed completely
* Calls onError if the request wasn't cancelled already
* Then removes all webRequest handlers
* @param {Object} details the details of the request
*/
onErrorOccuredHandler: (details) => {
if (details.requestId !== req.id) {
return;
}
if (!this.cancelled) {
this.onError('ERROROCCURED', details);
}
removeWebRequestHandlers();
this.close(true);
}
};
chrome.webRequest.onBeforeRequest.addListener(
webRequestHandlers.onBeforeRequestHandler, {
urls: ['<all_urls>']
}, ["blocking"]
);
chrome.webRequest.onHeadersReceived.addListener(
webRequestHandlers.onHeadersReceivedHandler, {
urls: ['<all_urls>']
}, ['blocking', 'responseHeaders']
);
chrome.webRequest.onCompleted.addListener(
webRequestHandlers.onCompletedHandler, {
urls: ['<all_urls>']
}, ['responseHeaders']
);
chrome.webRequest.onAuthRequired.addListener(
webRequestHandlers.onAuthRequiredHandler, {
urls: ['<all_urls>']
}, ['asyncBlocking', 'responseHeaders']
);
chrome.webRequest.onErrorOccurred.addListener(
webRequestHandlers.onErrorOccuredHandler, {
urls: ['<all_urls>']
}
);
var data = new FormData();
if (!this.useSSO) {
data.append('username', this.profile.requestOptions.form.username);
data.append('password', DAEncryption.decrypt(this.profile.requestOptions.form.password));
}
var xhr = new XMLHttpRequest();
xhr.open("POST", this.getUrl(this.profile.requestOptions.url));
xhr.withCredentials = true;
xhr.send(data);
}
/**
* Checks error details and converts it to a user firendly message
* Then calls cbFailure with an object containing the error message
* @param {String} type Type of the error
* @param {Object} details Error details
*/
onError(type, details) {
var s_admin = ' ' + global.gt.gettext('Please contact your system administrator.');
var msg = global.gt.gettext('Unexpected error occured.' + s_admin);
switch (type) {
case 'AUTHREQUIRED':
if (details.isProxy) {
msg = 'WebApp' + ' ';
} else {
msg = 'Proxy' + ' ';
}
msg += global.gt.gettext('Single Sign On failed. Please provide username and password.');
break;
case 'COMPLETED':
var MAPI_E = details.headerXZarafaHresult;
if (MAPI_E) {
switch (MAPI_E) {
case 'MAPI_E_LOGON_FAILED':
case 'MAPI_E_UNCONFIGURED':
msg = global.gt.gettext('Could not login, invalid username or password');
break;
case 'MAPI_E_NETWORK_ERROR':
msg = global.gt.gettext('Cannot connect to Kopano Core.');
break;
case 'MAPI_E_INVALID_WORKSTATION_ACCOUNT':
//msg = global.gt.gettext('Logon failed, another session already exists.');
this.destroySession();
this.login(details.openWindow);
return;
default:
msg = global.gt.gettext('Login failed, MAPI errror: ') + MAPI_E;
break;
}
} else {
switch (details.statusCode) {
case 200:
if (this.useSSO && !(details.wwwAuthMethods['ntlm'] ||
details.wwwAuthMethods['negotiate'])) {
msg = global.gt.gettext(
'Single Sign On not supported by your webserver. Please enter Username and Password.');
}
break;
case 401:
if (details.wwwAuthMethods['ntlm'] || details.wwwAuthMethods['negotiate']) {
msg = global.gt.gettext(
'The WebApp server requires SSO authentication. Please logon via SSO.');
} else {
msg = global.gt.gettext('Unhandled authentication error.' + s_admin);
}
break;
case 404:
msg = global.gt.gettext('Page not found on webserver.');
break;
case 500:
msg = global.gt.gettext('Internal server error.') + s_admin;
break;
default:
msg = global.gt.gettext('Unhandled http status code ') + details.statusCode + "." + s_admin;
}
}
break;
case 'ERROROCCURED':
switch (details.error) {
case 'net::ERR_NETWORK_ACCESS_DENIED':
msg = global.gt.gettext('Your Internet access is blocked.');
break;
case 'net::ERR_INTERNET_DISCONNECTED':
msg = global.gt.gettext('There is no Internet connection.');
break;
case 'net::ERR_CONNECTION_INTERRUPTED':
msg = global.gt.gettext('Your connection was interrupted.');
break;
case 'net::ERR_CONNECTION_REFUSED':
msg = global.gt.gettext('Connection refused.') + s_admin;
break;
case 'net::ERR_CONNECTION_TIMED_OUT':
msg = global.gt.gettext('Connection timed out.') + s_admin;
break;
case 'net::ERR_NAME_NOT_RESOLVED':
msg = global.gt.gettext('Could not resolve domain.') + s_admin;
break;
case 'net::ERR_BLOCKED_BY_CLIENT':
case 'net::ERR_BLOCKED_BY_ADMINISTRATOR':
case 'net::ERR_BLOCKED_ENROLLMENT_CHECK_PENDING':
case 'net::ERR_BLOCKED_BY_RESPONSE':
case 'net::ERR_BLOCKED_BY_XSS_AUDITOR':
msg = global.gt.gettext('Access to WebApp is blocked.');
break;
case 'net::ERR_ACCESS_DENIED':
msg = global.gt.gettext('Access to WebApp was denied.');
break;
default:
msg = global.gt.gettext('Error in connection. Reason: ' + details.error);
break;
}
break;
}
this.cbFailure({
message: msg,
MAPI_E: details.headerXZarafaHresult || null
});
}
/**
* Called when the login was successful
* Calls cbSuccess with the authtype and the request details
* @param {Object} req The collected details of the request
*/
onSuccess(req) {
this.profile.authType = 'webapp';
if (this.useSSO) {
this.profile.authType = 'sso';
} else if (req.wwwAuthMethods['basic']) {
this.profile.authType = 'basic';
}
req.profile = this.profile;
this.cbSuccess(req);
}
/**
* Show the webapp window
* @param {Boolean} focus If the shown window should be focussed
*/
show(focus) {
if (!this.nwWindow) {
return;
}
if (this.nwWindow.appWindow.isMinimized()) {
this.restore();
}
this.nwWindow.setShowInTaskbar(true);
this.nwWindow.show();
this.isHidden = false;
if (focus) {
this.focus();
}
}
/**
* Hides the webapp window
*/
hide() {
this.nwWindow.hide();
this.isHidden = true;
}
/**
* Toggles the taskbar visibility
* @param {Boolean} show If true show else hide
*/
setShowInTaskbar(show = true) {
this.nwWindow.setShowInTaskbar(show);
}
/**
* Does a reload of webapp
* @param {Boolean} silent If true reload in the background
*/
reload() {
this.nwWindow.window.onbeforeunload = null;
this.nwWindow.reload();
}
/**
* Restores a minimized webapp window
*/
restore() {
this.nwWindow.restore();
this.isHidden = false;
}
/**
* Focus the webapp window
*/
focus() {
this.nwWindow.focus();
}
/**
* Close the webapp window
* @param {Boolean} force If true the window is closed immediately
*/
close(force = false) {
if (this.nwWindow) {
localStorage.setItem('wasMaximizedOnClose', this.nwWindow.appWindow.isMaximized() ||
this.nwWindow.appWindow.isFullscreen());
this.nwWindow.close(force);
}
}
/**
* Cancel everything and close the window
*/
cancel() {
this.cancelled = true;
this.destroyWindow();
this.nwWindow = null;
this.showWebAppOnLoaded = true;
}
/**
* This is executed when the connection to webapp has been paralyzed.
*/
onParalyzed() {
let wasHiddenOrMinimized = this.isHidden || this.nwWindow.appWindow.isMinimized();
if (!wasHiddenOrMinimized) {
this.hide();
global.DA.winWelcome.window.showLoaderForm();
global.DA.winWelcome.show();
} else {
this.showWebAppOnLoaded = false;
}
this.login();
}
/**
* Event handler that is called when the logout button is clicked
*/
onLogout() {
global.DA.winWelcome.window.showWelcomeForm();
this.destroySession();
this.close(true);
this.nwWindow = null;
global.DA.winWelcome.show();
this.showWebAppOnLoaded = true;
}
/**
* The session is not always destroyed correctly by container.logout.
* So be sure it's gone, by calling index.php?logout
*/
destroySession() {
var xhr = new XMLHttpRequest();
xhr.open("GET", this.getUrl(this.profile.requestOptions.url.replace('index.php?logon', 'index.php?logout')), false);
xhr.withCredentials = true;
try {
xhr.send();
} catch(err) {
console.log('Destroying the session failed.', err);
}
}
/**
* If a NWWindow is left in a bad state, it needs to be closed
* in a special way. Open it with it's id and url "about:blank",
* then close it immediately.
*/
destroyWindow() {
if (this.nwWindow) {
nw.Window.open('about:blank', this.windowOptions, (win) => {
if (win) {
win.close(true);
}
});
}
}
/**
* Event handler that is called when webapp has loaded completely
*/
onLoaded() {
if (this.cancelled) {
this.close(true);
}
this.readWebAppSettings();
this.setZoomListeners();
this.setupParameterHandling();
global.DA.winWelcome.hide();
if (this.showWebAppOnLoaded) {
if (localStorage.getItem('wasMaximizedOnClose') === 'true') {
this.nwWindow.maximize();
}
this.show(true);
}
}
/**
* Event handler that is called when webapp does a reload
*/
onReload() {
this.hide();
global.DA.winWelcome.window.showLoaderForm();
global.DA.winWelcome.show();
}
/**
* Event handler that is called when the webapp window is minimized
*/
onMinimze() {
if (global.settings.general.hideOnMinimize) {
if (global.DA.isMacOS) {
this.setShowInTaskbar(false);
} else {
this.hide();
}
}
}
/**
* Event handler that is called when the webapp window is closed
*/
onClose() {
if (global.DA.winWelcome) {
global.DA.winWelcome.close(true);
}
if (global.DA.winPreferences) {
global.DA.winPreferences.close(true);
}
this.close(true);
nw.App.quit();
}
/**
* Sets listeners for the 'close', 'minimize', and 'new-win-policy' events
* of the WebApp window and also the custom events that are emitted by the injected script
*/
setWebAppListeners() {
this.nwWindow.on('close', () => {
this.onClose();
});
this.nwWindow.on('minimize', () => {
this.onMinimze();
});
this.nwWindow.on('new-win-policy', (frame, url, policy) => {
var isWebmeetingsUrl = url.startsWith(this.webAppSettings.webMeetingsUrl);
if (url) {
if (isWebmeetingsUrl) {
nw.Window.open(url, {
'title': 'Kopano DeskApp',
'width': 950,
'height': 600,
'position': 'center',
'inject_js_start': 'app/injection/inject_start.js',
'inject_js_end': 'app/injection/inject_webmeetings_end.js',
}, function(){});
} else {
nw.Shell.openExternal(url);
}
policy.ignore();
}
});
// Not for MacOS
if (!global.DA.isMacOS) {
this.nwWindow.on('enter-fullscreen', () => {
// global shortcut to leave fullscreen
var escapeShortcut = new nw.Shortcut({
key: "Escape",
active: () => {
if (this.nwWindow) {
this.nwWindow.leaveFullscreen();
nw.App.unregisterGlobalHotKey(escapeShortcut);
}
}
});
nw.App.registerGlobalHotKey(escapeShortcut);
});
}
const EventEmitter = require('events');
class WinWebAppEmitter extends EventEmitter {}
this.nwWindow.customEventEmitter = new WinWebAppEmitter();
this.nwWindow.customEventEmitter.on('WAparalyzed', () => {
this.onParalyzed();
});
this.nwWindow.customEventEmitter.on('WAloaded', () => {
this.onLoaded();
});
this.nwWindow.customEventEmitter.on('WAlogout', () => {
this.onLogout();
});
this.nwWindow.customEventEmitter.on('WAreload', () => {
this.onReload();
});
}
/**
* Adds listeners to zoom in and out.
*/
setZoomListeners() {
if (this.notifier) {
this.notifier = null;
}
/* load zoom level from localStorage */
if (global.settings.general.zoomLevel) {
this.nwWindow.zoomLevel = parseFloat(global.settings.general.zoomLevel);
}
/**
* Zoom in or out.
* CTRL + mousewheel and CTRL + "+/-"
* @param {Event} e event calling the function
*/
let zoom = (e) => {
if (e.ctrlKey) {
var zoomLevel, newZoomFactor;
var zoomStep = 5;
var zoomFactor = global.settings.general.zoomFactor ? parseInt(global.settings.general.zoomFactor) :
100;
/*
* That's not nice, but we can't detect the keyboard layout.
* NumpadSubtract and NumpadAdd, as the names say,
* BracketRight and Slash are plus and minus on a qwertz keyboard
* ShiftKey = true + Equal and Minus are plus and minus on a qwerty keyboard
* deltaY is the mousewheel
*/
if (e.deltaY > 0 ||
e.code === 'NumpadSubtract' ||
e.code === 'Slash' ||
e.code === 'Minus') {
newZoomFactor = zoomFactor - zoomStep;
} else if (e.deltaY < 0 ||
e.code === 'NumpadAdd' ||
e.code === 'BracketRight' ||
(e.shiftKey && e.code === 'Equal')) {
newZoomFactor = zoomFactor + zoomStep;
}
if (newZoomFactor) {
zoomLevel = global.DA.zoomFactorToLevel(newZoomFactor);
this.nwWindow.zoomLevel = zoomLevel;
/* store zoom factor and level to localStorage */
var zoomSettings = {
'general': {
'zoomFactor': newZoomFactor,
'zoomLevel': zoomLevel
}
};
global.DA.saveSettings(zoomSettings);
/* Notify the user about the current zoom factor */
if (this.notifier === null) {
this.notifier = this.nwWindow.window.container.getNotifier();
}
if (this.zoomNotification && this.nwWindow.window.document.getElementById(this.zoomNotification.id)) {
this.notifier.notify('info.sent', 'Zoom', global.settings.general.zoomFactor + '%', {
reference: this.zoomNotification,
update: true
});
} else {
this.zoomNotification = this.notifier.notify('info.sent', 'Zoom', global.settings.general.zoomFactor +
'%');
}
}
}
};
this.nwWindow.window.document.addEventListener("keydown", zoom);
this.nwWindow.window.document.addEventListener("wheel", zoom);
}
/**
* Set up how commandline parameters are handled.
* A determination between mac osx and others is needed, because
* apple does everything completely different.
*/
setupParameterHandling() {
if (this.onOpenListenerInstalled) {
return;
}
this.onOpenListenerInstalled = true;
var getopt = global.DA.requireCustomModule('getopt');
/* This is for the first start of deskapp */
if (WebAppWindow.isFirstWebAppLogin) {
if (global.DA.isMacOS) {
if (/^mailto/.test(nw.App.argv[0])) {
getopt.parseMailto({
'mailto': nw.App.argv[0]
});
}
/* Only one file supported by nw.js for now */
if (/^file/.test(nw.App.argv[0])) {
this.nwWindow.window.createMail({
'attach': [nw.App.argv[0]]
});
}
} else {
getopt.parseAndProcessArgv(nw.App.argv);
}
WebAppWindow.isFirstWebAppLogin = false;
}
/* Listen for the App open event from new DeskApp instances */
nw.App.on('open', (cmdline) => {
this.show(true);
console.log("on open", cmdline);
if (global.DA.isMacOS) {
if (/^mailto/.test(cmdline)) {
getopt.parseMailto({
'mailto': cmdline
});
}
/* Only one file supported by nw.js for now */
if (/^file/.test(cmdline)) {
this.nwWindow.window.createMail({
'attach': [cmdline]
});
}
} else {
getopt.parseAndProcessArgv(getopt.cmdlineToArgv(cmdline));
}
});
/*
* Listen for the reopen event. If fired set show in taskbar to true.
* This only applies to MacOS.
*/
nw.App.on('reopen', () => {
this.setShowInTaskbar(true);
});
}
/**
* Read some of webapps settings
* For now we only read webmeetings settings.
*/
readWebAppSettings() {
this.webAppSettings.webMeetingsUrl =
this.nwWindow.window.container.getSettingsModel().get('zarafa/v1/plugins/spreedwebrtc/domain') +
this.nwWindow.window.container.getSettingsModel().get('zarafa/v1/plugins/spreedwebrtc/url');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment