collabora-online/browser/src/core/Socket.js

1810 lines
59 KiB
JavaScript

/* -*- js-indent-level: 8; fill-column: 100 -*- */
/*
* L.Socket contains methods for the communication with the server
*/
/* global app _ vex $ errorMessages Uint8Array brandProductName brandProductFAQURL */
app.definitions.Socket = L.Class.extend({
ProtocolVersionNumber: '0.1',
ReconnectCount: 0,
WasShownLimitDialog: false,
WSDServer: {},
/// Whether Trace Event recording is enabled or not. ("Enabled" here means whether it can be
/// turned on (and off again), not whether it is on.)
enableTraceEventLogging: false,
// Will be set from lokitversion message
TunnelledDialogImageCacheSize: 0,
getParameterValue: function (s) {
var i = s.indexOf('=');
if (i === -1)
return undefined;
return s.substring(i+1);
},
initialize: function (map) {
window.app.console.debug('socket.initialize:');
this._map = map;
this._msgQueue = [];
this._delayedMessages = [];
this._handlingDelayedMessages = false;
},
getWebSocketBaseURI: function(map) {
return window.makeWsUrlWopiSrc('/cool/', map.options.doc + '?' + $.param(map.options.docParams));
},
connect: function(socket) {
var map = this._map;
if (map.options.permission) {
map.options.docParams['permission'] = map.options.permission;
}
if (this.socket) {
this.close();
}
if (socket && (socket.readyState === 1 || socket.readyState === 0)) {
this.socket = socket;
} else if (window.ThisIsAMobileApp) {
// We have already opened the FakeWebSocket over in global.js
} else {
try {
this.socket = window.createWebSocket(this.getWebSocketBaseURI(map));
} catch (e) {
this._map.fire('error', {msg: _('Oops, there is a problem connecting to %productName: ').replace('%productName', (typeof brandProductName !== 'undefined' ? brandProductName : 'Collabora Online Development Edition')) + e, cmd: 'socket', kind: 'failed', id: 3});
return;
}
}
this.socket.onerror = L.bind(this._onSocketError, this);
this.socket.onclose = L.bind(this._onSocketClose, this);
this.socket.onopen = L.bind(this._onSocketOpen, this);
this.socket.onmessage = L.bind(this._slurpMessage, this);
this.socket.binaryType = 'arraybuffer';
if (map.options.docParams.access_token && parseInt(map.options.docParams.access_token_ttl)) {
var tokenExpiryWarning = 900 * 1000; // Warn when 15 minutes remain
clearTimeout(this._accessTokenExpireTimeout);
this._accessTokenExpireTimeout = setTimeout(L.bind(this._sessionExpiredWarning, this),
parseInt(map.options.docParams.access_token_ttl) - Date.now() - tokenExpiryWarning);
}
// process messages for early socket connection
this._emptyQueue();
},
_emptyQueue: function () {
if (window.queueMsg && window.queueMsg.length > 0) {
for (var it = 0; it < window.queueMsg.length; it++) {
this._slurpMessage({data: window.queueMsg[it], textMsg: window.queueMsg[it]});
}
window.queueMsg = [];
}
},
_sessionExpiredWarning: function() {
clearTimeout(this._accessTokenExpireTimeout);
var expirymsg = errorMessages.sessionexpiry;
if (parseInt(this._map.options.docParams.access_token_ttl) - Date.now() <= 0) {
expirymsg = errorMessages.sessionexpired;
}
var dateTime = new Date(parseInt(this._map.options.docParams.access_token_ttl));
var dateOptions = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' };
var timerepr = dateTime.toLocaleDateString(String.locale, dateOptions);
this._map.fire('warn', {msg: expirymsg.replace('%time', timerepr)});
// If user still doesn't refresh the session, warn again periodically
this._accessTokenExpireTimeout = setTimeout(L.bind(this._sessionExpiredWarning, this),
120 * 1000);
},
setUnloading: function() {
if (this.socket.setUnloading)
this.socket.setUnloading();
},
close: function () {
this.socket.onerror = function () {};
this.socket.onclose = function () {};
this.socket.onmessage = function () {};
this.socket.close();
// Reset wopi's app loaded so that reconnecting again informs outerframe about initialization
this._map['wopi'].resetAppLoaded();
this._map.fire('docloaded', {status: false});
clearTimeout(this._accessTokenExpireTimeout);
},
connected: function() {
return this.socket && this.socket.readyState === 1;
},
sendMessage: function (msg) {
if (this._map._fatal) {
// Avoid communicating when we're in fatal state
return;
}
if (!this._map._active) {
// Avoid communicating when we're inactive.
if (typeof msg !== 'string')
return;
if (!msg.startsWith('useractive') && !msg.startsWith('userinactive'))
return;
}
if (this._map.uiManager && this._map.uiManager.isUIBlocked())
return;
var socketState = this.socket.readyState;
if (socketState === 2 || socketState === 3) {
this._map.loadDocument();
}
if (socketState === 1) {
this._doSend(msg);
}
else {
// push message while trying to connect socket again.
this._msgQueue.push(msg);
}
},
_doSend: function(msg) {
// Only attempt to log text frames, not binary ones.
if (typeof msg === 'string')
this._logSocket('OUTGOING', msg);
this.socket.send(msg);
},
_getParameterByName: function(url, name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'), results = regex.exec(url);
return results === null ? '' : results[1].replace(/\+/g, ' ');
},
_onSocketOpen: function () {
window.app.console.debug('_onSocketOpen:');
this._map._serverRecycling = false;
this._map._documentIdle = false;
// Always send the protocol version number.
// TODO: Move the version number somewhere sensible.
// Also send information about our performance timer epoch
var now0 = Date.now();
var now1 = performance.now();
var now2 = Date.now();
this._doSend('coolclient ' + this.ProtocolVersionNumber + ' ' + ((now0 + now2) / 2) + ' ' + now1);
var msg = 'load url=' + encodeURIComponent(this._map.options.doc);
if (this._map._docLayer) {
this._reconnecting = true;
// we are reconnecting after a lost connection
msg += ' part=' + this._map.getCurrentPartNumber();
}
if (this._map.options.timestamp) {
msg += ' timestamp=' + this._map.options.timestamp;
}
if (this._map._docPassword) {
msg += ' password=' + this._map._docPassword;
}
if (String.locale) {
msg += ' lang=' + String.locale;
}
if (window.deviceFormFactor) {
msg += ' deviceFormFactor=' + window.deviceFormFactor;
}
if (this._map.options.renderingOptions) {
var options = {
'rendering': this._map.options.renderingOptions
};
msg += ' options=' + JSON.stringify(options);
}
if (window.isLocalStorageAllowed) {
var spellOnline = window.localStorage.getItem('SpellOnline');
if (spellOnline) {
msg += ' spellOnline=' + spellOnline;
}
}
this._doSend(msg);
for (var i = 0; i < this._msgQueue.length; i++) {
this._doSend(this._msgQueue[i]);
}
this._msgQueue = [];
this._map._activate();
},
_utf8ToString: function (data) {
var strBytes = '';
for (var it = 0; it < data.length; it++) {
strBytes += String.fromCharCode(data[it]);
}
return strBytes;
},
// Returns true if, and only if, we are ready to start loading
// the tiles and rendering the document.
_isReady: function() {
if (window.bundlejsLoaded == false || window.fullyLoadedAndReady == false) {
return false;
}
if (typeof this._map == 'undefined' ||
isNaN(this._map.options.tileWidthTwips) ||
isNaN(this._map.options.tileHeightTwips)) {
return false;
}
var center = this._map.getCenter();
if (isNaN(center.lat) || isNaN(center.lng) || isNaN(this._map.getZoom())) {
return false;
}
return true;
},
_logSocket: function(type, msg) {
var fullDebug = this._map._docLayer && this._map._docLayer._debug;
if (fullDebug)
this._map._docLayer._debugSetPostMessage(type,msg);
if (!window.protocolDebug && !fullDebug)
return;
if (!fullDebug && msg.length > 256) // for reasonable performance.
msg = msg.substring(0,256) + '<truncated ' + (msg.length - 256) + 'chars>';
var status = '';
if (!window.fullyLoadedAndReady)
status += '[!fullyLoadedAndReady]';
if (!window.bundlejsLoaded)
status += '[!bundlejsLoaded]';
var color = type === 'OUTGOING' ? 'color:red' : 'color:blue';
window.app.console.log(+new Date() + ' %c' + type + status + '%c: ' + msg.concat(' ').replace(' ', '%c '),
'background:#ddf;color:black', color, 'color:black');
},
_queueSlurpEventEmission: function() {
var that = this;
if (!that._slurpTimer)
{
that._slurpTimer = setTimeout(function () {
that._slurpTimer = undefined;
that._emitSlurpedEvents();
}, 1 /* ms */);
}
},
_emitSlurpedEvents: function() {
var queueLength = this._slurpQueue.length;
var completeEventWholeFunction = this.createCompleteTraceEvent('emitSlurped-' + String(queueLength),
{'_slurpQueue.length' : String(queueLength)});
if (this._map && this._map._docLayer) {
this._map._docLayer.pauseDrawing();
// Queue an instant timeout early to try to measure the
// re-rendering delay before we get back to the main-loop.
if (this.traceEventRecordingToggle)
{
var that = this;
if (!that._renderEventTimer)
that._renderEventTimer = setTimeout(function() {
var now = performance.now();
var delta = now - that._renderEventTimerStart;
if (delta >= 2 /* ms */) // significant
{
that.sendMessage('TRACEEVENT name=browser-render' +
' ph=X ts=' + Math.round(that._renderEventTimerStart * 1000) +
' dur=' + Math.round((now - that._renderEventTimerStart) * 1000));
that._renderEventTimerStart = undefined;
}
that._renderEventTimer = undefined;
}, 0);
}
}
// window.app.console.log('Slurp events ' + that._slurpQueue.length);
var complete = true;
try {
for (var i = 0; i < queueLength; ++i) {
var evt = this._slurpQueue[i];
if (evt.isComplete()) {
var textMsg;
if (typeof (evt.data) === 'string') {
textMsg = evt.data.replace(/\s+/g, '.');
}
else if (typeof (evt.data) === 'object') {
textMsg = evt.textMsg.replace(/\s+/g, '.');
}
var completeEventOneMessage = this.createCompleteTraceEventFromEvent(textMsg);
try {
// it is - are you ?
this._onMessage(evt);
}
catch (e)
{
// unpleasant - but stops this one problem
// event stopping an unknown number of others.
window.app.console.log('Exception ' + e + ' emitting event ' + evt.data);
}
finally {
if (completeEventOneMessage)
completeEventOneMessage.finish();
}
} else {
// Stop emitting, re-start when we async images load.
this._slurpQueue = this._slurpQueue.slice(i, queueLength);
complete = false;
break;
}
}
}
finally {
if (completeEventWholeFunction)
completeEventWholeFunction.finish();
}
if (complete) // Finished all elements in the queue.
this._slurpQueue = [];
if (this._map) {
if (this._map._docLayer) {
// Resume with redraw if dirty due to previous _onMessage() calls.
this._map._docLayer.resumeDrawing(true);
}
// Let other layers / overlays catch up.
this._map.fire('messagesdone');
this._renderEventTimerStart = performance.now();
}
},
// The problem: if we process one websocket message at a time, the
// browser -loves- to trigger a re-render as we hit the main-loop,
// this takes ~200ms on a large screen, and worse we get
// producer/consumer issues that can fill a multi-second long
// buffer of web-socket messages in the client that we can't
// process so - slurp and the emit at idle - its faster to delay!
_slurpMessage: function(e) {
if (!this._slurpQueue || !this._slurpQueue.length) {
this._queueSlurpEventEmission();
this._slurpQueue = [];
}
this._extractTextImg(e);
this._slurpQueue.push(e);
},
// make profiling easier
_extractCopyObject: function(e) {
var index;
e.imgBytes = new Uint8Array(e.data);
// search for the first newline which marks the end of the message
index = e.imgBytes.indexOf(10);
if (index < 0)
index = e.imgBytes.length;
e.textMsg = String.fromCharCode.apply(null, e.imgBytes.subarray(0, index));
e.imgIndex = index + 1;
},
// convert to string of bytes without blowing the stack if data is large.
_strFromUint8: function(data) {
var i, chunk = 4096;
var strBytes = '';
for (i = 0; i < data.length; i += chunk)
strBytes += String.fromCharCode.apply(null, data.slice(i, i + chunk));
strBytes += String.fromCharCode.apply(null, data.slice(i));
return strBytes;
},
_extractImage: function(e) {
var img;
if (window.ThisIsTheiOSApp) {
// In the iOS app, the native code sends us the PNG tile already as a data: URL after the newline
var newlineIndex = e.textMsg.indexOf('\n');
if (newlineIndex > 0) {
img = e.textMsg.substring(newlineIndex+1);
e.textMsg = e.textMsg.substring(0, newlineIndex);
}
}
else
{
var data = e.imgBytes.subarray(e.imgIndex);
window.app.console.assert(data.length == 0 || data[0] != 68 /* D */, 'Socket: got a delta image, not supported !');
img = 'data:image/png;base64,' + window.btoa(this._strFromUint8(data));
if (L.Browser.cypressTest && localStorage.getItem('image_validation_test')) {
if (!window.imgDatas)
window.imgDatas = [];
window.imgDatas.push(img);
}
}
return img;
},
_extractTextImg: function (e) {
if (typeof (e.data) === 'string')
e.textMsg = e.data;
else if (typeof (e.data) === 'object')
this._extractCopyObject(e);
e.isComplete = function () {
if (this.image)
return !!this.imageIsComplete;
return true;
};
if (!e.textMsg.startsWith('tile:') &&
!e.textMsg.startsWith('renderfont:') &&
!e.textMsg.startsWith('windowpaint:'))
return;
if (e.textMsg.indexOf(' nopng') !== -1)
return;
// pass deltas through quickly.
if (e.imgBytes && e.imgBytes[e.imgIndex] === 68 /* D */)
{
window.app.console.log('Passed through delta object');
e.image = e.imgBytes.subarray(e.imgIndex);
e.imageIsComplete = true;
return;
}
var that = this;
var img = this._extractImage(e);
e.image = new Image();
e.image.onload = function() {
e.imageIsComplete = true;
that._queueSlurpEventEmission();
if (e.image.completeTraceEvent)
e.image.completeTraceEvent.finish();
};
e.image.onerror = function(err) {
window.app.console.log('Failed to load image ' + img + ' fun ' + err);
e.imageIsComplete = true;
that._queueSlurpEventEmission();
if (e.image.completeTraceEvent)
e.image.completeTraceEvent.abort();
};
e.image.completeTraceEvent = this.createAsyncTraceEvent('loadTile');
e.image.src = img;
},
_onMessage: function (e) {
var imgBytes, textMsg;
textMsg = e.textMsg;
imgBytes = e.imgBytes;
this._logSocket('INCOMING', textMsg);
var command = this.parseServerCmd(textMsg);
if (textMsg.startsWith('coolserver ')) {
// This must be the first message, unless we reconnect.
var oldId = null;
var oldVersion = null;
var sameFile = true;
// Check if we are reconnecting.
if (this.WSDServer && this.WSDServer.Id) {
// Yes we are reconnecting.
// If server is restarted, we have to refresh the page.
// If our connection was lost and is ready again, we will not need to refresh the page.
oldId = this.WSDServer.Id;
oldVersion = this.WSDServer.Version;
window.app.console.assert(this._map.options.wopiSrc === window.wopiSrc,
'wopiSrc mismatch!: ' + this._map.options.wopiSrc + ' != ' + window.wopiSrc);
// If another file is opened, we will not refresh the page.
if (this._map.options.previousWopiSrc && this._map.options.wopiSrc) {
if (this._map.options.previousWopiSrc !== this._map.options.wopiSrc)
sameFile = false;
}
}
this.WSDServer = JSON.parse(textMsg.substring(textMsg.indexOf('{')));
if (oldId && oldVersion && sameFile) {
if (this.WSDServer.Id !== oldId || this.WSDServer.Version !== oldVersion) {
var reloadMessage = _('Server is now reachable. We have to refresh the page now.');
if (window.mode.isMobile())
reloadMessage = _('Server is now reachable...');
var reloadFunc = function() { window.location.reload(); };
if (!this._map['wopi'].DisableInactiveMessages)
this._map.uiManager.showSnackbar(reloadMessage, _('RELOAD'), reloadFunc);
else
this._map.fire('postMessage', {msgId: 'Reloading', args: {Reason: 'Reconnected'}});
setTimeout(reloadFunc, 5000);
}
}
$('#coolwsd-version-label').text(_('COOLWSD version:'));
var h = this.WSDServer.Hash;
if (parseInt(h,16).toString(16) === h.toLowerCase().replace(/^0+/, '')) {
h = '<a href="javascript:void(window.open(\'https://github.com/CollaboraOnline/online/commits/' + h + '\'));">' + h + '</a>';
$('#coolwsd-version').html(this.WSDServer.Version + ' <span>git hash:&nbsp;' + h + this.WSDServer.Options + '</span>');
}
else {
$('#coolwsd-version').text(this.WSDServer.Version);
}
if (!window.ThisIsAMobileApp) {
var idUri = window.makeHttpUrl('/hosting/discovery');
$('#served-by-label').text(_('Served by:'));
$('#coolwsd-id').html('<a target="_blank" href="' + idUri + '">' + this.WSDServer.Id + '</a>');
}
// TODO: For now we expect perfect match in protocol versions
if (this.WSDServer.Protocol !== this.ProtocolVersionNumber) {
this._map.fire('error', {msg: _('Unsupported server version.')});
}
}
else if (textMsg.startsWith('lokitversion ')) {
$('#lokit-version-label').text(_('LOKit version:'));
var lokitVersionObj = JSON.parse(textMsg.substring(textMsg.indexOf('{')));
h = lokitVersionObj.BuildId.substring(0, 7);
if (parseInt(h,16).toString(16) === h.toLowerCase().replace(/^0+/, '')) {
h = '<a href="javascript:void(window.open(\'https://hub.libreoffice.org/git-core/' + h + '\'));">' + h + '</a>';
}
$('#lokit-version').html(lokitVersionObj.ProductName + ' ' +
lokitVersionObj.ProductVersion + lokitVersionObj.ProductExtension +
'<span> git hash:&nbsp;' + h + '<span>');
this.TunnelledDialogImageCacheSize = lokitVersionObj.tunnelled_dialog_image_cache_size;
}
else if (textMsg.startsWith('enabletraceeventlogging ')) {
this.enableTraceEventLogging = true;
}
else if (textMsg.startsWith('osinfo ')) {
var osInfo = textMsg.replace('osinfo ', '');
var osInfoElement = document.getElementById('os-info');
if (osInfoElement)
osInfoElement.innerText = osInfo;
}
else if (textMsg.startsWith('clipboardkey: ')) {
var key = textMsg.substring('clipboardkey: '.length);
if (this._map._clip)
this._map._clip.setKey(key);
}
else if (textMsg.startsWith('perm:')) {
var perm = textMsg.substring('perm:'.length).trim();
// Never make the permission more permissive than it originally was.
if (this._map.options.permission == 'edit')
{
this._map.options.permission = perm;
}
if (this._map._docLayer) {
this._map.setPermission(this._map.options.permission);
}
app.file.disableSidebar = perm !== 'edit';
app.file.readOnly = this._map.options.permission === 'readonly';
return;
}
else if (textMsg.startsWith('filemode:')) {
var json = JSON.parse(textMsg.substring('filemode:'.length).trim());
// Never make the permission more permissive than it originally was.
if (this._map.options.permission == 'edit' && json.readOnly)
{
this._map.options.permission = 'readonly';
}
if (this._map._docLayer) {
this._map.setPermission(this._map.options.permission);
}
app.file.readOnly = this._map.options.permission === 'readonly';
app.file.editComment = json.editComment; // Allowed even in readonly mode.
}
else if (textMsg.startsWith('lockfailed:')) {
this._map.onLockFailed(textMsg.substring('lockfailed:'.length).trim());
return;
}
else if (textMsg.startsWith('wopi: ')) {
// Handle WOPI related messages
var wopiInfo = JSON.parse(textMsg.substring(textMsg.indexOf('{')));
this._map.fire('wopiprops', wopiInfo);
return;
}
else if (textMsg.startsWith('loadstorage: ')) {
if (textMsg.substring(textMsg.indexOf(':') + 2) === 'failed') {
window.app.console.debug('Loading document from a storage failed');
this._map.fire('postMessage', {
msgId: 'App_LoadingStatus',
args: {
Status: 'Failed'
}
});
}
}
else if (textMsg.startsWith('lastmodtime: ')) {
var time = textMsg.substring(textMsg.indexOf(' ') + 1);
this._map.updateModificationIndicator(time);
return;
}
else if (textMsg.startsWith('commandresult: ')) {
var commandresult = JSON.parse(textMsg.substring(textMsg.indexOf('{')));
if (commandresult['command'] === 'savetostorage' || commandresult['command'] === 'save') {
if (commandresult['success']) {
// Close any open confirmation dialogs
vex.closeAll();
}
var postMessageObj = {
success: commandresult['success'],
result: commandresult['result'],
errorMsg: commandresult['errorMsg']
};
this._map.fire('postMessage', {msgId: 'Action_Save_Resp', args: postMessageObj});
} else if (commandresult['command'] === 'load') {
postMessageObj = {
success: commandresult['success'],
result: commandresult['result'],
errorMsg: commandresult['errorMsg']
};
this._map.fire('postMessage', {msgId: 'Action_Load_Resp', args: postMessageObj});
}
return;
}
else if (textMsg.startsWith('close: ')) {
textMsg = textMsg.substring('close: '.length);
msg = '';
var postMsgData = {};
var showMsgAndReload = false;
// This is due to document owner terminating the session
if (textMsg === 'ownertermination') {
msg = _('Session terminated by document owner');
postMsgData['Reason'] = 'OwnerTermination';
}
else if (textMsg === 'idle' || textMsg === 'oom') {
if (window.mode.isDesktop()) {
msg = _('Idle document - please click to reload and resume editing');
} else {
msg = _('Idle document - please tap to reload and resume editing');
}
this._map._documentIdle = true;
postMsgData['Reason'] = 'DocumentIdle';
if (textMsg === 'oom')
postMsgData['Reason'] = 'OOM';
}
else if (textMsg === 'shuttingdown') {
msg = _('Server is shutting down for maintenance (auto-saving)');
postMsgData['Reason'] = 'ShuttingDown';
}
else if (textMsg === 'docdisconnected') {
msg = _('Oops, there is a problem connecting the document');
postMsgData['Reason'] = 'DocumentDisconnected';
}
else if (textMsg === 'recycling') {
msg = _('Server is down, restarting automatically. Please wait.');
this._map._active = false;
this._map._serverRecycling = true;
// Prevent reconnecting the world at the same time.
var min = 5000;
var max = 10000;
var timeoutMs = Math.floor(Math.random() * (max - min) + min);
var socket = this;
map = this._map;
clearTimeout(vex.timer);
vex.timer = setInterval(function() {
if (socket.connected()) {
// We're connected: cancel timer and dialog.
clearTimeout(vex.timer);
vex.closeAll();
return;
}
try {
map.loadDocument(map);
} catch (error) {
window.app.console.warn('Cannot load document.');
}
}, timeoutMs);
}
else if (textMsg.startsWith('documentconflict')) {
msg = _('Document has changed in storage. Loading the new document. Your version is available as revision.');
showMsgAndReload = true;
}
else if (textMsg.startsWith('versionrestore:')) {
textMsg = textMsg.substring('versionrestore:'.length).trim();
if (textMsg === 'prerestore_ack') {
msg = _('Restoring older revision. Any unsaved changes will be available in version history');
this._map.fire('postMessage', {msgId: 'App_VersionRestore', args: {Status: 'Pre_Restore_Ack'}});
showMsgAndReload = true;
}
}
else if (textMsg.startsWith('reloadafterrename')) {
msg = _('Reloading the document after rename');
showMsgAndReload = true;
}
if (showMsgAndReload) {
if (this._map._docLayer) {
this._map._docLayer.removeAllViews();
}
// Detach all the handlers from current socket, otherwise _onSocketClose tries to reconnect again
// However, we want to reconnect manually here.
this.close();
// Reload the document
this._map._active = false;
map = this._map;
clearTimeout(vex.timer);
vex.timer = setInterval(function() {
try {
// Activate and cancel timer and dialogs.
map._activate();
} catch (error) {
window.app.console.warn('Cannot activate map');
}
}, 3000);
}
// Close any open dialogs first.
vex.closeAll();
var message = '';
if (!this._map['wopi'].DisableInactiveMessages) {
message = msg;
}
var dialogOptions = {
message: message,
contentClassName: 'cool-user-idle'
};
var restartConnectionFn;
if (textMsg === 'idle' || textMsg === 'oom') {
var map = this._map;
restartConnectionFn = function() {
if (map._documentIdle)
{
window.app.console.debug('idleness: reactivating');
map._documentIdle = false;
map._docLayer._setCursorVisible();
return map._activate();
}
return false;
};
dialogOptions.afterClose = restartConnectionFn;
var dialogOpened = vex.dialog.open(dialogOptions);
this._map._textInput.hideCursor();
dialogOpened.contentEl.onclick = restartConnectionFn;
$('.vex-overlay').addClass('cool-user-idle-overlay');
if (message === '')
$('.cool-user-idle').css('display', 'none');
}
if (postMsgData['Reason']) {
// Tell WOPI host about it which should handle this situation
this._map.fire('postMessage', {msgId: 'Session_Closed', args: postMsgData});
}
if (textMsg === 'ownertermination') {
this._map.remove();
}
return;
}
else if (textMsg.startsWith('error:')
&& (command.errorCmd === 'storage' || command.errorCmd === 'saveas') || command.errorCmd === 'downloadas') {
if (command.errorCmd === 'saveas') {
this._map.fire('postMessage', {
msgId: 'Action_Save_Resp',
args: {
success: false,
result: command.errorKind
}
});
}
this._map.hideBusy();
var storageError;
if (command.errorKind === 'savediskfull') {
storageError = errorMessages.storage.savediskfull;
}
else if (command.errorKind === 'savefailed') {
storageError = errorMessages.storage.savefailed;
}
else if (command.errorKind === 'renamefailed') {
storageError = errorMessages.storage.renamefailed;
}
else if (command.errorKind === 'saveunauthorized') {
storageError = errorMessages.storage.saveunauthorized;
}
else if (command.errorKind === 'saveasfailed') {
storageError = errorMessages.storage.saveasfailed;
}
else if (command.errorKind === 'loadfailed') {
storageError = errorMessages.storage.loadfailed;
// Since this is a document load failure, wsd will disconnect the socket anyway,
// better we do it first so that another error message doesn't override this one
// upon socket close.
this.close();
}
else if (command.errorKind === 'documentconflict')
{
var that = this;
storageError = errorMessages.storage.documentconflict;
vex.closeAll();
var dialogButtons = [
$.extend({}, vex.dialog.buttons.YES, {
text: _('Discard'),
className: 'vex-dialog-button-secondary',
click: function() {
this.value = 'discard';
this.close();
}}),
$.extend({}, vex.dialog.buttons.YES, {
text: _('Overwrite'),
className: 'vex-dialog-button-secondary',
click: function() {
this.value = 'overwrite';
this.close();
}}),
$.extend({}, vex.dialog.buttons.YES, {
text: '',
className: 'vex-dialog-button-spacer'
})
];
if (!that._map['wopi'].UserCanNotWriteRelative) {
dialogButtons.push(
$.extend({}, vex.dialog.buttons.YES, {
text: _('Save to new file'),
className: 'vex-dialog-button-primary',
click: function() {
this.value = 'saveas';
this.close();
}}),
$.extend({}, vex.dialog.buttons.YES, {
text: _('Cancel'),
className: 'vex-dialog-button-secondary vex-dialog-button-cancel',
click: function() {
this.value = 'cancel';
this.close();
}})
);
} else {
dialogButtons.push(
$.extend({}, vex.dialog.buttons.YES, {
text: _('Cancel'),
className: 'vex-dialog-button-primary vex-dialog-button-cancel',
click: function() {
this.value = 'cancel';
this.close();
}})
);
}
vex.dialog.open({
unsafeMessage: '<h1 class="vex-dialog-title">' + vex._escapeHtml(_('Document has been changed')) + '</h1><p class="vex-dialog-message">' + vex._escapeHtml(_('Document has been changed in storage. What would you like to do with your unsaved changes?')) + '</p>',
escapeButtonCloses: false,
overlayClosesOnClick: false,
contentClassName: 'vex-content vex-3btns',
buttons: dialogButtons,
showCloseButton: true,
callback: function(value) {
if (value === 'discard') {
// They want to refresh the page and load document again for all
that.sendMessage('closedocument');
} else if (value === 'overwrite') {
// They want to overwrite
that.sendMessage('savetostorage force=1');
} else if (value === 'saveas') {
var filename = that._map['wopi'].BaseFileName;
if (filename) {
filename = L.LOUtil.generateNewFileName(filename, '_new');
that._map.saveAs(filename);
}
}
},
afterOpen: function() {
this.contentEl.style.width = '600px';
}
});
return;
}
// Skip empty errors (and allow for suppressing errors by making them blank).
if (storageError && storageError != '') {
// Parse the storage url as link
var tmpLink = document.createElement('a');
tmpLink.href = this._map.options.doc;
// Insert the storage server address to be more friendly
storageError = storageError.replace('%storageserver', tmpLink.host);
this._map.fire('warn', {msg: storageError});
return;
}
}
else if (textMsg.startsWith('error:') && command.errorCmd === 'internal') {
this._map.hideBusy();
this._map._fatal = true;
if (command.errorKind === 'diskfull') {
this._map.fire('error', {msg: errorMessages.diskfull});
}
else if (command.errorKind === 'unauthorized') {
this._map.fire('error', {msg: errorMessages.unauthorized});
}
if (this._map._docLayer) {
this._map._docLayer.removeAllViews();
this._map._docLayer._resetClientVisArea();
}
this.close();
return;
}
else if (textMsg.startsWith('error:') && command.errorCmd === 'load') {
this._map.hideBusy();
this.close();
var errorKind = command.errorKind;
var passwordNeeded = false;
if (errorKind.startsWith('passwordrequired')) {
passwordNeeded = true;
var msg = '';
var passwordType = errorKind.split(':')[1];
if (passwordType === 'to-view') {
msg += _('Document requires password to view.');
}
else if (passwordType === 'to-modify') {
msg += _('Document requires password to modify.');
msg += ' ';
msg += _('Hit Cancel to open in view-only mode.');
}
} else if (errorKind.startsWith('wrongpassword')) {
passwordNeeded = true;
msg = _('Wrong password provided. Please try again.');
} else if (errorKind.startsWith('faileddocloading')) {
this._map._fatal = true;
this._map.fire('error', {msg: errorMessages.faileddocloading});
} else if (errorKind.startsWith('docloadtimeout')) {
this._map._fatal = true;
this._map.fire('error', {msg: errorMessages.docloadtimeout});
} else if (errorKind.startsWith('docunloading')) {
// The document is unloading. Have to wait a bit.
this._map._active = false;
clearTimeout(vex.timer);
if (this.ReconnectCount++ >= 10) {
this._map.fire('error', {msg: errorMessages.docunloadinggiveup});
return; // Give up.
}
map = this._map;
vex.timer = setInterval(function() {
try {
// Activate and cancel timer and dialogs.
map._activate();
} catch (error) {
window.app.console.warn('Cannot activate map');
}
// .5, 2, 4.5, 8, 12.5, 18, 24.5, 32, 40.5 seconds
}, 500 * this.ReconnectCount * this.ReconnectCount); // Quadratic back-off.
if (this.ReconnectCount > 1) {
this._map.showBusy(errorMessages.docunloadingretry, false);
}
}
if (passwordNeeded) {
// Ask the user for password
vex.dialog.open({
contentClassName: 'vex-has-inputs',
message: msg,
input: '<input name="password" type="password" required />',
buttons: [
$.extend({}, vex.dialog.buttons.YES, { text: _('OK') }),
$.extend({}, vex.dialog.buttons.NO, { text: _('Cancel') })
],
callback: L.bind(function(data) {
if (data) {
this._map._docPassword = data.password;
if (window.ThisIsAMobileApp) {
window.postMobileMessage('loadwithpassword password=' + data.password);
}
this._map.loadDocument();
} else if (passwordType === 'to-modify') {
this._map._docPassword = '';
this._map.loadDocument();
} else {
this._map.fire('postMessage', {msgId: 'UI_Cancel_Password'});
this._map.hideBusy();
}
}, this)
});
return;
}
}
else if (textMsg.startsWith('error:') && !this._map._docLayer) {
textMsg = textMsg.substring(6);
if (command.errorKind === 'hardlimitreached') {
textMsg = errorMessages.limitreachedprod;
textMsg = textMsg.replace(/%0/g, command.params[0]);
textMsg = textMsg.replace(/%1/g, command.params[1]);
}
else if (command.errorKind === 'serviceunavailable') {
textMsg = errorMessages.serviceunavailable;
}
this._map._fatal = true;
this._map._active = false; // Practically disconnected.
this._map.fire('error', {msg: textMsg});
}
else if (textMsg.startsWith('info:') && command.errorCmd === 'socket') {
if (command.errorKind === 'limitreached' && !this.WasShownLimitDialog) {
this.WasShownLimitDialog = true;
textMsg = errorMessages.limitreached;
textMsg = textMsg.replace(/{docs}/g, command.params[0]);
textMsg = textMsg.replace(/{connections}/g, command.params[1]);
textMsg = textMsg.replace(/{productname}/g, (typeof brandProductName !== 'undefined' ?
brandProductName : 'Collabora Online Development Edition'));
var brandFAQURL = (typeof brandProductFAQURL !== 'undefined') ?
brandProductFAQURL : 'https://collaboraonline.github.io/post/faq/';
this._map.fire('infobar',
{
msg: textMsg,
action: brandFAQURL,
actionLabel: errorMessages.infoandsupport
});
}
}
else if (textMsg.startsWith('pong ') && this._map._docLayer && this._map._docLayer._debug) {
var times = this._map._docLayer._debugTimePING;
var timeText = this._map._docLayer._debugSetTimes(times, +new Date() - this._map._docLayer._debugPINGQueue.shift());
this._map._docLayer._debugData['ping'].setPrefix('Server ping time: ' + timeText +
'. Rendered tiles: ' + command.rendercount +
', last: ' + (command.rendercount - this._map._docLayer._debugRenderCount));
this._map._docLayer._debugRenderCount = command.rendercount;
}
else if (textMsg.startsWith('saveas:') || textMsg.startsWith('renamefile:')) {
this._renameOrSaveAsCallback(textMsg, command);
}
else if (textMsg.startsWith('warn:')) {
var len = 'warn: '.length;
textMsg = textMsg.substring(len);
if (textMsg.startsWith('saveas:')) {
var userName = command.username ? command.username : _('Someone');
vex.dialog.confirm({
message: userName + _(' saved this document as ') + command.filename + _('. Do you want to join?'),
callback: L.bind(function (val) {
if (val) this._renameOrSaveAsCallback(textMsg, command);
}, this)
});
}
}
else if (textMsg.startsWith('statusindicator:')) {
//FIXME: We should get statusindicator when saving too, no?
this._map.showBusy(window.ThisIsAMobileApp? _('Loading...'): _('Connecting...'), true);
if (textMsg.startsWith('statusindicator: ready')) {
// We're connected: cancel timer and dialog.
this.ReconnectCount = 0;
clearTimeout(vex.timer);
vex.closeAll();
}
}
else if (window.ThisIsAMobileApp && textMsg.startsWith('mobile:')) {
// allow passing some events easily from the mobile app
var mobileEvent = textMsg.substring('mobile: '.length);
this._map.fire(mobileEvent);
}
else if (textMsg.startsWith('blockui:')) {
textMsg = textMsg.substring('blockui:'.length).trim();
msg = null;
if (textMsg === 'rename') {
msg = _('The document is being renamed and will reload shortly');
}
this._map.fire('blockUI', {message: msg});
return;
}
else if (textMsg.startsWith('unblockui:')) {
this._map.fire('unblockUI');
return;
}
else if (textMsg.startsWith('featurelock: ')) {
// Handle feature locking related messages
var lockInfo = JSON.parse(textMsg.substring(textMsg.indexOf('{')));
this._map._setLockProps(lockInfo);
return;
}
else if (textMsg.startsWith('restrictedCommands: ')) {
// Handle restriction related messages
var restrictionInfo = JSON.parse(textMsg.substring(textMsg.indexOf('{')));
this._map._setRestrictions(restrictionInfo);
return;
}
else if (textMsg.startsWith('blockedcommand: ')) {
var blockedInfo = app.socket.parseServerCmd(textMsg.substring(16));
if (blockedInfo.errorKind === 'restricted')
window.app.console.log('Restricted command "' + blockedInfo.errorCmd + '" was blocked');
else if (blockedInfo.errorKind === 'locked')
this._map.openUnlockPopup(blockedInfo.errorCmd);
return;
}
else if (!textMsg.startsWith('tile:') && !textMsg.startsWith('renderfont:') && !textMsg.startsWith('windowpaint:')) {
if (imgBytes !== undefined) {
try {
// if it's not a tile, parse the whole message
textMsg = String.fromCharCode.apply(null, imgBytes);
} catch (error) {
// big data string
textMsg = this._utf8ToString(imgBytes);
}
}
// Decode UTF-8 in case it is binary frame
if (typeof e.data === 'object') {
// FIXME: Not sure what this code is supposed to do. Doesn't
// decodeURIComponent() exactly reverse what window.escape() (which
// is a deprecated equivalent of encodeURIComponent()) does? In what
// case is this code even hit? If somebody figures out what is going
// on here, please replace this comment with an explanation.
textMsg = decodeURIComponent(window.escape(textMsg));
}
}
if (textMsg.startsWith('status:')) {
this._onStatusMsg(textMsg, command);
}
// These can arrive very early during the startup, and never again.
if (textMsg.startsWith('statusindicator')) {
if (textMsg.startsWith('statusindicatorstart:')) {
var tokens = textMsg.split(' ');
this._map.fire('statusindicator', {
statusType : 'start',
text: tokens.length > 1 ? tokens[1] : ''
});
return;
}
else if (textMsg.startsWith('statusindicatorsetvalue:')) {
var value = textMsg.match(/\d+/g)[0];
this._map.fire('statusindicator', {statusType : 'setvalue', value : value});
return;
}
else if (textMsg.startsWith('statusindicatorfinish:')) {
this._map.fire('statusindicator', {statusType : 'finish'});
this._map._fireInitComplete('statusindicatorfinish');
return;
}
}
else if (textMsg.startsWith('jsdialog:')) {
this._onJSDialog(textMsg, e.callback);
}
else if (textMsg.startsWith('hyperlinkclicked:')) {
this._onHyperlinkClickedMsg(textMsg);
}
var msgDelayed = false;
if (!this._isReady() || !this._map._docLayer || this._delayedMessages.length || this._handlingDelayedMessages) {
msgDelayed = this._tryToDelayMessage(textMsg);
}
if (this._map._docLayer && !msgDelayed) {
this._map._docLayer._onMessage(textMsg, e.image);
}
},
_renameOrSaveAsCallback: function(textMsg, command) {
this._map.hideBusy();
if (command !== undefined && command.url !== undefined && command.url !== '') {
var url = command.url;
// setup for loading the new document, and trigger the load
var docUrl = url.split('?')[0];
this._map.options.doc = docUrl;
this._map.options.previousWopiSrc = this._map.options.wopiSrc; // After save-as op, we may connect to another server, then code will think that server has restarted. In this case, we don't want to reload the page (detect the file name is different).
this._map.options.wopiSrc = encodeURIComponent(docUrl);
window.wopiSrc = this._map.options.wopiSrc;
if (textMsg.startsWith('renamefile:')) {
this._map.fire('postMessage', {
msgId: 'File_Rename',
args: {
NewName: command.filename
}
});
} else if (textMsg.startsWith('saveas:')) {
var accessToken = this._getParameterByName(url, 'access_token');
var accessTokenTtl = this._getParameterByName(url, 'access_token_ttl');
if (accessToken !== undefined) {
if (accessTokenTtl === undefined) {
accessTokenTtl = 0;
}
this._map.options.docParams = { 'access_token': accessToken, 'access_token_ttl': accessTokenTtl };
}
else {
this._map.options.docParams = {};
}
// if this is save-as, we need to load the document with edit permission
// otherwise the user has to close the doc then re-open it again
// in order to be able to edit.
this._map.options.permission = 'edit';
this.close();
this._map.loadDocument();
this._map.sendInitUNOCommands();
this._map.fire('postMessage', {
msgId: 'Action_Save_Resp',
args: {
success: true,
fileName: decodeURIComponent(command.filename)
}
});
}
}
// var name = command.name; - ignored, we get the new name via the wopi's BaseFileName
},
_tryToDelayMessage: function(textMsg) {
var delayed = false;
if (textMsg.startsWith('window:') ||
textMsg.startsWith('celladdress:') ||
textMsg.startsWith('cellviewcursor:') ||
textMsg.startsWith('statechanged:') ||
textMsg.startsWith('invalidatecursor:') ||
textMsg.startsWith('viewinfo:')) {
//window.app.console.log('_tryToDelayMessage: textMsg: ' + textMsg);
var message = {msg: textMsg};
this._delayedMessages.push(message);
delayed = true;
}
if (delayed && !this._delayedMsgHandlerTimeoutId) {
this._handleDelayedMessages();
}
return delayed;
},
_handleDelayedMessages: function() {
if (!this._isReady() || !this._map._docLayer || this._handlingDelayedMessages) {
var that = this;
// Retry in a bit.
this._delayedMsgHandlerTimeoutId = setTimeout(function() {
that._handleDelayedMessages();
}, 100);
return;
}
var messages = [];
for (var i = 0; i < this._delayedMessages.length; ++i) {
var message = this._delayedMessages[i];
if (message)
messages.push(message.msg);
}
this._delayedMessages = [];
this._delayedMsgHandlerTimeoutId = null;
this._handlingDelayedMessages = true;
if (this._map._docLayer) {
for (var k = 0; k < messages.length; ++k) {
try {
this._map._docLayer._onMessage(messages[k]);
} catch (e) {
// unpleasant - but stops this one problem
// event stopping an unknown number of others.
window.app.console.log('Exception ' + e + ' emitting event ' + messages[k]);
}
}
}
this._handlingDelayedMessages = false;
},
_onStatusMsg: function(textMsg, command) {
var that = this;
if (!this._isReady()) {
// Retry in a bit.
setTimeout(function() {
that._onStatusMsg(textMsg, command);
}, 100);
return;
}
if (!this._map._docLayer) {
// first status message, we need to create the document layer
var tileWidthTwips = this._map.options.tileWidthTwips;
var tileHeightTwips = this._map.options.tileHeightTwips;
if (this._map.options.zoom !== this._map.options.defaultZoom) {
var scale = this._map.options.crs.scale(this._map.options.defaultZoom - this._map.options.zoom);
tileWidthTwips = Math.round(tileWidthTwips * scale);
tileHeightTwips = Math.round(tileHeightTwips * scale);
}
var docLayer = null;
if (command.type === 'text') {
docLayer = new L.WriterTileLayer('', {
permission: this._map.options.permission,
tileWidthTwips: tileWidthTwips / app.dpiScale,
tileHeightTwips: tileHeightTwips / app.dpiScale,
docType: command.type
});
}
else if (command.type === 'spreadsheet') {
docLayer = new L.CalcTileLayer('', {
permission: this._map.options.permission,
tileWidthTwips: tileWidthTwips / app.dpiScale,
tileHeightTwips: tileHeightTwips / app.dpiScale,
docType: command.type
});
}
else if (command.type === 'presentation' || command.type === 'drawing') {
docLayer = new L.ImpressTileLayer('', {
permission: this._map.options.permission,
tileWidthTwips: tileWidthTwips / app.dpiScale,
tileHeightTwips: tileHeightTwips / app.dpiScale,
docType: command.type
});
}
this._map._docLayer = docLayer;
this._map.addLayer(docLayer);
this._map.fire('doclayerinit');
}
else if (this._reconnecting) {
// we are reconnecting ...
this._reconnecting = false;
this._map._docLayer._resetClientVisArea();
this._map._docLayer._requestNewTiles();
this._map.fire('statusindicator', {statusType: 'reconnected'});
this._map._isNotebookbarLoadedOnCore = false;
var uiMode = this._map.uiManager.getCurrentMode();
this._map.fire('changeuimode', {mode: uiMode, force: true});
this._map.setPermission(this._map.options.permission);
}
this._map.fire('docloaded', {status: true});
if (this._map._docLayer) {
this._map._docLayer._onMessage(textMsg);
}
},
// show labels instead of editable fields in message boxes
_preProcessMessageDialog: function(msgData) {
for (var i in msgData.children) {
var child = msgData.children[i];
if (child.type === 'multilineedit')
child.type = 'fixedtext';
else if (child.children)
this._preProcessMessageDialog(child);
}
},
_onJSDialog: function(textMsg, callback) {
var msgData = JSON.parse(textMsg.substring('jsdialog:'.length + 1));
if (msgData.children && !L.Util.isArray(msgData.children)) {
window.app.console.warn('_onJSDialogMsg: The children\'s data should be created of array type');
return;
}
if (msgData.action) {
var that = this;
var fireJSDialogEvent = function () {
switch (msgData.action) {
case 'update':
that._map.fire('jsdialogupdate', {data: msgData});
return true;
case 'action':
that._map.fire('jsdialogaction', {data: msgData});
return true;
}
return false;
};
var isNotebookbarInitialized = (this._map.uiManager && this._map.uiManager.notebookbar);
if (msgData.jsontype === 'notebookbar' && !isNotebookbarInitialized) {
setTimeout(fireJSDialogEvent, 1000);
return;
} else if (fireJSDialogEvent() === true) {
return;
}
}
if (msgData.type === 'messagebox')
this._preProcessMessageDialog(msgData);
// re/create
if (window.mode.isMobile()) {
if (msgData.type == 'borderwindow')
return;
if (msgData.jsontype === 'formulabar') {
this._map.fire('formulabar', {data: msgData});
return;
}
if (msgData.enabled || msgData.type === 'modalpopup' || msgData.type === 'snackbar') {
this._map.fire('mobilewizard', {data: msgData, callback: callback});
} else {
this._map.fire('closemobilewizard');
}
} else if (msgData.jsontype === 'autofilter') {
this._map.fire('autofilterdropdown', msgData);
} else if (msgData.jsontype === 'dialog') {
this._map.fire('jsdialog', {data: msgData, callback: callback});
} else if (msgData.jsontype === 'sidebar') {
this._map.fire('sidebar', {data: msgData});
} else if (msgData.jsontype === 'formulabar') {
this._map.fire('formulabar', {data: msgData});
} else if (msgData.jsontype === 'notebookbar') {
if (msgData.children) {
for (var i = 0; i < msgData.children.length; i++) {
if (msgData.children[i].type === 'control') {
msgData.children[i].id = msgData.id;
this._map.fire('notebookbar', msgData.children[i]);
return;
}
}
}
}
},
_onHyperlinkClickedMsg: function (textMsg) {
var link = null;
var coords = null;
var hyperlinkMsgStart = 'hyperlinkclicked: ';
var coordinatesMsgStart = ' coordinates: ';
if (textMsg.indexOf(coordinatesMsgStart) !== -1) {
var coordpos = textMsg.indexOf(coordinatesMsgStart);
link = textMsg.substring(hyperlinkMsgStart.length, coordpos);
coords = textMsg.substring(coordpos+coordinatesMsgStart.length);
} else
link = textMsg.substring(hyperlinkMsgStart.length);
this._map.fire('hyperlinkclicked', {url: link, coordinates: coords});
},
_onSocketError: function () {
window.app.console.debug('_onSocketError:');
this._map.hideBusy();
// Let onclose (_onSocketClose) report errors.
},
_onSocketClose: function () {
window.app.console.debug('_onSocketClose:');
if (this.ReconnectCount > 0)
return;
var isActive = this._map._active;
this._map.hideBusy();
this._map._active = false;
if (this._map._docLayer) {
this._map._docLayer.removeAllViews();
this._map._docLayer._resetClientVisArea();
this._map._docLayer._graphicSelection = new L.LatLngBounds(new L.LatLng(0, 0), new L.LatLng(0, 0));
this._map._docLayer._onUpdateGraphicSelection();
}
if (isActive && this._reconnecting) {
// Don't show this before first transparently trying to reconnect.
this._map.fire('error', {msg: _('Well, this is embarrassing, we cannot connect to your document. Please try again.'), cmd: 'socket', kind: 'closed', id: 4});
}
// Reset wopi's app loaded so that reconnecting again informs outerframe about initialization
this._map['wopi'].resetAppLoaded();
this._map.fire('docloaded', {status: false});
// We need to make sure that the message slurping processes the
// events first, because there could have been a message like
// "close: idle" from the server.
// Without the timeout, we'd immediately reconnect (because the
// "close: idle" was not processed yet).
var that = this;
setTimeout(function () {
if (!that._reconnecting) {
that._reconnecting = true;
if (!that._map._documentIdle)
that._map.showBusy(_('Reconnecting...'), false);
that._map._activate();
}
}, 1 /* ms */);
if (!this._map['wopi'].DisableInactiveMessages)
this._map.uiManager.showSnackbar(_('The server has been disconnected.'));
},
parseServerCmd: function (msg) {
var tokens = msg.split(/[ \n]+/);
var command = {};
for (var i = 0; i < tokens.length; i++) {
if (tokens[i].substring(0, 9) === 'tileposx=') {
command.x = parseInt(tokens[i].substring(9));
}
else if (tokens[i].substring(0, 9) === 'tileposy=') {
command.y = parseInt(tokens[i].substring(9));
}
else if (tokens[i].substring(0, 2) === 'x=') {
command.x = parseInt(tokens[i].substring(2));
}
else if (tokens[i].substring(0, 2) === 'y=') {
command.y = parseInt(tokens[i].substring(2));
}
else if (tokens[i].substring(0, 10) === 'tilewidth=') {
command.tileWidth = parseInt(tokens[i].substring(10));
}
else if (tokens[i].substring(0, 11) === 'tileheight=') {
command.tileHeight = parseInt(tokens[i].substring(11));
}
else if (tokens[i].substring(0, 6) === 'width=') {
command.width = parseInt(tokens[i].substring(6));
}
else if (tokens[i].substring(0, 7) === 'height=') {
command.height = parseInt(tokens[i].substring(7));
}
else if (tokens[i].substring(0, 5) === 'part=') {
command.part = parseInt(tokens[i].substring(5));
}
else if (tokens[i].substring(0, 6) === 'parts=') {
command.parts = parseInt(tokens[i].substring(6));
}
else if (tokens[i].substring(0, 8) === 'current=') {
command.selectedPart = parseInt(tokens[i].substring(8));
}
else if (tokens[i].substring(0, 3) === 'id=') {
// remove newline characters
command.id = tokens[i].substring(3).replace(/(\r\n|\n|\r)/gm, '');
}
else if (tokens[i].substring(0, 5) === 'type=') {
// remove newline characters
command.type = tokens[i].substring(5).replace(/(\r\n|\n|\r)/gm, '');
}
else if (tokens[i].substring(0, 4) === 'cmd=') {
command.errorCmd = tokens[i].substring(4);
}
else if (tokens[i].substring(0, 5) === 'code=') {
command.errorCode = tokens[i].substring(5);
}
else if (tokens[i].substring(0, 5) === 'kind=') {
command.errorKind = tokens[i].substring(5);
}
else if (tokens[i].substring(0, 5) === 'jail=') {
command.jail = tokens[i].substring(5);
}
else if (tokens[i].substring(0, 4) === 'dir=') {
command.dir = tokens[i].substring(4);
}
else if (tokens[i].substring(0, 11) === 'downloadid=') {
command.downloadid = tokens[i].substring(11);
}
else if (tokens[i].substring(0, 5) === 'name=') {
command.name = tokens[i].substring(5);
}
else if (tokens[i].substring(0, 9) === 'filename=') {
command.filename = tokens[i].substring(9);
}
else if (tokens[i].substring(0, 5) === 'port=') {
command.port = tokens[i].substring(5);
}
else if (tokens[i].substring(0, 5) === 'font=') {
command.font = tokens[i].substring(5);
}
else if (tokens[i].substring(0, 5) === 'char=') {
command.char = tokens[i].substring(5);
}
else if (tokens[i].substring(0, 4) === 'url=') {
command.url = tokens[i].substring(4);
}
else if (tokens[i].substring(0, 7) === 'viewid=') {
command.viewid = tokens[i].substring(7);
}
else if (tokens[i].substring(0, 8) === 'nviewid=') {
command.nviewid = tokens[i].substring(8);
}
else if (tokens[i].substring(0, 7) === 'params=') {
command.params = tokens[i].substring(7).split(',');
}
else if (tokens[i].substring(0, 9) === 'renderid=') {
command.renderid = tokens[i].substring(9);
}
else if (tokens[i].substring(0, 12) === 'rendercount=') {
command.rendercount = parseInt(tokens[i].substring(12));
}
else if (tokens[i].startsWith('wid=')) {
command.wireId = this.getParameterValue(tokens[i]);
}
else if (tokens[i].substring(0, 6) === 'title=') {
command.title = tokens[i].substring(6);
}
else if (tokens[i].substring(0, 12) === 'dialogwidth=') {
command.dialogwidth = tokens[i].substring(12);
}
else if (tokens[i].substring(0, 13) === 'dialogheight=') {
command.dialogheight = tokens[i].substring(13);
}
else if (tokens[i].substring(0, 10) === 'rectangle=') {
command.rectangle = tokens[i].substring(10);
}
else if (tokens[i].substring(0, 12) === 'hiddenparts=') {
var hiddenparts = tokens[i].substring(12).split(',');
command.hiddenparts = [];
hiddenparts.forEach(function (item) {
command.hiddenparts.push(parseInt(item));
});
}
else if (tokens[i].startsWith('selectedparts=')) {
var selectedParts = tokens[i].substring(14).split(',');
command.selectedParts = [];
selectedParts.forEach(function (item) {
command.selectedParts.push(parseInt(item));
});
}
else if (tokens[i].startsWith('rtlparts=')) {
var rtlParts = tokens[i].substring(9).split(',');
command.rtlParts = [];
rtlParts.forEach(function (item) {
command.rtlParts.push(parseInt(item));
});
}
else if (tokens[i].startsWith('hash=')) {
command.hash = tokens[i].substring('hash='.length);
}
else if (tokens[i] === 'nopng') {
command.nopng = true;
}
else if (tokens[i].substring(0, 9) === 'username=') {
command.username = tokens[i].substring(9);
}
else if (tokens[i].startsWith('pagerectangles=')) {
command.pageRectangleList = tokens[i].substring(15).split(';');
command.pageRectangleList = command.pageRectangleList.map(function(element) {
element = element.split(',');
return [parseInt(element[0]), parseInt(element[1]), parseInt(element[2]), parseInt(element[3])];
});
}
}
if (command.tileWidth && command.tileHeight && this._map._docLayer) {
var defaultZoom = this._map.options.zoom;
var scale = command.tileWidth / this._map._docLayer.options.tileWidthTwips;
// scale = 1.2 ^ (defaultZoom - zoom)
// zoom = defaultZoom -log(scale) / log(1.2)
command.zoom = Math.round(defaultZoom - Math.log(scale) / Math.log(1.2));
}
return command;
},
setTraceEventLogging: function (enabled) {
this.traceEventRecordingToggle = enabled;
this.sendMessage('traceeventrecording ' + (this.traceEventRecordingToggle ? 'start' : 'stop'));
// Just as a test, uncomment this to toggle SAL_WARN and
// SAL_INFO selection between two states: 1) the default
// as directed by the SAL_LOG environment variable, and
// 2) all warnings on plus SAL_INFO for sc.
//
// (Note that coolwsd sets the SAL_LOG environment variable
// to "-WARN-INFO", i.e. the default is that nothing is
// logged from core.)
// app.socket.sendMessage('sallogoverride ' + (app.socket.traceEventRecordingToggle ? '+WARN+INFO.sc' : 'default'));
},
traceEventRecordingToggle: false,
_stringifyArgs: function (args) {
return (args == null ? '' : (' args=' + JSON.stringify(args)));
},
// The args parameter, if present, should be an object where both keys and values are strings that don't contain any spaces.
emitInstantTraceEvent: function (name, args) {
if (this.traceEventRecordingToggle)
this.sendMessage('TRACEEVENT name=' + name + ' ph=i ts=' + Math.round(performance.now() * 1000)
+ this._stringifyArgs(args));
},
asyncTraceEventCounter: 0,
// simulate a threads per live async event to help the chrome renderer
asyncTracePseudoThread: 1,
createAsyncTraceEvent: function (name, args) {
if (!this.traceEventRecordingToggle)
return null;
var result = {};
result.id = this.asyncTraceEventCounter++;
result.tid = this.asyncTracePseudoThread++;
result.active = true;
result.args = args;
if (this.traceEventRecordingToggle)
this.sendMessage('TRACEEVENT name=' + name +
' ph=S ts=' + Math.round(performance.now() * 1000) +
' id=' + result.id + ' tid=' + result.tid +
this._stringifyArgs(args));
var that = this;
result.finish = function () {
that.asyncTracePseudoThread--;
if (this.active) {
that.sendMessage('TRACEEVENT name=' + name +
' ph=F ts=' + Math.round(performance.now() * 1000) +
' id=' + this.id + ' tid=' + this.tid +
that._stringifyArgs(this.args));
this.active = false;
}
};
result.abort = function () {
that.asyncTracePseudoThread--;
this.active = false;
};
return result;
},
createCompleteTraceEvent: function (name, args) {
if (!this.traceEventRecordingToggle)
return null;
var result = {};
result.active = true;
result.begin = performance.now();
result.args = args;
var that = this;
result.finish = function () {
if (this.active) {
var now = performance.now();
that.sendMessage('TRACEEVENT name=' + name +
' ph=X ts=' + Math.round(this.begin * 1000) +
' dur=' + Math.round((now - this.begin) * 1000)
+ that._stringifyArgs(args));
this.active = false;
}
};
result.abort = function () {
this.active = false;
};
return result;
},
// something we can grok quickly in the trace viewer
createCompleteTraceEventFromEvent: function(textMsg) {
if (!this.traceEventRecordingToggle)
return null;
var pretty;
if (!textMsg)
pretty = 'blob';
else {
var idx = textMsg.indexOf(':');
if (idx > 0)
pretty = textMsg.substring(0,idx);
else if (textMsg.length < 25)
pretty = textMsg;
else
pretty = textMsg.substring(0, 25);
}
return this.createCompleteTraceEvent(pretty, { message: textMsg });
},
threadLocalLoggingLevelToggle: false
});