/////////////////////////////////////////////////////////////////////////////// /* acme.js Some minimal shared infrastructure used by aLaMode, DCLinabox (as a child only) and MonDeSi, and allowing each other's monitoring iframes to be embedded in the other. This JavaScript is loaded into the top-level, parent page. An uncommon name was chosen for the module. COPYRIGHT --------- Copyright (C) 2014-2020 Mark G.Daniel This program, comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under the conditions of the GNU GENERAL PUBLIC LICENSE, version 3, or any later version. http://www.gnu.org/licenses/gpl.txt VERSION ------- 03-DEC-2020 MGD bugfix; acmeIpcOpen() params = '?data=1;' + params; 11-JAN-2015 MGD v1.0.0 release 02-FEB-2014 MGD initial development */ /////////////////////////////////////////////////////////////////////////////// var $AcmeVersion = '1.0.1'; // some invoking executable defined values if (typeof $ExeVersion == 'undefined') throw '$ExeVersion?'; if (typeof $ScriptName == 'undefined') throw '$ScriptName?'; if (typeof $WebSocket == 'undefined') throw '$WebSocket?'; if (!window.WebSocket) $WebSocket = false; if (!window.XMLHttpRequest) throw 'XMLHttpRequest?'; var acme_AppName; var JSON_SENTINAL = '"""'; // shortcuts var $byId = document.getElementById.bind(document); var $byName = document.getElementsByName.bind(document); var $byTag = document.getElementsByTagName.bind(document); acmeBaseHref(); // a child always has a frame number delivered to it in the query string var acme_IsChild = (location.search.substr(0,7) == '?frame='); function acmeIsChild () { return acme_IsChild; } function acmeIsParent () { return !acme_IsChild; } // frames (children) are numbered 1..n with zero indicating the parent var acme_FrameNumber = parseInt(location.search.substr('?frame='.length)); function acmeFrameNumber () { return acme_FrameNumber; } // standlone window (cf. embedded) var acme_StandAlone = (window.location.search.search('salone=1') > 0) || (window.location.search.search('monitor=2') > 0); function acmeStandAlone () { return acme_StandAlone; } /////////////////////////////////////////////////////////////////////////////// /* Derive an application identifying name from the script name. */ function acmeAppName () { if (acme_AppName) return acme_AppName; var lio = $ScriptName.lastIndexOf('/'); if (lio == -1) var name = $ScriptName.toLowerCase(); else var name = $ScriptName.substr(lio+1).toLowerCase(); lio = name.lastIndexOf('.exe'); if (lio == -1) lio = name.lastIndexOf('.com'); if (lio != -1) name = name.substring(0,lio); // support script name obfuscation by eliminating all but alphanumerics name = name.replace(/[^a-zA-Z0-9-]/g,''); return (acme_AppName = name); } /////////////////////////////////////////////////////////////////////////////// /* Set the default base href. */ function acmeBaseHref () { var base = document.createElement('base'); base.href = '/' + acmeAppName() + '/-/'; $byTag('head')[0].appendChild(base); } /////////////////////////////////////////////////////////////////////////////// /* Dynamically load a file. Multiple files are loaded serially IN-ORDER. Can be JavaScript (.js), style sheet (.css) or other (e.g. .html). Callback is optional. Non-JavaScript, non-style-sheet files must have an associated callback via which the content is delivered. */ if (typeof acme_LoadFileName == 'undefined') { // do not reinitialise if this script is called multiple times var acme_LoadFileName = []; var acme_LoadFileCall = []; var acme_LoadFileIndex = 0; } function acmeLoadFile(filename,callback) { if (arguments.length) { acme_LoadFileName.push(filename); if (typeof callback == 'undefined') callback = null; acme_LoadFileCall.push(callback); if (acme_LoadFileIndex < acme_LoadFileName.length-1) return; } var fname = acme_LoadFileName[acme_LoadFileIndex]; var ext = fname.match(/\.[0-9a-z]+$/i); if (ext == '.js' || ext == '.css') { if (ext == '.js') { hndl = document.createElement('script'); hndl.setAttribute('type','text/javascript'); hndl.setAttribute('language','JavaScript'); hndl.setAttribute('src',fname); } else { hndl = document.createElement('link'); hndl.setAttribute("rel", "stylesheet"); hndl.setAttribute('type','text/css'); hndl.setAttribute('href',fname); } hndl.onerror = function() { alert('Failed to load: ' + fname); throw new Error('Failed to load: ' + fname); }; hndl.onload = function() { var callb = acme_LoadFileCall[acme_LoadFileIndex]; if (callb) { if (typeof callb == 'string') eval(callb); else callb(hndl.responseText); } if (++acme_LoadFileIndex < acme_LoadFileName.length) setTimeout(acmeLoadFile,1); }; document.getElementsByTagName('head')[0].appendChild(hndl); } else { var hndl = new XMLHttpRequest(); hndl.open("GET",fname); hndl.onreadystatechange = function() { if (hndl.readyState == 4) { if (hndl.status == 200) { var callb = acme_LoadFileCall[acme_LoadFileIndex]; if (callb) { if (typeof callb == 'string') { // string must be in the form: (responseText); var responseText = hndl.responseText; eval(callb); } else callb(hndl.responseText); } if (++acme_LoadFileIndex < acme_LoadFileName.length) setTimeout(acmeLoadFile,1); } else { alert('Failed to load: ' + fname); throw new Error('Failed to load: ' + fname); } } } hndl.send(); } } /////////////////////////////////////////////////////////////////////////////// /* Set a child iframe width and height. */ var acme_ThisFrame = null, acme_FitWindowTimer = null; if (acme_IsChild) { // periodically check that the iframe size has been adjusted setInterval (acmeAdjustSize, 4900); } else if (acme_StandAlone) { // in the parent ensure a standalone page size is periodically adjusted acme_FitWindowTimer = setInterval (acmeFitWindow, 5000); window.addEventListener ('resize', acmeFitWindow); } function acmeAdjustSize () { if (arguments.length == 0) { // in child iframe if (!acme_ThisFrame) { var number = parseInt(location.search.substr('?frame='.length)); acme_ThisFrame = 'FRAME' + number; } setTimeout('acmeAdjustSize(true)',100); } else if (arguments.length == 1) { // in child iframe var div; if (!(div = $byId('acmeAppDiv'))) if (!(div = $byName('acmeAppDiv')[0])) div = document.body.firstChild; var displayWidth = Math.max(div.scrollWidth, div.offsetWidth+20, div.clientWidth+20); var displayHeight = Math.max(div.scrollHeight, div.offsetHeight+20, div.clientHeight+20); var json = '{"$SetIframeSize":true'; json += ',"frame":"' + acme_ThisFrame + '"'; json += ',"width":' + displayWidth; json += ',"height":' + displayHeight; json += '}'; window.parent.postMessage(json,'*'); } else if (arguments.length > 1) { // in parent window var frame = arguments[0]; var width = arguments[1]; var height = arguments[2]; var obj = $byId(frame); obj.width = obj.style.width = width + "px"; obj.height = obj.style.height = height + "px"; if (acme_StandAlone) acmeFitWindow(); } } /////////////////////////////////////////////////////////////////////////////// /* Fit the open()ed window to the sections of the page (generally iframes). The window size changes with elements' being made hidden and visible. */ var acme_FitHeight = 0, acme_FitLast = 0, acme_FitTimer = null, acme_FitWidth = 0; function acmeFitWindow (forceFit) { var cnt, obj, height = 0, width = 0; if (acme_IsChild) { // in child iframe, pass the message up to the parent window.parent.postMessage('{"$FitWindow":true}','*'); return; } // no more than every 200mS var last = ((new Date()).getTime() / 200).toFixed(0); if (last == acme_FitLast) return; acme_FitLast = last; // provide a single trailing timer event that forces a fit if (acme_FitTimer) { clearInterval(acme_FitTimer); acme_FitTimer = null; } if (!forceFit) acme_FitTimer = setTimeout('acmeFitWindow(true)',250); for (cnt = 1; true; cnt++) { if (!(obj = $byId('SECTION'+cnt))) break; width = Math.max(width,obj.scrollWidth,obj.offsetWidth,obj.clientWidth); height += Math.max(obj.scrollHeight,obj.offsetHeight,obj.clientHeight); } if (height) height += 15; for (cnt = 1; true; cnt++) { if (!(obj = $byId('FRAME'+cnt))) break; width = Math.max(width,obj.scrollWidth,obj.offsetWidth,obj.clientWidth); height += Math.max(obj.scrollHeight,obj.offsetHeight,obj.clientHeight); height += 5; } // plus window accoutrements width += (window.outerWidth - window.innerWidth); height += (window.outerHeight - window.innerHeight); var ht = $byTag('html')[0]; ht.style.overflowX = 'hidden'; // if the fit is not being forced and it's the same size as last time if (!forceFit && acme_FitWidth == width && acme_FitHeight == height) return; acme_FitWidth = width; acme_FitHeight = height; if (acme_FitHeight >= screen.availHeight) { ht.style.overflowY = 'scroll'; acme_FitWidth += 15; } else ht.style.overflowY = 'hidden'; window.resizeTo(acme_FitWidth,acme_FitHeight); } /////////////////////////////////////////////////////////////////////////////// /* Change the window title. Remotely called via acmePostedMessage() via postMessage(). */ var acme_TitleArray = new Array(); var acme_TitleTitle = null; function acmeAddToTitle (string) { // only add the once if (acme_TitleArray.indexOf(string) != -1) return; if (!acme_TitleTitle) acme_TitleTitle = document.title; acme_TitleArray.push(string); acme_TitleArray.sort(function(s1,s2){return s1.localeCompare(s2)}); document.title = ''; for (var idx = 0; idx < acme_TitleArray.length; idx++) document.title += acme_TitleArray[idx] + '\u00a0~\u00a0'; document.title += acme_TitleTitle; } /////////////////////////////////////////////////////////////////////////////// /* Manipulate an array containing a boolean representing the success or not of the frame load. When the frame(s) are created by the iframe builder the array is set to false. When acmeCheckIframe() is called by the frame onload=.. it uses this function (incestuously called via a postMessage() RPC) to check for a successful load and provide an alert in the parent window if not. Must be capable of working cross-domain. */ // only need this in the iframe(s) parent var acme_IframeCheckParam = []; var acme_IframeCheckTimer = []; function acmeIframeCheck () { var fnumber = arguments[0]; var param = arguments[1]; if (acme_IsChild) { // child (inside iframe) if (typeof arguments[0] != 'number') { param = arguments[0]; fnumber = acme_FrameNumber; } if (typeof param == 'undefined') param = null; // provide the parent with the parameter var json = '{"$IframeCheck":true,"frame":' + fnumber; if (typeof param == 'string') json += ',"param":"' + param + '"}'; else json += ',"param":' + param + '}'; window.parent.postMessage(json,'*'); } else { // parent (of iframe(s)) if (typeof param == 'undefined') { // iframe onload=".." irrespective of success or fail if (!acme_IframeCheckTimer[fnumber]) { // give a loaded resource's JavaScript time to set the boolean acme_IframeCheckTimer[fnumber] = setTimeout('acmeIframeCheck('+fnumber+')',5000); return; } } else if (typeof param == 'string') { // initial call to indicate we're begining to load this frame acme_IframeCheckParam[fnumber] = param; return; } if (param == true) acme_IframeCheckParam[fnumber] = param; if (acme_IframeCheckParam[fnumber] == true) return; var URI = acme_IframeCheckParam[fnumber]; var msg = '"' + URI + '" failed to load. Retry?'; if (confirm(msg)) { // retry the iframe load var ifr = $byId('FRAME'+fnumber); ifr.setAttribute('src','javascript:'); ifr.setAttribute('src',URI); ifr.setAttribute('onload','acmeIframeCheck('+fnumber+')'); } } } /////////////////////////////////////////////////////////////////////////////// /* Double-duty; receive message from a (potentially cross-domain) child via postMessage(), and receives message from (potentially cross-domain) parent postMessage(). */ function acmePostedMessage(event) { if (typeof event.data != 'string') throw 'non-string postMessage()'; var data = JSON.parse(event.data); if (data.$SetIframeSize) acmeAdjustSize(data.frame, data.width, data.height); else if (data.$FitWindow) acmeFitWindow(); else if (data.$IframeCheck) acmeIframeCheck(data.frame,data.param); else if (data.$AddToTitle) acmeAddToTitle(data.node); else if (data.$ConfigCollect) $CollectClick(data.checked); else if (console.log) console.log(JSON.stringify(data)); else alert(JSON.stringify(data)); } /////////////////////////////////////////////////////////////////////////////// /* Called from acmePostedMessage() from the overall configuration panel to open/close the frame's WebSocket connection. */ function acmeConfigCollect (checked) { if (checked) acmeIpcOpen(); else acmeIpcClose(); } /////////////////////////////////////////////////////////////////////////////// /* WASD IPC WebSocket/XHR management. Transparently handles WebSocket-enabled environments (combination of WASD and browser) and the fallback to a slightly clunkier XMLHttpResquest() implementation. This IPC is only designed to support the single streaming JSON data stream associated with these WASD application. */ var acme_IpcCount = 0, acme_IpcConn = null, acme_IpcOnClose = null, acme_IpcOnMessage = null, acme_IpcOnOpen = null, acme_IpcReconnect = null, acme_IpcXrhParseStart = 0, acme_IpcXhrParseTimer = null; // register a function for when the WebSocket closes function acmeIpcOnClose (callback) { if (callback != null && typeof callback != 'function') throw 'not a function'; acme_IpcOnClose = callback; } // register a function to receive data function acmeIpcOnMessage (callback) { if (callback != null && typeof callback != 'function') throw 'not a function'; acme_IpcOnMessage = callback; } // register a function to execute when opened function acmeIpcOnOpen (callback) { if (callback != null && typeof callback != 'function') throw 'not a function'; acme_IpcOnOpen = callback; } // return the WebSocket/XHR summary function acmeIpcSummary () { var txt; if ($WebSocket) txt = 'WebSocket()\n(re)connections: ' + acme_IpcCount + '\n'; else txt = 'XMLHttpRequest()\n(re)connections: ' + acme_IpcCount + '\n' + 'responseText.length: ' + acme_IpcConn.responseText.length + '\n'; return txt; } /* With an XMLHttpRequest() the response content stream is returned in periodic bursts of data. Parse these progressive bursts using the sentinal string (three consecutive quote marks should never occur in well-formed JSON). */ function acmeIpcXhrParseData () { var data, end, len, start, txt; start = acme_IpcXrhParseStart; // accomodate MSIE (again) if (typeof acme_IpcConn.responseText == 'unknown') return; txt = acme_IpcConn.responseText; len = txt.length; if (len <= start+1) return; for (;;) { end = txt.substr(start).indexOf(JSON_SENTINAL); if (end == -1) break; end += start; data = txt.substring(start,end); start = end + 3; if (acme_IpcOnMessage) acme_IpcOnMessage(data); } acme_IpcXrhParseStart = start; } function acmeIpcOpen (params) { if (acme_IpcConn) return true; if (typeof params == 'undefined') params = ''; // reopen after a short random period of time var tout = 1500 + Math.floor((Math.random()*1000)); acme_IpcReconnect = 'setTimeout("acmeIpcOpen(\'' + params + '\')",' + tout + ')'; if ($WebSocket) { if (window.location.protocol == 'http:') var URL = 'ws://'; else var URL = 'wss://'; URL += window.location.host + $ScriptName + '?data=1'; acme_IpcConn = new WebSocket(URL); acme_IpcConn.onopen = function(evt) { acme_IpcCount++; if (acme_IpcOnOpen) acme_IpcOnOpen(); }; acme_IpcConn.onclose = function (evt) { acmeIpcClosure(evt) }; acme_IpcConn.onmessage = function (evt) { if (acme_IpcOnMessage) { // unpack multiple JSON data from a single message var data = evt.data.split(JSON_SENTINAL); for (var idx = 0; idx < data.length; idx++) if (data[idx].length) acme_IpcOnMessage(data[idx]); } }; } else { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function () { if (xhr.readyState == 2) { acme_IpcCount++; if (acme_IpcOnOpen) acme_IpcOnOpen(); } else if (xhr.readyState == 4) { // request complete (may be OK may be disconnected */ acmeIpcClosure (); } } acme_IpcConn = xhr; xhr.onerror = function() { acmeIpcClosure (); } xhr.onabort = function() { acmeIpcClosure (); } acme_IpcXrhParseStart = 0; if (typeof xhr.onprogress == 'undefined') acme_IpcXhrParseTimer = setInterval('acmeIpcXhrParseData()',1000); else xhr.onprogress = acmeIpcXhrParseData; if (params.length) params = '?data=1&' + params; else params = '?data=1'; xhr.open('GET',$ScriptName+params,true); xhr.send(); } return true; } // handle the closure function acmeIpcClosure () { /* https://tools.ietf.org/html/rfc6455#section-7.4.1 */ /* browser error, assume WebSocket is not supported by server */ // if (event.code == 1006) $WebSocket = false; // can be called multiple times (ie.e onabort, onerror, readyState == 4) if (!acme_IpcConn) return; if ($WebSocket) acme_IpcConn.close(); acme_IpcConn = null; if (acme_IpcXhrParseTimer) { clearInterval(acme_IpcXhrParseTimer); acme_IpcXhrParseTimer = null; } if (acme_IpcOnClose) acme_IpcOnClose(); // if intended to reconnect (either re-establish or new with params) if (acme_IpcReconnect) setTimeout(acme_IpcReconnect,10); } // explicitly close the WebSocket/XHR function acmeIpcClose (reconnect) { if (acme_IpcConn) { if (typeof reconnect == 'undefined') acme_IpcReconnect = null; else acme_IpcReconnect = reconnect; if ($WebSocket) acme_IpcConn.close(); else acme_IpcConn.abort(); } else if (reconnect) setTimeout(reconnect,10); } // send a WebSocket/XHR message function acmeIpcSend (msg) { if (acme_IpcConn) { if ($WebSocket) acme_IpcConn.send(msg); else { // much heavier-handed with XHR acmeIpcClose('acmeIpcOpen("'+msg+'")'); } } } function acmeIpcConnected () { if (acme_IpcConn) return true; else return false; } function acmeIpcCount () { return acme_IpcCount; } /////////////////////////////////////////////////////////////////////////////// /* In-line code to load the corresponding JavaScript code and when loaded to execute a callback of the same name prefixed by a dollar. */ acmeLoadFile (acmeAppName()+'.js','$'+acmeAppName()+'()'); ///////////////////////////////////////////////////////////////////////////////