libreoffice-online/browser/js/global.js
Caolán McNamara 1bd1e5a828 wasm is derived from the mobile, but doesn't have on-screen keyboard
There's a bit of a mobile/wasm munge with wasm considered a subset
of mobile in some places which should be unwound a bit, but for the
purposes of usable wasm by default, don't assume that wasm has an
onscreen keyboard so we don't lose keystrokes on assuming that
there will be an onscreen keyboard appearing when we lose and regain
focus.

Signed-off-by: Caolán McNamara <caolan.mcnamara@collabora.com>
Change-Id: I0baae0d414ce9aafd1c27d74bcdad2276d104ee5
2023-12-08 14:17:27 +00:00

1110 lines
35 KiB
JavaScript

/* -*- js-indent-level: 8 -*- */
/* global Uint8Array _ */
/*
For extending window.app object, please see "docstate.js" file.
Below definition is only for the properties that this (global.js) file needs at initialization.
*/
window.app = {
socket: null,
console: {}
};
(function (global) {
global.logServer = function (log) {
if (window.ThisIsAMobileApp) {
window.postMobileError(log);
} else if (global.socket && (global.socket instanceof WebSocket) && global.socket.readyState === 1) {
global.socket.send(log);
} else if (global.socket && global.L && global.app.definitions.Socket &&
(global.socket instanceof global.app.definitions.Socket) && global.socket.connected()) {
global.socket.sendMessage(log);
} else {
var req = new XMLHttpRequest();
var url = global.location.protocol + '//' + global.location.host + global.location.pathname.match(/.*\//) + 'logging.html';
req.open('POST', url, true);
req.setRequestHeader('Content-type','application/json; charset=utf-8');
req.send(log);
}
};
// enable later toggling
global.setLogging = function(doLogging)
{
var loggingMethods = ['error', 'warn', 'info', 'debug', 'trace', 'log', 'assert', 'time', 'timeEnd', 'group', 'groupEnd'];
if (!doLogging) {
var noop = function() {};
for (var i = 0; i < loggingMethods.length; i++) {
window.app.console[loggingMethods[i]] = noop;
}
} else {
for (var i = 0; i < loggingMethods.length; i++) {
if (!Object.prototype.hasOwnProperty.call(window.console, loggingMethods[i])) {
continue;
}
(function(method) {
window.app.console[method] = function logWithCool() {
var args = Array.prototype.slice.call(arguments);
return window.console[method].apply(console, args);
};
}(loggingMethods[i]));
}
window.onerror = function (msg, src, row, col, err) {
var data = {
userAgent: navigator.userAgent.toLowerCase(),
vendor: navigator.vendor.toLowerCase(),
message: msg,
source: src,
line: row,
column: col
};
var desc = err ? err.message || '(no message)': '(no err)', stack = err ? err.stack || '(no stack)': '(no err)';
var log = 'jserror ' + JSON.stringify(data, null, 2) + '\n' + desc + '\n' + stack + '\n';
global.logServer(log);
return false;
};
}
};
global.setLogging(global.coolLogging == 'true');
global.getParameterByName = function (name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
var results = regex.exec(location.search);
return results === null ? '' : results[1].replace(/\+/g, ' ');
};
var ua = navigator.userAgent.toLowerCase(),
uv = navigator.vendor.toLowerCase(),
doc = document.documentElement,
ie = 'ActiveXObject' in window,
cypressTest = ua.indexOf('cypress') !== -1,
webkit = ua.indexOf('webkit') !== -1,
phantomjs = ua.indexOf('phantom') !== -1,
android23 = ua.search('android [23]') !== -1,
chrome = ua.indexOf('chrome') !== -1,
gecko = (ua.indexOf('gecko') !== -1 || (cypressTest && 'MozUserFocus' in doc.style))
&& !webkit && !window.opera && !ie,
safari = !chrome && (ua.indexOf('safari') !== -1 || uv.indexOf('apple') == 0),
win = navigator.platform.indexOf('Win') === 0,
mobile = typeof orientation !== 'undefined' || ua.indexOf('mobile') !== -1,
msPointer = !window.PointerEvent && window.MSPointerEvent,
pointer = (window.PointerEvent && navigator.pointerEnabled && navigator.maxTouchPoints) || msPointer,
ie3d = ie && ('transition' in doc.style),
webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23,
gecko3d = 'MozPerspective' in doc.style,
opera12 = 'OTransition' in doc.style;
var chromebook = window.ThisIsTheAndroidApp && window.COOLMessageHandler.isChromeOS();
var isInternetExplorer = (navigator.userAgent.toLowerCase().indexOf('msie') != -1 ||
navigator.userAgent.toLowerCase().indexOf('trident') != -1);
var navigatorLang = navigator.languages && navigator.languages.length ? navigator.languages[0] :
(navigator.language || navigator.userLanguage || navigator.browserLanguage || navigator.systemLanguage);
function getFirefoxVersion() {
var version = '';
var userAgent = navigator.userAgent.toLowerCase();
if (userAgent.indexOf('firefox') !== -1) {
var matches = userAgent.match(/firefox\/([0-9]+\.*[0-9]*)/);
if (matches) {
version = matches[1];
}
}
return version;
}
global.L = {};
global.L.Params = {
/// Shows close button if non-zero value provided
closeButtonEnabled: global.getParameterByName('closebutton'),
/// Shows revision history file menu option
revHistoryEnabled: global.getParameterByName('revisionhistory'),
};
global.L.Browser = {
// @property ie: Boolean
// `true` for all Internet Explorer versions (not Edge).
ie: ie,
// @property ielt9: Boolean
// `true` for Internet Explorer versions less than 9.
ielt9: ie && !document.addEventListener,
// @property edge: Boolean
// `true` for the Edge web browser.
edge: 'msLaunchUri' in navigator && !('documentMode' in document),
// @property webkit: Boolean
// `true` for webkit-based browsers like Chrome and Safari (including mobile versions).
webkit: webkit,
// @property gecko: Boolean
// `true` for gecko-based browsers like Firefox.
gecko: gecko,
// @property geckoVersion: String
// Firefox version: abc.d.
geckoVersion: getFirefoxVersion(),
// @property android: Boolean
// `true` for any browser running on an Android platform.
android: ua.indexOf('android') !== -1,
// @property android23: Boolean
// `true` for browsers running on Android 2 or Android 3.
android23: android23,
// @property chrome: Boolean
// `true` for the Chrome browser.
chrome: chrome,
// @property safari: Boolean
// `true` for the Safari browser.
safari: safari,
// @property win: Boolean
// `true` when the browser is running in a Windows platform
win: win,
// @property ie3d: Boolean
// `true` for all Internet Explorer versions supporting CSS transforms.
ie3d: ie3d,
// @property isInternetExplorer: Boolean
// `true` for Internet Explorer
isInternetExplorer: isInternetExplorer,
// @property webkit3d: Boolean
// `true` for webkit-based browsers supporting CSS transforms.
webkit3d: webkit3d,
// @property gecko3d: Boolean
// `true` for gecko-based browsers supporting CSS transforms.
gecko3d: gecko3d,
// @property opera12: Boolean
// `true` for the Opera browser supporting CSS transforms (version 12 or later).
opera12: opera12,
// @property any3d: Boolean
// `true` for all browsers supporting CSS transforms.
any3d: !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d) && !opera12 && !phantomjs,
// @property mobile: Boolean
// `true` for all browsers running in a mobile device.
mobile: mobile,
// @property mobileWebkit: Boolean
// `true` for all webkit-based browsers in a mobile device.
mobileWebkit: mobile && webkit,
// @property mobileWebkit3d: Boolean
// `true` for all webkit-based browsers in a mobile device supporting CSS transforms.
mobileWebkit3d: mobile && webkit3d,
// @property mobileOpera: Boolean
// `true` for the Opera browser in a mobile device.
mobileOpera: mobile && window.opera,
// @property mobileGecko: Boolean
// `true` for gecko-based browsers running in a mobile device.
mobileGecko: mobile && gecko,
// @property cypressTest: Boolean
// `true` when the browser run by cypress
cypressTest: cypressTest,
// @property msPointer: Boolean
// `true` for browsers implementing the Microsoft touch events model (notably IE10).
msPointer: !!msPointer,
// @property pointer: Boolean
// `true` for all browsers supporting [pointer events](https://msdn.microsoft.com/en-us/library/dn433244%28v=vs.85%29.aspx).
pointer: !!pointer,
// @property retina: Boolean
// `true` for browsers on a high-resolution "retina" screen.
retina: (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1,
// @property lang: String
// browser language locale
lang: navigatorLang
};
global.keyboard = {
onscreenKeyboardHint: window.uiDefaults['onscreenKeyboardHint'],
// If there's an onscreen keyboard, we don't want to trigger it with innocuous actions like panning around a spreadsheet
// on the other hand, if there is a hardware keyboard we want to do things like focusing contenteditables so that typing is
// recognized without tapping again. This is an impossible problem, because browsers do not give us enough information
// Instead, let's just guess
guessOnscreenKeyboard: function() {
if (global.keyboard.onscreenKeyboardHint != undefined) return global.keyboard.onscreenKeyboardHint;
return (window.ThisIsAMobileApp && !window.ThisIsTheEmscriptenApp) || global.mode.isMobile() || global.mode.isTablet();
// It's better to guess that more devices will have an onscreen keyboard than reality,
// because calc becomes borderline unusable if you miss a device that pops up an onscreen keyboard which covers
// a sizeable portion of the screen
},
// alternatively, maybe someone else (e.g. an integrator) knows more about the situation than we do. In this case, let's
// let them override our default
hintOnscreenKeyboard: function(hint) {
global.keyboard.onscreenKeyboardHint = hint;
},
};
global.touch = {
/// a touchscreen event handler, supports both DOM and hammer.js events
isTouchEvent: function(e) {
if (L.Browser.cypressTest && global.L.Browser.mobile) {
return true; // As cypress tests on mobile tend to use "click" events instead of touches... we cheat to get them recognized as touch events
}
if (!e.pointerType) {
return !(e instanceof MouseEvent);
}
return e.pointerType === 'touch' || e.pointerType == 'pen' || e.pointerType == 'kinect';
},
/// a decorator that only runs the function if the event is a touch event
touchOnly: function(f) {
return function(e) {
if (!global.touch.isTouchEvent(e)) return;
return f.apply(this, arguments);
};
},
/// a decorator that only runs the function if the event is not a touch event
mouseOnly: function(f) {
return function(e) {
if (global.touch.isTouchEvent(e)) return;
return f.apply(this, arguments);
};
},
/// detect if the primary pointing device is of limited accuracy (generally a touchscreen)
/// you shouldn't use this for determining the behavior of an event (use isTouchEvent instead), but this may
/// be useful for determining what UI to show (e.g. the draggable teardrops under the cursor)
hasPrimaryTouchscreen: function() {
return window.matchMedia('(pointer: coarse)').matches;
},
/// detect any pointing device is of limited accuracy (generally a touchscreen)
/// you shouldn't use this for determining the behavior of an event (use isTouchEvent instead), but this may
/// be useful for determining what UI to show (e.g. the draggable teardrops under the cursor)
hasAnyTouchscreen: function() {
return window.matchMedia('(any-pointer: coarse)').matches;
},
};
global.mode = {
isChromebook: function() {
return chromebook;
},
// Here "mobile" means "mobile phone" (at least for now). Has to match small screen size
// requirement.
isMobile: function() {
if (global.mode.isChromebook())
return false;
if (global.L.Browser.mobile && L.Browser.cypressTest) {
return true;
}
return L.Browser.mobile && (screen.width < 768 || screen.height < 768);
},
// Mobile device with big screen size.
isTablet: function() {
if (global.mode.isChromebook())
return false;
return L.Browser.mobile && !window.mode.isMobile();
},
isDesktop: function() {
if (global.mode.isChromebook())
return true;
return !L.Browser.mobile;
},
getDeviceFormFactor: function() {
if (window.mode.isMobile())
return 'mobile';
else if (window.mode.isTablet())
return 'tablet';
else if (window.mode.isDesktop())
return 'desktop';
else
return null;
}
};
global.deviceFormFactor = window.mode.getDeviceFormFactor();
document.addEventListener('contextmenu', function(e) {
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
}
}, false);
global.fakeWebSocketCounter = 0;
global.FakeWebSocket = function () {
this.binaryType = 'arraybuffer';
this.bufferedAmount = 0;
this.extensions = '';
this.protocol = '';
this.readyState = 1;
this.id = window.fakeWebSocketCounter++;
this.onclose = function() {
};
this.onerror = function() {
};
this.onmessage = function() {
};
this.onopen = function() {
};
this.close = function() {
};
};
global.FakeWebSocket.prototype.send = function(data) {
window.postMobileMessage(data);
};
global.proxySocketCounter = 0;
global.ProxySocket = function (uri) {
var that = this;
this.uri = uri;
this.binaryType = 'arraybuffer';
this.bufferedAmount = 0;
this.extensions = '';
this.unloading = false;
this.protocol = '';
this.connected = true;
this.readyState = 0; // connecting
this.sessionId = 'open';
this.id = window.proxySocketCounter++;
this.msgInflight = 0;
this.openInflight = 0;
this.inSerial = 0;
this.outSerial = 0;
this.minPollMs = 25; // Anything less than ~25 ms can overwhelm the HTTP server.
this.maxPollMs = 500; // We can probably go as much as 1-2 seconds without ill-effect.
this.curPollMs = this.minPollMs; // The current poll period.
this.minIdlePollsToThrottle = 3; // This many 'no data' responses and we throttle.
this.throttleFactor = 1.15; // How rapidly to throttle. 15% takes 4s to go from 25 to 500ms.
this.lastDataTimestamp = performance.now(); // The last time we got any data.
this.onclose = function() {
};
this.onerror = function() {
};
this.onmessage = function() {
};
this.decoder = new TextDecoder();
this.doSlice = function(bytes,start,end) { return bytes.slice(start,end); };
this.decode = function(bytes,start,end) {
return this.decoder.decode(this.doSlice(bytes, start,end));
};
this.parseIncomingArray = function(arr) {
//window.app.console.debug('proxy: parse incoming array of length ' + arr.length);
for (var i = 0; i < arr.length; ++i)
{
var left = arr.length - i;
if (left < 4)
{
//window.app.console.debug('no data left');
break;
}
var type = String.fromCharCode(arr[i+0]);
if (type != 'T' && type != 'B')
{
window.app.console.debug('wrong data type: ' + type);
break;
}
i++;
// Serial
if (arr[i] !== 48 && arr[i+1] !== 120) // '0x'
{
window.app.console.debug('missing hex preamble');
break;
}
i += 2;
var numStr = '';
var start = i;
while (arr[i] != 10) // '\n'
i++;
numStr = this.decode(arr, start, i);
var serial = parseInt(numStr, 16);
i++; // skip \n
// Size:
if (arr[i] !== 48 && arr[i+1] !== 120) // '0x'
{
window.app.console.debug('missing hex preamble');
break;
}
i += 2;
start = i;
while (arr[i] != 10) // '\n'
i++;
numStr = this.decode(arr, start, i);
var size = parseInt(numStr, 16);
i++; // skip \n
var data;
if (type == 'T')
data = this.decode(arr, i, i + size);
else
data = this.doSlice(arr, i, i + size);
if (serial !== that.inSerial + 1) {
window.app.console.debug('Error: serial mismatch ' + serial + ' vs. ' + (that.inSerial + 1));
}
that.inSerial = serial;
this.onmessage({ data: data });
i += size; // skip trailing '\n' in loop-increment
}
};
this.sendQueue = '';
this._signalErrorClose = function() {
clearInterval(this.pollInterval);
clearTimeout(this.delaySession);
this.pollInterval = undefined;
this.delaySession = undefined;
if (that.readyState < 3)
{
this.onerror();
this.onclose();
}
this.sessionId = 'open';
this.inSerial = 0;
this.outSerial = 0;
this.msgInflight = 0;
this.openInflight = 0;
this.readyState = 3; // CLOSED
};
// For those who think that long-running sockets are a
// better way to wait: you're so right. However, each
// consumes a scarce server worker thread while it waits,
// so ... back in the real world:
this._setPollInterval = function(intervalMs) {
clearInterval(this.pollInterval);
if (this.readyState === 1)
this.pollInterval = setInterval(this.doSend, intervalMs);
},
this.doSend = function () {
if (that.sessionId === 'open')
{
if (that.readyState === 3)
window.app.console.debug('Error: sending on closed socket');
return;
}
if (that.msgInflight >= 4) // something went badly wrong.
{
// We shouldn't get here because we throttle sending when we
// have something in flight, but if the server hangs, we
// will do up to 3 retries before we end up here and yield.
if (that.curPollMs < that.maxPollMs)
{
that.curPollMs = Math.min(that.maxPollMs, that.curPollMs * that.throttleFactor) | 0;
window.app.console.debug('High latency connection - too much in-flight, throttling to ' + that.curPollMs + ' ms.');
that._setPollInterval(that.curPollMs);
}
else if (performance.now() - that.lastDataTimestamp > 30 * 1000)
{
window.app.console.debug('Close connection after no response for 30secs');
that._signalErrorClose();
}
else
window.app.console.debug('High latency connection - too much in-flight, pausing.');
return;
}
// Maximize the timeout, instead of stopping altogethr,
// so we don't hang when the following request takes
// too long, hangs, throws, etc. we can recover.
that._setPollInterval(that.maxPollMs);
//window.app.console.debug('send msg - ' + that.msgInflight + ' on session ' +
// that.sessionId + ' queue: "' + that.sendQueue + '"');
var req = new XMLHttpRequest();
req.open('POST', that.getEndPoint('write'));
req.responseType = 'arraybuffer';
req.addEventListener('load', function() {
if (this.status == 200)
{
var data = new Uint8Array(this.response);
if (data.length)
{
// We have some data back from WSD.
// Another user might be editing and we want
// to see their changes in real time.
that.curPollMs = that.minPollMs; // Drain fast.
that._setPollInterval(that.curPollMs);
that.lastDataTimestamp = performance.now();
that.parseIncomingArray(data);
return;
}
}
else
{
window.app.console.debug('proxy: error on incoming response ' + this.status);
that._signalErrorClose();
}
if (that.curPollMs < that.maxPollMs) // If we aren't throttled, see if we should.
{
// Has it been long enough since we got any data?
var timeSinceLastDataMs = (performance.now() - that.lastDataTimestamp) | 0;
if (timeSinceLastDataMs >= that.minIdlePollsToThrottle * that.curPollMs)
{
// Throttle.
that.curPollMs = Math.min(that.maxPollMs, that.curPollMs * that.throttleFactor) | 0;
//window.app.console.debug('No data for ' + timeSinceLastDataMs + ' ms -- throttling to ' + that.curPollMs + ' ms.');
}
}
that._setPollInterval(that.curPollMs);
});
req.addEventListener('loadend', function() {
that.msgInflight--;
});
req.send(that.sendQueue);
that.sendQueue = '';
that.msgInflight++;
};
this.getSessionId = function() {
if (this.openInflight > 0)
{
window.app.console.debug('Waiting for session open');
return;
}
if (this.delaySession)
return;
// avoid attempting to re-connect too quickly
if (global.lastCreatedProxySocket)
{
var msSince = performance.now() - global.lastCreatedProxySocket;
if (msSince < 250) {
var delay = 250 - msSince;
window.app.console.debug('Wait to re-try session creation for ' + delay + 'ms');
this.curPollMs = delay; // ms
this.delaySession = setTimeout(function() {
that.delaySession = undefined;
that.getSessionId();
}, delay);
return;
}
}
global.lastCreatedProxySocket = performance.now();
var req = new XMLHttpRequest();
req.open('POST', that.getEndPoint('open'));
req.responseType = 'text';
req.addEventListener('load', function() {
window.app.console.debug('got session: ' + this.responseText);
if (this.status !== 200 || !this.responseText ||
this.responseText.indexOf('\n') >= 0) // multi-line error
{
window.app.console.debug('Error: failed to fetch session id! error: ' + this.status);
that._signalErrorClose();
}
else // we connected - lets get going ...
{
that.sessionId = this.responseText;
that.readyState = 1;
that.onopen();
that._setPollInterval(that.curPollMs);
}
});
req.addEventListener('loadend', function() {
window.app.console.debug('Open completed state: ' + that.readyState);
that.openInflight--;
});
req.send('');
this.openInflight++;
};
this.send = function(msg) {
var hadData = this.sendQueue.length > 0;
this.sendQueue = this.sendQueue.concat(
'B0x' + this.outSerial.toString(16) + '\n' +
'0x' + (new TextEncoder().encode(msg)).length.toString(16) + '\n' + msg + '\n');
this.outSerial++;
// Send ASAP, if we have throttled.
if (that.curPollMs > that.minPollMs || !hadData)
{
// Unless we are backed up.
if (that.msgInflight <= 3)
{
//window.app.console.debug('Have data to send, lowering poll interval.');
that.curPollMs = that.minPollMs;
that._setPollInterval(that.curPollMs);
}
}
};
this.sendCloseMsg = function(beacon) {
var url = that.getEndPoint('close');
if (!beacon)
{
var req = new XMLHttpRequest();
req.open('POST', url);
req.send('');
}
else
navigator.sendBeacon(url, '');
};
this.close = function() {
var oldState = this.readyState;
window.app.console.debug('proxy: close socket');
this.readyState = 3;
this.onclose();
clearInterval(this.pollInterval);
clearTimeout(this.delaySession);
this.pollInterval = undefined;
if (oldState === 1) // was open
this.sendCloseMsg(this.unloading);
this.sessionId = 'open';
};
this.setUnloading = function() {
this.unloading = true;
};
this.getEndPoint = function(command) {
var base = this.uri;
return base + '/' + this.sessionId + '/' + command + '/' + this.outSerial;
};
window.app.console.debug('proxy: new socket ' + this.id + ' ' + this.uri);
// queue fetch of session id.
this.getSessionId();
};
if (global.socketProxy)
{
// re-write relative URLs in CSS - somewhat grim.
window.addEventListener('load', function() {
var replaceUrls = function(rules, replaceBase) {
if (!rules)
return;
for (var r = 0; r < rules.length; ++r) {
// check subset of rules like @media or @import
if (rules[r] && rules[r].type != 1) {
replaceUrls(rules[r].cssRules || rules[r].rules, replaceBase);
continue;
}
if (!rules[r] || !rules[r].style)
continue;
var img = rules[r].style.backgroundImage;
if (img === '' || img === undefined)
continue;
if (img.startsWith('url("images/'))
{
rules[r].style.backgroundImage =
img.replace('url("images/', replaceBase + '/images/');
}
if (img.startsWith('url("remote/'))
{
rules[r].style.backgroundImage =
img.replace('url("remote/', replaceBase + '/remote/');
}
}
};
var sheets = document.styleSheets;
for (var i = 0; i < sheets.length; ++i) {
var relBases;
try {
relBases = sheets[i].href.split('/');
} catch (err) {
window.app.console.log('Missing href from CSS number ' + i);
continue;
}
relBases.pop(); // bin last - css name.
var replaceBase = 'url("' + relBases.join('/');
var rules;
try {
rules = sheets[i].cssRules || sheets[i].rules;
} catch (err) {
window.app.console.log('Missing CSS from ' + sheets[i].href);
continue;
}
replaceUrls(rules, replaceBase);
}
}, false);
}
// indirect socket to wrap the asyncness around fetching the routetoken from indirection url endpoint
global.IndirectSocket = function(uri) {
var that = this;
this.uri = uri;
this.binaryType = '';
this.unloading = false;
this.readyState = 0; // connecting
this.innerSocket = undefined;
this.onclose = function() {};
this.onerror = function () {};
this.onmessage = function () {};
this.onopen = function () {};
this.close = function() {
this.innerSocket.close();
};
this.send = function(msg) {
this.innerSocket.send(msg);
};
this.setUnloading = function() {
this.unloading = true;
};
this.sendPostMsg = function(errorCode) {
var errorMsg;
if (errorCode === 0) {
errorMsg = _('Cluster is scaling, retrying...');
} else if (errorCode === 1) {
errorMsg = _('Document is migrating to new server, retrying...');
} else {
errorMsg = _('Failed to get RouteToken from controller');
}
var msg = {
'MessageId': 'Action_Load_Resp',
'SendTime': Date.now(),
'Values': {
success: false,
errorMsg: errorMsg,
errorType: 'clusterscaling'
}
};
window.parent.postMessage(JSON.stringify(msg), '*');
};
var http = new XMLHttpRequest();
http.open('GET', global.indirectionUrl + '?Uri=' + encodeURIComponent(that.uri), true);
http.responseType = 'json';
http.addEventListener('load', function() {
if (this.status === 200) {
var uriWithRouteToken = http.response.uri;
global.expectedServerId = http.response.serverId;
var params = (new URL(uriWithRouteToken)).searchParams;
global.routeToken = params.get('RouteToken');
window.app.console.log('updated routeToken: ' + global.routeToken);
that.innerSocket = new WebSocket(uriWithRouteToken);
that.innerSocket.binaryType = that.binaryType;
that.innerSocket.onerror = function() {
that.readyState = that.innerSocket.readyState;
that.onerror();
};
that.innerSocket.onclose = function() {
that.readyState = 3;
that.onclose();
that.innerSocket.onerror = function () {};
that.innerSocket.onclose = function () {};
that.innerSocket.onmessage = function () {};
};
that.innerSocket.onopen = function() {
that.readyState = 1;
that.onopen();
};
that.innerSocket.onmessage = function(e) {
that.readyState = that.innerSocket.readyState;
that.onmessage(e);
};
} else if (this.status === 202) {
that.sendPostMsg(http.response.errorCode);
var timeoutFn = function (indirectionUrl, uri) {
console.warn('Requesting again for routeToken');
this.open('GET', indirectionUrl + '?Uri=' + encodeURIComponent(uri), true);
this.send();
}.bind(this);
setTimeout(timeoutFn, 3000, global.indirectionUrl, that.uri);
} else {
window.app.console.error('Indirection url: error on incoming response ' + this.status);
that.sendPostMsg(-1);
}
});
http.send();
};
global.createWebSocket = function(uri) {
if ('processCoolUrl' in window) {
uri = window.processCoolUrl({ url: uri, type: 'ws' });
}
if (global.socketProxy) {
window.socketProxy = true;
return new global.ProxySocket(uri);
} else if (global.indirectionUrl != '' && !global.migrating) {
window.indirectSocket = true;
return new global.IndirectSocket(uri);
} else {
return new WebSocket(uri);
}
};
global._ = function (string) {
// In the mobile app case we can't use the stuff from l10n-for-node, as that assumes HTTP.
if (window.ThisIsAMobileApp) {
// We use another approach just for iOS for now.
if (window.LOCALIZATIONS && Object.prototype.hasOwnProperty.call(window.LOCALIZATIONS, string)) {
// window.postMobileDebug('_(' + string + '): YES: ' + window.LOCALIZATIONS[string]);
var result = window.LOCALIZATIONS[string];
if (window.LANG === 'de-CH') {
result = result.replace(/ß/g, 'ss');
}
return result;
} else {
// window.postMobileDebug('_(' + string + '): NO');
return string;
}
} else {
return string.toLocaleString();
}
};
// Some global variables are defined in cool.html, among them:
// global.host: the host URL, with ws(s):// protocol
// global.serviceRoot: an optional root path on the server, typically blank.
// Setup global.webserver: the host URL, with http(s):// protocol (used to fetch files).
if (global.webserver === undefined) {
var protocol = window.location.protocol === 'file:' ? 'https:' : window.location.protocol;
global.webserver = global.host.replace(/^(ws|wss):/i, protocol);
global.webserver = global.webserver.replace(/\/*$/, ''); // Remove trailing slash.
}
var docParams, wopiParams;
var filePath = global.getParameterByName('file_path');
global.wopiSrc = global.getParameterByName('WOPISrc');
if (global.wopiSrc != '') {
global.docURL = decodeURIComponent(global.wopiSrc);
if (global.accessToken !== '') {
wopiParams = { 'access_token': global.accessToken, 'access_token_ttl': global.accessTokenTTL };
}
else if (global.accessHeader !== '') {
wopiParams = { 'access_header': global.accessHeader };
}
if (wopiParams) {
docParams = Object.keys(wopiParams).map(function(key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(wopiParams[key]);
}).join('&');
}
} else if (window.ThisIsTheEmscriptenApp) {
// This is of course just a horrible temporary hack
global.docURL = 'file:///sample.docx';
} else {
global.docURL = filePath;
}
// Form a valid WS URL to the host with the given path.
global.makeWsUrl = function (path) {
window.app.console.assert(global.host.startsWith('ws'), 'host is not ws: ' + global.host);
return global.host + global.serviceRoot + path;
};
// Form a URI from the docUrl and wopiSrc and encodes.
// The docUrlParams, suffix, and wopiSrc are optionally hexified.
global.routeToken = '';
global.makeDocAndWopiSrcUrl = function (root, docUrlParams, suffix, wopiSrcParam) {
var wopiSrc = '';
if (global.wopiSrc != '') {
wopiSrc = '?WOPISrc=' + global.wopiSrc;
if (global.routeToken != '')
wopiSrc += '&RouteToken=' + global.routeToken;
wopiSrc += '&compat=';
if (wopiSrcParam && wopiSrcParam.length > 0)
wopiSrc += '&' + wopiSrcParam;
}
else if (wopiSrcParam && wopiSrcParam.length > 0) {
wopiSrc = '?' + wopiSrcParam;
}
suffix = suffix || '/ws';
var encodedDocUrl = encodeURIComponent(docUrlParams) + suffix + wopiSrc;
if (global.hexifyUrl)
encodedDocUrl = global.hexEncode(encodedDocUrl);
return root + encodedDocUrl + '/ws';
};
// Form a valid WS URL to the host with the given path and
// encode the document URL and params.
global.makeWsUrlWopiSrc = function (path, docUrlParams, suffix, wopiSrcParam) {
var websocketURI = global.makeWsUrl(path);
return global.makeDocAndWopiSrcUrl(websocketURI, docUrlParams, suffix, wopiSrcParam);
};
// Form a valid HTTP URL to the host with the given path.
global.makeHttpUrl = function (path) {
window.app.console.assert(global.webserver.startsWith('http'), 'webserver is not http: ' + global.webserver);
return global.webserver + global.serviceRoot + path;
};
// Form a valid HTTP URL to the host with the given path and
// encode the document URL and params.
global.makeHttpUrlWopiSrc = function (path, docUrlParams, suffix, wopiSrcParam) {
var httpURI = window.makeHttpUrl(path);
return global.makeDocAndWopiSrcUrl(httpURI, docUrlParams, suffix, wopiSrcParam);
};
// Encode a string to hex.
global.hexEncode = function (string) {
var bytes = new TextEncoder().encode(string);
var hex = '0x';
for (var i = 0; i < bytes.length; ++i) {
hex += bytes[i].toString(16);
}
return hex;
};
// Decode hexified string back to plain text.
global.hexDecode = function (hex) {
if (hex.startsWith('0x'))
hex = hex.substr(2);
var bytes = new Uint8Array(hex.length / 2);
for (var i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return new TextDecoder().decode(bytes);
};
if (window.ThisIsAMobileApp) {
global.socket = new global.FakeWebSocket();
window.TheFakeWebSocket = global.socket;
} else {
// The URL may already contain a query (e.g., 'http://server.tld/foo/wopi/files/bar?desktop=baz') - then just append more params
var docParamsPart = docParams ? (global.docURL.includes('?') ? '&' : '?') + docParams : '';
var websocketURI = global.makeWsUrlWopiSrc('/cool/', global.docURL + docParamsPart);
try {
global.socket = global.createWebSocket(websocketURI);
} catch (err) {
window.app.console.log(err);
}
}
var lang = encodeURIComponent(global.getParameterByName('lang'));
if (lang)
window.langParam = lang;
else
window.langParam = 'en-US';
window.langParamLocale = new Intl.Locale(window.langParam);
global.queueMsg = [];
if (window.ThisIsTheEmscriptenApp)
// Temporary hack
window.LANG = 'en-US';
else if (window.ThisIsAMobileApp)
window.LANG = lang;
if (global.socket && global.socket.readyState !== 3) {
global.socket.onopen = function () {
if (global.socket.readyState === 1) {
var ProtocolVersionNumber = '0.1';
var timestamp = encodeURIComponent(global.getParameterByName('timestamp'));
var msg = 'load url=' + encodeURIComponent(global.docURL);
var now0 = Date.now();
var now1 = performance.now();
var now2 = Date.now();
global.socket.send('coolclient ' + ProtocolVersionNumber + ' ' + ((now0 + now2) / 2) + ' ' + now1);
var isCalcTest =
global.docURL.includes('data/desktop/calc/') ||
global.docURL.includes('data/mobile/calc/') ||
global.docURL.includes('data/multiuser/calc/');
if (L.Browser.cypressTest && isCalcTest)
window.enableAccessibility = false;
var accessibilityState = window.localStorage.getItem('accessibilityState') === 'true';
accessibilityState = accessibilityState || (L.Browser.cypressTest && !isCalcTest);
msg += ' accessibilityState=' + accessibilityState;
if (window.ThisIsAMobileApp) {
msg += ' lang=' + window.LANG;
} else {
if (timestamp) {
msg += ' timestamp=' + timestamp;
}
if (lang) {
msg += ' lang=' + lang;
}
// renderingOptions?
}
if (window.deviceFormFactor) {
msg += ' deviceFormFactor=' + window.deviceFormFactor;
}
if (window.isLocalStorageAllowed) {
var spellOnline = window.localStorage.getItem('SpellOnline');
if (spellOnline) {
msg += ' spellOnline=' + spellOnline;
}
}
msg += ' timezone=' + Intl.DateTimeFormat().resolvedOptions().timeZone;
global.socket.send(msg);
}
};
global.socket.onerror = function (event) {
window.app.console.log(event);
};
global.socket.onclose = function (event) {
window.app.console.log(event);
};
global.socket.onmessage = function (event) {
if (typeof global.socket._onMessage === 'function') {
global.socket._emptyQueue();
global.socket._onMessage(event);
} else {
global.queueMsg.push(event.data);
}
};
global.socket.binaryType = 'arraybuffer';
if (window.ThisIsAMobileApp && !window.ThisIsTheEmscriptenApp) {
// This corresponds to the initial GET request when creating a WebSocket
// connection and tells the app's code that it is OK to start invoking
// TheFakeWebSocket's onmessage handler. The app code that handles this
// special message knows the document to be edited anyway, and can send it
// on as necessary to the Online code.
window.postMobileMessage('HULLO');
// A FakeWebSocket is immediately open.
this.socket.onopen();
}
}
}(window));