2018-07-24 07:53:26 -05:00
/* -*- js-indent-level: 8 -*- */
2020-03-19 10:54:28 -05:00
/* global Uint8Array */
2021-03-29 10:54:40 -05:00
2021-05-11 06:12:30 -05:00
window . app = { // Shouldn't have any functions defined.
2021-05-11 06:54:19 -05:00
definitions : { } , // Class instances are created using definitions under this variable.
2021-07-16 11:14:38 -05:00
dpiScale : window . devicePixelRatio ,
roundedDpiScale : Math . round ( window . devicePixelRatio ) ,
2021-07-24 08:23:05 -05:00
twipsToPixels : 0 , // Twips to pixels multiplier.
pixelsToTwips : 0 , // Pixels to twips multiplier.
2021-05-11 06:12:30 -05:00
file : {
editComment : false ,
readOnly : true ,
2022-02-09 06:47:21 -06:00
disableSidebar : false ,
2021-05-11 06:12:30 -05:00
size : {
pixels : [ 0 , 0 ] , // This can change according to the zoom level and document's size.
2021-05-18 15:03:07 -05:00
twips : [ 0 , 0 ]
2021-05-11 06:12:30 -05:00
} ,
2021-07-31 05:18:56 -05:00
viewedRectangle : [ 0 , 0 , 0 , 0 ] , // Visible part of the file (x, y, w, h).
2021-05-11 06:12:30 -05:00
fileBasedView : false , // (draw-impress only) Default is false. For read-only documents, user can view all parts at once. In that case, this variable is set to "true".
2021-05-29 08:10:29 -05:00
calc : {
cellCursor : {
address : [ 0 , 0 ] ,
rectangle : {
pixels : [ 0 , 0 , 0 , 0 ] ,
twips : [ 0 , 0 , 0 , 0 ]
} ,
visible : false ,
}
2021-07-23 09:13:26 -05:00
} ,
writer : {
pageRectangleList : [ ] // Array of arrays: [x, y, w, h] (as usual) // twips only. Pixels will be calculated on the fly. Corresponding pixels may change too ofte
} ,
2021-05-11 06:12:30 -05:00
} ,
view : {
2021-08-03 05:46:02 -05:00
commentHasFocus : false ,
2021-05-11 06:12:30 -05:00
size : {
pixels : [ 0 , 0 ] // This can be larger than the document's size.
}
} ,
2021-05-11 06:54:19 -05:00
tile : {
size : {
pixels : [ 0 , 0 ] ,
twips : [ 0 , 0 ]
}
} ,
2021-05-11 06:12:30 -05:00
socket : null ,
2021-12-20 03:27:33 -06:00
console : { } ,
2021-05-11 06:12:30 -05:00
} ;
2021-03-29 10:54:40 -05:00
2018-04-12 14:57:00 -05:00
( function ( global ) {
2021-06-14 10:14:31 -05:00
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 ) ;
2021-06-14 10:47:34 -05:00
} else if ( global . socket && global . L && global . app . definitions . Socket &&
( global . socket instanceof global . app . definitions . Socket ) && global . socket . connected ( ) ) {
2021-06-14 10:14:31 -05:00
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 ) ;
}
} ;
2022-02-11 12:56:37 -06:00
// enable later toggling
global . setLogging = function ( doLogging )
{
2022-02-14 04:07:42 -06:00
var loggingMethods = [ 'error' , 'warn' , 'info' , 'debug' , 'trace' , 'log' , 'assert' , 'time' , 'timeEnd' , 'group' , 'groupEnd' ] ;
2022-02-11 12:56:37 -06:00
if ( ! doLogging ) {
var noop = function ( ) { } ;
2021-12-20 03:27:33 -06:00
2022-02-11 12:56:37 -06:00
for ( var i = 0 ; i < loggingMethods . length ; i ++ ) {
window . app . console [ loggingMethods [ i ] ] = noop ;
2021-12-20 03:27:33 -06:00
}
2022-02-11 12:56:37 -06:00
} 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 ) ;
2021-12-20 03:27:33 -06:00
2022-02-11 12:56:37 -06:00
return window . console [ method ] . apply ( console , args ) ;
} ;
} ( loggingMethods [ i ] ) ) ;
}
2021-12-20 03:27:33 -06:00
2022-02-11 12:56:37 -06:00
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
2021-12-20 03:27:33 -06:00
} ;
2022-02-11 12:56:37 -06:00
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 ;
2021-06-14 10:14:31 -05:00
} ;
2022-02-11 12:56:37 -06:00
}
} ;
2021-06-14 10:14:31 -05:00
2022-02-11 12:56:37 -06:00
global . setLogging ( global . coolLogging == 'true' ) ;
2021-06-14 10:14:31 -05:00
2020-07-27 09:04:07 -05:00
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 , ' ' ) ;
} ;
2020-02-14 14:51:47 -06:00
var ua = navigator . userAgent . toLowerCase ( ) ,
uv = navigator . vendor . toLowerCase ( ) ,
doc = document . documentElement ,
ie = 'ActiveXObject' in window ,
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 && ! 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 ,
cypressTest = ua . indexOf ( 'cypress' ) !== - 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 ;
2021-11-18 06:08:14 -06:00
var chromebook = window . ThisIsTheAndroidApp && window . COOLMessageHandler . isChromeOS ( ) ;
2020-08-07 06:49:32 -05:00
2020-02-14 14:51:47 -06:00
var touch = ! window . L _NO _TOUCH && ( pointer || 'ontouchstart' in window ||
2020-08-07 06:49:32 -05:00
( window . DocumentTouch && document instanceof window . DocumentTouch ) ) && ! chromebook ;
2020-02-14 14:51:47 -06:00
var isInternetExplorer = ( navigator . userAgent . toLowerCase ( ) . indexOf ( 'msie' ) != - 1 ||
navigator . userAgent . toLowerCase ( ) . indexOf ( 'trident' ) != - 1 ) ;
global . L = { } ;
2020-07-27 09:04:07 -05:00
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' ) ,
} ;
2020-02-14 14:51:47 -06:00
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 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 touch: Boolean
// `true` for all browsers supporting [touch events](https://developer.mozilla.org/docs/Web/API/Touch_events).
touch : ! ! touch ,
// @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
} ;
2020-03-26 03:42:32 -05:00
global . mode = {
2020-07-15 14:29:23 -05:00
isChromebook : function ( ) {
2020-08-07 06:49:32 -05:00
return chromebook ;
2020-07-15 14:29:23 -05:00
} ,
2020-03-26 03:42:32 -05:00
// Here "mobile" means "mobile phone" (at least for now). Has to match small screen size
// requirement.
isMobile : function ( ) {
2020-07-15 14:29:23 -05:00
if ( global . mode . isChromebook ( ) )
return false ;
2021-01-08 07:14:59 -06:00
if ( global . L . Browser . mobile && L . Browser . cypressTest ) {
2020-03-26 03:42:32 -05:00
return true ;
}
2020-04-08 07:15:22 -05:00
return L . Browser . mobile && ( screen . width < 768 || screen . height < 768 ) ;
2020-03-26 03:42:32 -05:00
} ,
// Mobile device with big screen size.
isTablet : function ( ) {
2020-07-15 14:29:23 -05:00
if ( global . mode . isChromebook ( ) )
return false ;
2020-03-26 03:42:32 -05:00
return L . Browser . mobile && ! window . mode . isMobile ( ) ;
} ,
isDesktop : function ( ) {
2020-07-15 14:29:23 -05:00
if ( global . mode . isChromebook ( ) )
return true ;
2020-03-26 03:42:32 -05:00
return ! L . Browser . mobile ;
2020-04-20 14:26:21 -05:00
} ,
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 ;
2020-03-26 03:42:32 -05:00
}
} ;
2020-04-20 14:26:21 -05:00
global . deviceFormFactor = window . mode . getDeviceFormFactor ( ) ;
2020-01-07 12:04:14 -06:00
document . addEventListener ( 'contextmenu' , function ( e ) {
if ( e . preventDefault ) {
e . preventDefault ( ) ;
} else {
e . returnValue = false ;
}
} , false ) ;
2019-03-06 05:32:59 -06:00
global . fakeWebSocketCounter = 0 ;
global . FakeWebSocket = function ( ) {
this . binaryType = 'arraybuffer' ;
this . bufferedAmount = 0 ;
this . extensions = '' ;
this . protocol = '' ;
this . readyState = 1 ;
this . id = window . fakeWebSocketCounter ++ ;
this . sendCounter = 0 ;
this . onclose = function ( ) {
} ;
this . onerror = function ( ) {
} ;
this . onmessage = function ( ) {
} ;
this . onopen = function ( ) {
} ;
2020-03-04 07:54:04 -06:00
this . close = function ( ) {
} ;
2020-01-16 08:44:23 -06:00
} ;
2019-03-06 05:32:59 -06:00
global . FakeWebSocket . prototype . send = function ( data ) {
this . sendCounter ++ ;
window . postMobileMessage ( data ) ;
2020-01-16 08:44:23 -06:00
} ;
2019-03-06 05:32:59 -06:00
2020-03-04 07:54:04 -06:00
global . proxySocketCounter = 0 ;
global . ProxySocket = function ( uri ) {
2020-03-20 11:38:14 -05:00
var that = this ;
2020-03-04 07:54:04 -06:00
this . uri = uri ;
this . binaryType = 'arraybuffer' ;
this . bufferedAmount = 0 ;
this . extensions = '' ;
2020-05-09 13:41:40 -05:00
this . unloading = false ;
2020-03-04 07:54:04 -06:00
this . protocol = '' ;
this . connected = true ;
this . readyState = 0 ; // connecting
2020-05-09 13:41:40 -05:00
this . sessionId = 'open' ;
2020-03-04 07:54:04 -06:00
this . id = window . proxySocketCounter ++ ;
this . sendCounter = 0 ;
2020-05-13 11:21:07 -05:00
this . msgInflight = 0 ;
2020-06-08 09:46:29 -05:00
this . openInflight = 0 ;
2020-04-18 12:40:59 -05:00
this . inSerial = 0 ;
this . outSerial = 0 ;
2020-05-21 20:22:10 -05:00
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.
2020-03-04 07:54:04 -06:00
this . onclose = function ( ) {
} ;
this . onerror = function ( ) {
} ;
this . onmessage = function ( ) {
} ;
2021-10-26 06:26:49 -05:00
2021-08-19 05:25:51 -05:00
this . decoder = new TextDecoder ( ) ;
this . doSlice = function ( bytes , start , end ) { return bytes . slice ( start , end ) ; } ;
2021-10-26 06:26:49 -05:00
2020-05-25 14:44:31 -05:00
this . decode = function ( bytes , start , end ) {
return this . decoder . decode ( this . doSlice ( bytes , start , end ) ) ;
} ;
2020-03-19 10:54:28 -05:00
this . parseIncomingArray = function ( arr ) {
2021-12-20 03:27:33 -06:00
//window.app.console.debug('proxy: parse incoming array of length ' + arr.length);
2020-03-19 10:54:28 -05:00
for ( var i = 0 ; i < arr . length ; ++ i )
{
var left = arr . length - i ;
if ( left < 4 )
{
2021-12-20 03:27:33 -06:00
//window.app.console.debug('no data left');
2020-03-19 10:54:28 -05:00
break ;
}
var type = String . fromCharCode ( arr [ i + 0 ] ) ;
if ( type != 'T' && type != 'B' )
{
2021-12-20 03:27:33 -06:00
window . app . console . debug ( 'wrong data type: ' + type ) ;
2020-03-19 10:54:28 -05:00
break ;
}
2020-04-18 12:40:59 -05:00
i ++ ;
// Serial
if ( arr [ i ] !== 48 && arr [ i + 1 ] !== 120 ) // '0x'
2020-03-19 10:54:28 -05:00
{
2021-12-20 03:27:33 -06:00
window . app . console . debug ( 'missing hex preamble' ) ;
2020-03-19 10:54:28 -05:00
break ;
}
2020-04-18 12:40:59 -05:00
i += 2 ;
2020-03-19 10:54:28 -05:00
var numStr = '' ;
var start = i ;
while ( arr [ i ] != 10 ) // '\n'
i ++ ;
2020-05-25 14:44:31 -05:00
numStr = this . decode ( arr , start , i ) ;
2020-04-18 12:40:59 -05:00
var serial = parseInt ( numStr , 16 ) ;
i ++ ; // skip \n
// Size:
if ( arr [ i ] !== 48 && arr [ i + 1 ] !== 120 ) // '0x'
{
2021-12-20 03:27:33 -06:00
window . app . console . debug ( 'missing hex preamble' ) ;
2020-04-18 12:40:59 -05:00
break ;
}
i += 2 ;
start = i ;
while ( arr [ i ] != 10 ) // '\n'
i ++ ;
2020-05-25 14:44:31 -05:00
numStr = this . decode ( arr , start , i ) ;
2020-03-19 10:54:28 -05:00
var size = parseInt ( numStr , 16 ) ;
i ++ ; // skip \n
var data ;
2020-05-25 14:44:31 -05:00
if ( type == 'T' )
data = this . decode ( arr , i , i + size ) ;
2020-03-19 10:54:28 -05:00
else
2020-05-25 14:44:31 -05:00
data = this . doSlice ( arr , i , i + size ) ;
2020-03-19 10:54:28 -05:00
2020-04-18 12:40:59 -05:00
if ( serial !== that . inSerial + 1 ) {
2021-12-20 03:27:33 -06:00
window . app . console . debug ( 'Error: serial mismatch ' + serial + ' vs. ' + ( that . inSerial + 1 ) ) ;
2020-04-18 12:40:59 -05:00
}
that . inSerial = serial ;
2020-03-19 10:54:28 -05:00
this . onmessage ( { data : data } ) ;
i += size ; // skip trailing '\n' in loop-increment
}
} ;
2020-03-20 11:38:14 -05:00
this . sendQueue = '' ;
2020-05-21 15:25:34 -05:00
this . _signalErrorClose = function ( ) {
2020-06-09 05:29:11 -05:00
clearInterval ( this . pollInterval ) ;
clearTimeout ( this . delaySession ) ;
this . pollInterval = undefined ;
this . delaySession = undefined ;
2020-05-21 15:25:34 -05:00
if ( that . readyState < 3 )
{
this . onerror ( ) ;
this . onclose ( ) ;
}
2020-06-09 04:44:30 -05:00
this . sessionId = 'open' ;
this . inSerial = 0 ;
this . outSerial = 0 ;
this . msgInflight = 0 ;
this . openInflight = 0 ;
2020-05-21 15:25:34 -05:00
this . readyState = 3 ; // CLOSED
} ;
2020-06-09 05:29:11 -05:00
// 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:
2020-05-21 20:22:10 -05:00
this . _setPollInterval = function ( intervalMs ) {
2020-06-09 05:29:11 -05:00
clearInterval ( this . pollInterval ) ;
if ( this . readyState === 1 )
this . pollInterval = setInterval ( this . doSend , intervalMs ) ;
2020-05-21 20:22:10 -05:00
} ,
2020-03-20 11:38:14 -05:00
this . doSend = function ( ) {
2020-05-09 13:41:40 -05:00
if ( that . sessionId === 'open' )
2020-05-13 11:21:07 -05:00
{
2020-05-21 20:22:10 -05:00
if ( that . readyState === 3 )
2021-12-20 03:27:33 -06:00
window . app . console . debug ( 'Error: sending on closed socket' ) ;
2020-05-13 11:21:07 -05:00
return ;
}
2020-05-21 20:22:10 -05:00
2020-05-13 11:21:07 -05:00
if ( that . msgInflight >= 4 ) // something went badly wrong.
2020-03-19 10:54:28 -05:00
{
2020-05-21 20:22:10 -05:00
// 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 ;
2021-12-20 03:27:33 -06:00
window . app . console . debug ( 'High latency connection - too much in-flight, throttling to ' + that . curPollMs + ' ms.' ) ;
2020-05-21 20:22:10 -05:00
that . _setPollInterval ( that . curPollMs ) ;
}
2020-06-09 05:29:11 -05:00
else if ( performance . now ( ) - that . lastDataTimestamp > 30 * 1000 )
{
2021-12-20 03:27:33 -06:00
window . app . console . debug ( 'Close connection after no response for 30secs' ) ;
2020-06-09 05:29:11 -05:00
that . _signalErrorClose ( ) ;
}
2020-05-21 20:22:10 -05:00
else
2021-12-20 03:27:33 -06:00
window . app . console . debug ( 'High latency connection - too much in-flight, pausing.' ) ;
2020-05-13 11:21:07 -05:00
return ;
2020-03-19 10:54:28 -05:00
}
2020-05-21 20:22:10 -05:00
// 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 ) ;
2021-12-20 03:27:33 -06:00
//window.app.console.debug('send msg - ' + that.msgInflight + ' on session ' +
2020-10-20 06:05:07 -05:00
// that.sessionId + ' queue: "' + that.sendQueue + '"');
2020-05-13 11:21:07 -05:00
var req = new XMLHttpRequest ( ) ;
req . open ( 'POST' , that . getEndPoint ( 'write' ) ) ;
req . responseType = 'arraybuffer' ;
req . addEventListener ( 'load' , function ( ) {
if ( this . status == 200 )
2020-05-21 20:22:10 -05:00
{
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 ;
}
}
2020-05-13 11:21:07 -05:00
else
2020-05-21 15:25:34 -05:00
{
2021-12-20 03:27:33 -06:00
window . app . console . debug ( 'proxy: error on incoming response ' + this . status ) ;
2020-05-21 15:25:34 -05:00
that . _signalErrorClose ( ) ;
}
2020-05-21 20:22:10 -05:00
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 ;
2021-12-20 03:27:33 -06:00
//window.app.console.debug('No data for ' + timeSinceLastDataMs + ' ms -- throttling to ' + that.curPollMs + ' ms.');
2020-05-21 20:22:10 -05:00
}
}
that . _setPollInterval ( that . curPollMs ) ;
2020-05-13 11:21:07 -05:00
} ) ;
req . addEventListener ( 'loadend' , function ( ) {
that . msgInflight -- ;
} ) ;
2020-03-20 11:38:14 -05:00
req . send ( that . sendQueue ) ;
that . sendQueue = '' ;
2020-05-13 11:21:07 -05:00
that . msgInflight ++ ;
2020-03-20 11:38:14 -05:00
} ;
this . getSessionId = function ( ) {
2020-06-08 09:46:29 -05:00
if ( this . openInflight > 0 )
{
2021-12-20 03:27:33 -06:00
window . app . console . debug ( 'Waiting for session open' ) ;
2020-06-08 09:46:29 -05:00
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 ;
2021-12-20 03:27:33 -06:00
window . app . console . debug ( 'Wait to re-try session creation for ' + delay + 'ms' ) ;
2020-06-08 09:46:29 -05:00
this . curPollMs = delay ; // ms
this . delaySession = setTimeout ( function ( ) {
that . delaySession = undefined ;
that . getSessionId ( ) ;
} , delay ) ;
return ;
}
}
global . lastCreatedProxySocket = performance . now ( ) ;
2020-03-20 11:38:14 -05:00
var req = new XMLHttpRequest ( ) ;
2020-05-09 13:41:40 -05:00
req . open ( 'POST' , that . getEndPoint ( 'open' ) ) ;
2020-03-20 11:38:14 -05:00
req . responseType = 'text' ;
req . addEventListener ( 'load' , function ( ) {
2021-12-20 03:27:33 -06:00
window . app . console . debug ( 'got session: ' + this . responseText ) ;
2020-06-08 09:46:29 -05:00
if ( this . status !== 200 || ! this . responseText ||
this . responseText . indexOf ( '\n' ) >= 0 ) // multi-line error
2020-04-28 09:18:24 -05:00
{
2021-12-20 03:27:33 -06:00
window . app . console . debug ( 'Error: failed to fetch session id! error: ' + this . status ) ;
2020-05-21 15:25:34 -05:00
that . _signalErrorClose ( ) ;
2020-04-28 09:18:24 -05:00
}
2020-06-09 04:44:30 -05:00
else // we connected - lets get going ...
2020-04-28 09:18:24 -05:00
{
that . sessionId = this . responseText ;
that . readyState = 1 ;
that . onopen ( ) ;
2020-06-09 05:29:11 -05:00
that . _setPollInterval ( that . curPollMs ) ;
2020-04-28 09:18:24 -05:00
}
2020-03-20 11:38:14 -05:00
} ) ;
2020-06-08 09:46:29 -05:00
req . addEventListener ( 'loadend' , function ( ) {
2021-12-20 03:27:33 -06:00
window . app . console . debug ( 'Open completed state: ' + that . readyState ) ;
2020-06-08 09:46:29 -05:00
that . openInflight -- ;
} ) ;
2020-03-20 11:38:14 -05:00
req . send ( '' ) ;
2020-06-08 09:46:29 -05:00
this . openInflight ++ ;
2020-03-20 11:38:14 -05:00
} ;
this . send = function ( msg ) {
2020-05-21 20:22:10 -05:00
var hadData = this . sendQueue . length > 0 ;
2020-03-20 11:38:14 -05:00
this . sendQueue = this . sendQueue . concat (
2020-04-18 12:40:59 -05:00
'B0x' + this . outSerial . toString ( 16 ) + '\n' +
'0x' + msg . length . toString ( 16 ) + '\n' + msg + '\n' ) ;
this . outSerial ++ ;
2020-05-21 20:22:10 -05:00
// Send ASAP, if we have throttled.
if ( that . curPollMs > that . minPollMs || ! hadData )
{
// Unless we are backed up.
if ( that . msgInflight <= 3 )
{
2021-12-20 03:27:33 -06:00
//window.app.console.debug('Have data to send, lowering poll interval.');
2020-05-21 20:22:10 -05:00
that . curPollMs = that . minPollMs ;
that . _setPollInterval ( that . curPollMs ) ;
}
}
2020-03-20 11:38:14 -05:00
} ;
2020-05-09 13:41:40 -05:00
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 , '' ) ;
} ;
2020-03-04 07:54:04 -06:00
this . close = function ( ) {
2020-06-08 08:48:57 -05:00
var oldState = this . readyState ;
2021-12-20 03:27:33 -06:00
window . app . console . debug ( 'proxy: close socket' ) ;
2020-03-04 07:54:04 -06:00
this . readyState = 3 ;
this . onclose ( ) ;
2020-06-09 05:29:11 -05:00
clearInterval ( this . pollInterval ) ;
2020-06-08 09:46:29 -05:00
clearTimeout ( this . delaySession ) ;
2020-06-09 05:29:11 -05:00
this . pollInterval = undefined ;
2020-06-08 08:48:57 -05:00
if ( oldState === 1 ) // was open
this . sendCloseMsg ( this . unloading ) ;
2020-05-09 13:41:40 -05:00
this . sessionId = 'open' ;
} ;
this . setUnloading = function ( ) {
this . unloading = true ;
2020-03-04 07:54:04 -06:00
} ;
2020-05-09 13:41:40 -05:00
this . getEndPoint = function ( command ) {
2020-03-04 07:54:04 -06:00
var base = this . uri ;
2020-05-09 13:41:40 -05:00
return base + '/' + this . sessionId + '/' + command + '/' + this . outSerial ;
2020-03-04 07:54:04 -06:00
} ;
2021-12-20 03:27:33 -06:00
window . app . console . debug ( 'proxy: new socket ' + this . id + ' ' + this . uri ) ;
2020-03-04 07:54:04 -06:00
2020-03-20 11:38:14 -05:00
// queue fetch of session id.
this . getSessionId ( ) ;
2020-03-04 07:54:04 -06:00
} ;
2020-03-20 14:05:48 -05:00
if ( global . socketProxy )
{
// re-write relative URLs in CSS - somewhat grim.
window . addEventListener ( 'load' , function ( ) {
2020-06-24 11:39:32 -05:00
var replaceUrls = function ( rules , replaceBase ) {
if ( ! rules )
return ;
2020-03-20 14:05:48 -05:00
for ( var r = 0 ; r < rules . length ; ++ r ) {
2020-06-24 11:39:32 -05:00
// check subset of rules like @media or @import
if ( rules [ r ] && rules [ r ] . type != 1 ) {
replaceUrls ( rules [ r ] . cssRules || rules [ r ] . rules , replaceBase ) ;
continue ;
}
2020-03-20 14:05:48 -05:00
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 ) ;
}
}
2020-06-24 11:39:32 -05:00
} ;
var sheets = document . styleSheets ;
for ( var i = 0 ; i < sheets . length ; ++ i ) {
var relBases = sheets [ i ] . href . split ( '/' ) ;
relBases . pop ( ) ; // bin last - css name.
var replaceBase = 'url("' + relBases . join ( '/' ) + '/images/' ;
var rules ;
try {
rules = sheets [ i ] . cssRules || sheets [ i ] . rules ;
} catch ( err ) {
2021-12-20 03:27:33 -06:00
window . app . console . log ( 'Missing CSS from ' + sheets [ i ] . href ) ;
2020-06-24 11:39:32 -05:00
continue ;
}
replaceUrls ( rules , replaceBase ) ;
2020-03-20 14:05:48 -05:00
}
} , false ) ;
}
2020-03-04 07:54:04 -06:00
global . createWebSocket = function ( uri ) {
2022-01-26 12:30:22 -06:00
if ( 'processCoolUrl' in window ) {
uri = window . processCoolUrl ( { url : uri , type : 'ws' } ) ;
}
2020-03-04 07:54:04 -06:00
if ( global . socketProxy ) {
2020-05-20 09:17:44 -05:00
window . socketProxy = true ;
2020-03-04 07:54:04 -06:00
return new global . ProxySocket ( uri ) ;
} else {
return new WebSocket ( uri ) ;
}
} ;
2019-03-15 11:23:28 -05:00
global . _ = function ( string ) {
// In the mobile app case we can't use the stuff from l10n-for-node, as that assumes HTTP.
2019-12-11 04:46:52 -06:00
if ( window . ThisIsAMobileApp ) {
2019-01-26 15:21:48 -06:00
// We use another approach just for iOS for now.
2020-10-27 07:33:32 -05:00
if ( window . LOCALIZATIONS && Object . prototype . hasOwnProperty . call ( window . LOCALIZATIONS , string ) ) {
2019-01-26 15:21:48 -06:00
// 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 ;
2018-11-15 16:38:40 -06:00
}
2019-03-15 11:23:28 -05:00
} else {
return string . toLocaleString ( ) ;
2018-11-09 14:28:46 -06:00
}
2019-03-15 11:23:28 -05:00
} ;
2018-04-12 14:57:00 -05:00
2021-10-26 08:23:28 -05:00
// Some global variables are defined in cool.html, among them:
2021-08-30 08:27:56 -05:00
// global.host: the host URL, with ws(s):// protocol
2021-08-30 08:43:26 -05:00
// global.serviceRoot: an optional root path on the server, typically blank.
2021-08-30 08:27:56 -05:00
// 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.
}
2019-01-27 12:04:26 -06:00
var docParams , wopiParams ;
var filePath = global . getParameterByName ( 'file_path' ) ;
2021-09-01 19:47:30 -05:00
global . wopiSrc = global . getParameterByName ( 'WOPISrc' ) ;
if ( global . wopiSrc != '' ) {
global . docURL = decodeURIComponent ( global . wopiSrc ) ;
2019-01-27 12:04:26 -06:00
if ( global . accessToken !== '' ) {
wopiParams = { 'access_token' : global . accessToken , 'access_token_ttl' : global . accessTokenTTL } ;
}
else if ( global . accessHeader !== '' ) {
wopiParams = { 'access_header' : global . accessHeader } ;
}
2020-06-01 07:18:13 -05:00
2020-04-22 08:48:18 -05:00
if ( wopiParams ) {
docParams = Object . keys ( wopiParams ) . map ( function ( key ) {
return encodeURIComponent ( key ) + '=' + encodeURIComponent ( wopiParams [ key ] ) ;
} ) . join ( '&' ) ;
}
2019-01-27 12:04:26 -06:00
} else {
global . docURL = filePath ;
}
2021-08-31 06:58:11 -05:00
// Form a valid WS URL to the host with the given path.
global . makeWsUrl = function ( path ) {
2021-12-20 03:27:33 -06:00
window . app . console . assert ( global . host . startsWith ( 'ws' ) , 'host is not ws: ' + global . host ) ;
2021-08-31 06:58:11 -05:00
return global . host + global . serviceRoot + path ;
} ;
2021-09-02 07:46:56 -05:00
// Form a URI from the docUrl and wopiSrc and encodes.
// The docUrlParams, suffix, and wopiSrc are optionally hexified.
global . makeDocAndWopiSrcUrl = function ( root , docUrlParams , suffix , wopiSrcParam ) {
2021-09-01 19:47:30 -05:00
var wopiSrc = '' ;
if ( global . wopiSrc != '' ) {
wopiSrc = '?WOPISrc=' + global . wopiSrc + '&compat=' ;
2021-09-02 07:46:56 -05:00
if ( wopiSrcParam && wopiSrcParam . length > 0 )
wopiSrc += '&' + wopiSrcParam ;
}
else if ( wopiSrcParam && wopiSrcParam . length > 0 ) {
wopiSrc = '?' + wopiSrcParam ;
2021-09-01 19:47:30 -05:00
}
2021-09-02 07:46:56 -05:00
suffix = suffix || '/ws' ;
var encodedDocUrl = encodeURIComponent ( docUrlParams ) + suffix + wopiSrc ;
2021-09-01 19:47:30 -05:00
if ( global . hexifyUrl )
encodedDocUrl = global . hexEncode ( encodedDocUrl ) ;
2021-09-02 07:46:56 -05:00
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 ) ;
2021-09-01 19:47:30 -05:00
} ;
2021-08-31 06:52:38 -05:00
// Form a valid HTTP URL to the host with the given path.
global . makeHttpUrl = function ( path ) {
2021-12-20 03:27:33 -06:00
window . app . console . assert ( global . webserver . startsWith ( 'http' ) , 'webserver is not http: ' + global . webserver ) ;
2021-08-29 06:58:43 -05:00
return global . webserver + global . serviceRoot + path ;
} ;
2021-09-02 07:46:56 -05:00
// 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 ) ;
} ;
2021-08-29 12:35:47 -05:00
// Encode a string to hex.
global . hexEncode = function ( string ) {
var bytes = new TextEncoder ( ) . encode ( string ) ;
var hex = '0x' ;
2022-02-11 12:56:37 -06:00
for ( var i = 0 ; i < bytes . length ; ++ i ) {
2021-08-29 12:35:47 -05:00
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 ) ;
2022-02-11 12:56:37 -06:00
for ( var i = 0 ; i < bytes . length ; i ++ ) {
2021-08-29 12:35:47 -05:00
bytes [ i ] = parseInt ( hex . substr ( i * 2 , 2 ) , 16 ) ;
}
return new TextDecoder ( ) . decode ( bytes ) ;
} ;
2019-03-06 05:32:59 -06:00
if ( window . ThisIsAMobileApp ) {
global . socket = new global . FakeWebSocket ( ) ;
window . TheFakeWebSocket = global . socket ;
} else {
2021-05-12 05:12:35 -05:00
// 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 : '' ;
2021-11-15 09:52:32 -06:00
var websocketURI = global . makeWsUrlWopiSrc ( '/cool/' , global . docURL + docParamsPart ) ;
2019-03-06 05:32:59 -06:00
try {
2020-03-04 07:54:04 -06:00
global . socket = global . createWebSocket ( websocketURI ) ;
2019-03-06 05:32:59 -06:00
} catch ( err ) {
2021-12-20 03:27:33 -06:00
window . app . console . log ( err ) ;
2019-03-06 05:32:59 -06:00
}
2019-01-27 12:04:26 -06:00
}
2019-11-12 12:49:19 -06:00
var lang = global . getParameterByName ( 'lang' ) ;
2019-03-07 12:02:15 -06:00
global . queueMsg = [ ] ;
2019-12-11 04:46:52 -06:00
if ( window . ThisIsAMobileApp )
2019-11-12 12:49:19 -06:00
window . LANG = lang ;
2019-01-27 12:04:26 -06:00
if ( global . socket && global . socket . readyState !== 3 ) {
global . socket . onopen = function ( ) {
if ( global . socket . readyState === 1 ) {
var ProtocolVersionNumber = '0.1' ;
2019-11-12 12:49:19 -06:00
var timestamp = global . getParameterByName ( 'timestamp' ) ;
var msg = 'load url=' + encodeURIComponent ( global . docURL ) ;
2021-05-14 06:24:14 -05:00
var now0 = Date . now ( ) ;
var now1 = performance . now ( ) ;
var now2 = Date . now ( ) ;
2021-11-17 04:16:38 -06:00
global . socket . send ( 'coolclient ' + ProtocolVersionNumber + ' ' + ( ( now0 + now2 ) / 2 ) + ' ' + now1 ) ;
2019-11-12 12:49:19 -06:00
2019-12-11 04:46:52 -06:00
if ( window . ThisIsAMobileApp ) {
2019-11-12 12:49:19 -06:00
msg += ' lang=' + window . LANG ;
} else {
if ( timestamp ) {
msg += ' timestamp=' + timestamp ;
}
if ( lang ) {
msg += ' lang=' + lang ;
}
// renderingOptions?
}
2020-04-20 14:26:21 -05:00
if ( window . deviceFormFactor ) {
msg += ' deviceFormFactor=' + window . deviceFormFactor ;
}
2020-10-14 04:16:47 -05:00
if ( window . isLocalStorageAllowed ) {
var spellOnline = window . localStorage . getItem ( 'SpellOnline' ) ;
2021-02-10 21:05:01 -06:00
if ( spellOnline ) {
msg += ' spellOnline=' + spellOnline ;
}
2020-10-14 04:16:47 -05:00
}
2019-11-12 12:49:19 -06:00
global . socket . send ( msg ) ;
2019-01-27 12:04:26 -06:00
}
2020-01-16 08:44:23 -06:00
} ;
2019-01-27 12:04:26 -06:00
global . socket . onerror = function ( event ) {
2021-12-20 03:27:33 -06:00
window . app . console . log ( event ) ;
2020-01-16 08:44:23 -06:00
} ;
2019-01-27 12:04:26 -06:00
global . socket . onclose = function ( event ) {
2021-12-20 03:27:33 -06:00
window . app . console . log ( event ) ;
2020-01-16 08:44:23 -06:00
} ;
2019-01-27 12:04:26 -06:00
global . socket . onmessage = function ( event ) {
2019-06-19 02:48:54 -05:00
if ( typeof global . socket . _onMessage === 'function' ) {
2020-03-12 22:29:39 -05:00
global . socket . _emptyQueue ( ) ;
2019-03-07 12:02:15 -06:00
global . socket . _onMessage ( event ) ;
} else {
global . queueMsg . push ( event . data ) ;
}
2020-01-16 08:44:23 -06:00
} ;
2019-01-27 12:04:26 -06:00
global . socket . binaryType = 'arraybuffer' ;
2019-03-06 07:29:17 -06:00
if ( window . ThisIsAMobileApp ) {
// 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 ( ) ;
}
2019-01-27 12:04:26 -06:00
}
2018-04-12 14:57:00 -05:00
} ( window ) ) ;