Module AppRuntime EnableExplicit ;- Instance structure Structure Instance InstanceID.i ; stable unique ID — used as the JS↔SB bridge key Window.i View.i AppID.s Perms.s EndStructure Global NewList Instances.Instance() Global _NextID = 1 ; auto-incrementing ID counter ;- Pending async operations ; One pending op of each type at a time (JS is single-threaded). Global _PL_ID = -1 : Global _PL_KMID.s = "" ; list Global _PS_ID = -1 : Global _PS_KMID.s = "" ; stat (standalone) Global _PSR_ID = -1 : Global _PSR_KMID.s = "" ; stat-before-read/delete Global _PR_ID = -1 : Global _PR_KMID.s = "" ; read Global _PW_ID = -1 : Global _PW_KMID.s = "" ; write / mkdir / delete Global _PC_ID = -1 : Global _PC_KMID.s = "" ; confirm dialog ;- Private declarations Declare _FindByID(ID) Declare _FindByWindow(Win) Declare _HasPerm(ID, Token.s) Declare _SendResponse(ID, KMID.s, Success, DataString.s) Declare.s _StoragePath(ID, RelPath.s) Declare _Remove(ID) Declare _CloseByID(ID) Declare _Dispatch(ID, RawMsg.s) Declare _DoList(ID, KMID.s, Path.s) Declare _DoStat(ID, KMID.s, Path.s) Declare _DoStatForRead(ID, KMID.s, Path.s) Declare _DoRead(ID, KMID.s, FileID) Declare _DoWrite(ID, KMID.s, Path.s, Content.s) Declare _DoMkdir(ID, KMID.s, Path.s) Declare _DoDelete(ID, KMID.s, Path.s) Declare _ListCallback(Success, DataString.s) Declare _StatCallback(Success, DataString.s) Declare _StatForReadCallback(Success, DataString.s) Declare _ReadCallback(Success, DataString.s) Declare _WriteCallback(Success, DataString.s) Declare _ConfirmCallback(Result) Declare Handler_Resize() Declare Handler_Close() ;- Public procedures ; Call once from Desktop::Open. Procedure Init() !window._kumos_rt = { instances: {} }; !window.addEventListener('message', function(e) { ! var inst; ! for (var id in window._kumos_rt.instances) { ! inst = window._kumos_rt.instances[id]; ! if (inst && inst.frameEl && inst.frameEl.contentWindow === e.source) { ! if (typeof e.data === 'string') { ! appruntime$f__dispatch(inst.id, e.data); ! } ! return; ! } ! } !}); EndProcedure ; Open a new sandboxed app window. Procedure Launch(AppID.s, ManifestJSON.s, Permissions.s) Protected Count, W, H, Win, View, ID Protected AppName.s, N.s, Src.s AppName = AppID If ParseJSON(0, ManifestJSON) N = GetJSONString(GetJSONMember(JSONValue(0), "name")) If N <> "" : AppName = N : EndIf FreeJSON(0) EndIf Count = ListSize(Instances()) W = 640 H = 480 Win = OpenWindow(#PB_Any, 80 + (Count % 8) * 24, 80 + (Count % 8) * 24, W, H, AppName, #PB_Window_TitleBar | #PB_Window_SizeGadget | #PB_Window_SystemMenu) View = WebGadget(#PB_Any, 0, 0, W, H, "about:blank") Src = "/api/apps/" + AppID + "/index.html" ID = _NextID _NextID + 1 !(function(){ ! var frames = document.querySelectorAll('iframe[src="about:blank"]:not([data-kumos-instance])'); ! var f = frames[frames.length - 1]; ! if (!f) return; ! f.setAttribute('data-kumos-instance', String(v_id)); ! // No allow-same-origin -> cross-origin isolation even from same host. ! f.setAttribute('sandbox', 'allow-scripts allow-forms allow-modals allow-downloads allow-pointer-lock'); ! window._kumos_rt.instances[v_id] = { id: v_id, frameEl: f, appId: v_appid }; ! f.src = v_src; !})(); AddElement(Instances()) Instances()\InstanceID = ID Instances()\Window = Win Instances()\View = View Instances()\AppID = AppID Instances()\Perms = Permissions BindEvent(#PB_Event_SizeWindow, @Handler_Resize(), Win) BindEvent(#PB_Event_CloseWindow, @Handler_Close(), Win) Desktop::Register(AppName, Win, "📦") EndProcedure ;- Private procedures ; Leaves list cursor on the found element. Returns #True if found. Procedure _FindByID(ID) ForEach Instances() If Instances()\InstanceID = ID : ProcedureReturn #True : EndIf Next ProcedureReturn #False EndProcedure Procedure _FindByWindow(Win) ForEach Instances() If Instances()\Window = Win : ProcedureReturn #True : EndIf Next ProcedureReturn #False EndProcedure Procedure _HasPerm(ID, Token.s) If Not _FindByID(ID) : ProcedureReturn #False : EndIf ProcedureReturn Bool(FindString(Instances()\Perms, ~"\"" + Token + ~"\"") > 0) EndProcedure Procedure _SendResponse(ID, KMID.s, Success, DataString.s) !var inst = window._kumos_rt.instances[v_id]; !if (!inst || !inst.frameEl) return; !var resp = { kmid: v_kmid, success: !!v_success }; !if (v_success) { ! try { resp.data = JSON.parse(v_datastring); } ! catch(e) { resp.data = v_datastring || null; } !} else { ! resp.error = v_datastring || 'Error'; !} !try { inst.frameEl.contentWindow.postMessage(JSON.stringify(resp), '*'); } catch(e) {} EndProcedure Procedure.s _StoragePath(ID, RelPath.s) Protected Base.s If Not _FindByID(ID) : ProcedureReturn "" : EndIf Base = "/.apps/" + Instances()\AppID + "/" If RelPath = "" Or RelPath = "/" : ProcedureReturn Base : EndIf If Left(RelPath, 1) = "/" : RelPath = Mid(RelPath, 2) : EndIf ProcedureReturn Base + RelPath EndProcedure Procedure _Remove(ID) If Not _FindByID(ID) : ProcedureReturn : EndIf !delete window._kumos_rt.instances[v_id]; UnbindEvent(#PB_Event_SizeWindow, @Handler_Resize(), Instances()\Window) UnbindEvent(#PB_Event_CloseWindow, @Handler_Close(), Instances()\Window) Desktop::Unregister(Instances()\Window) DeleteElement(Instances()) EndProcedure Procedure _CloseByID(ID) _Remove(ID) EndProcedure ;- Message dispatcher Procedure _Dispatch(ID, RawMsg.s) Protected Root, Args, TType Protected KMID.s, Action.s, Path.s, Content.s, Title_.s, Msg.s, MsgType.s If Not _FindByID(ID) : ProcedureReturn : EndIf If Not ParseJSON(0, RawMsg) : ProcedureReturn : EndIf Root = JSONValue(0) KMID = GetJSONString(GetJSONMember(Root, "kmid")) Action = GetJSONString(GetJSONMember(Root, "action")) Args = GetJSONMember(Root, "args") If KMID = "" : FreeJSON(0) : ProcedureReturn : EndIf Path = GetJSONString(GetJSONMember(Args, "path")) Content = GetJSONString(GetJSONMember(Args, "content")) Title_ = GetJSONString(GetJSONMember(Args, "title")) Msg = GetJSONString(GetJSONMember(Args, "message")) MsgType = GetJSONString(GetJSONMember(Args, "type")) FreeJSON(0) Select Action Case "window.ready" _FindByID(ID) _SendResponse(ID, KMID, #True, ~"{\"app_id\":\"" + ReplaceString(Instances()\AppID, ~"\"", ~"\\\"") + ~"\"}") Case "window.setTitle" _FindByID(ID) SetWindowTitle(Instances()\Window, Title_) _SendResponse(ID, KMID, #True, "") Case "window.close" _SendResponse(ID, KMID, #True, "") !setTimeout(function(){ appruntime$f__closebyid(v_id); }, 50); Case "notify.toast" If Not _HasPerm(ID, "notify") : _SendResponse(ID, KMID, #False, "Permission denied: notify") : ProcedureReturn : EndIf TType = Notify::#Info If MsgType = "success" : TType = Notify::#Success ElseIf MsgType = "warning" : TType = Notify::#Warning ElseIf MsgType = "error" : TType = Notify::#Error EndIf Notify::Toast(Msg, TType) _SendResponse(ID, KMID, #True, "") Case "notify.confirm" If Not _HasPerm(ID, "notify") : _SendResponse(ID, KMID, #False, "Permission denied: notify") : ProcedureReturn : EndIf If _PC_ID >= 0 : _SendResponse(ID, KMID, #False, "A dialog is already open") : ProcedureReturn : EndIf _PC_ID = ID : _PC_KMID = KMID Notify::Confirm(Title_, Msg, @_ConfirmCallback()) Case "storage.list" : _DoList(ID, KMID, _StoragePath(ID, Path)) Case "storage.stat" : _DoStat(ID, KMID, _StoragePath(ID, Path)) Case "storage.read" : _DoStatForRead(ID, KMID, _StoragePath(ID, Path)) Case "storage.write" : _DoWrite(ID, KMID, _StoragePath(ID, Path), Content) Case "storage.mkdir" : _DoMkdir(ID, KMID, _StoragePath(ID, Path)) Case "storage.delete" : _DoDelete(ID, KMID, _StoragePath(ID, Path)) Case "fs.list" If Not _HasPerm(ID, "fs.read") : _SendResponse(ID, KMID, #False, "Permission denied: fs.read") : ProcedureReturn : EndIf _DoList(ID, KMID, Path) Case "fs.stat" If Not _HasPerm(ID, "fs.read") : _SendResponse(ID, KMID, #False, "Permission denied: fs.read") : ProcedureReturn : EndIf _DoStat(ID, KMID, Path) Case "fs.read" If Not _HasPerm(ID, "fs.read") : _SendResponse(ID, KMID, #False, "Permission denied: fs.read") : ProcedureReturn : EndIf _DoStatForRead(ID, KMID, Path) Case "fs.write" If Not _HasPerm(ID, "fs.write") : _SendResponse(ID, KMID, #False, "Permission denied: fs.write") : ProcedureReturn : EndIf _DoWrite(ID, KMID, Path, Content) Default _SendResponse(ID, KMID, #False, "Unknown action: " + Action) EndSelect EndProcedure ;- Async FS helpers Procedure _DoList(ID, KMID.s, Path.s) If _PL_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf _PL_ID = ID : _PL_KMID = KMID HTTPRequest(#PB_HTTP_Get, "/api/fs/list?path=" + URLEncoder(Path, #PB_UTF8), "", @_ListCallback()) EndProcedure Procedure _DoStat(ID, KMID.s, Path.s) If _PS_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf _PS_ID = ID : _PS_KMID = KMID HTTPRequest(#PB_HTTP_Get, "/api/fs/stat?path=" + URLEncoder(Path, #PB_UTF8), "", @_StatCallback()) EndProcedure Procedure _DoStatForRead(ID, KMID.s, Path.s) If _PSR_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf _PSR_ID = ID : _PSR_KMID = KMID HTTPRequest(#PB_HTTP_Get, "/api/fs/stat?path=" + URLEncoder(Path, #PB_UTF8), "", @_StatForReadCallback()) EndProcedure Procedure _DoRead(ID, KMID.s, FileID) If _PR_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf _PR_ID = ID : _PR_KMID = KMID HTTPRequest(#PB_HTTP_Get, "/api/fs/read?id=" + FileID, "", @_ReadCallback()) EndProcedure Procedure _DoWrite(ID, KMID.s, Path.s, Content.s) If _PW_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf _PW_ID = ID : _PW_KMID = KMID HTTPRequest(#PB_HTTP_Post, "/api/fs/write", "path=" + URLEncoder(Path, #PB_UTF8) + "&content=" + URLEncoder(Content, #PB_UTF8), @_WriteCallback()) EndProcedure Procedure _DoMkdir(ID, KMID.s, Path.s) If _PW_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf _PW_ID = ID : _PW_KMID = KMID HTTPRequest(#PB_HTTP_Post, "/api/fs/mkdir", "path=" + URLEncoder(Path, #PB_UTF8), @_WriteCallback()) EndProcedure Procedure _DoDelete(ID, KMID.s, Path.s) If _PSR_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf _PSR_ID = ID : _PSR_KMID = "DEL:" + KMID HTTPRequest(#PB_HTTP_Get, "/api/fs/stat?path=" + URLEncoder(Path, #PB_UTF8), "", @_StatForReadCallback()) EndProcedure ;- Callbacks Procedure _ListCallback(Success, DataString.s) Protected ID Protected KMID.s If _PL_ID < 0 : ProcedureReturn : EndIf ID = _PL_ID : KMID = _PL_KMID _PL_ID = -1 : _PL_KMID = "" _SendResponse(ID, KMID, Success, DataString) EndProcedure Procedure _StatCallback(Success, DataString.s) Protected ID Protected KMID.s If _PS_ID < 0 : ProcedureReturn : EndIf ID = _PS_ID : KMID = _PS_KMID _PS_ID = -1 : _PS_KMID = "" _SendResponse(ID, KMID, Success, DataString) EndProcedure Procedure _StatForReadCallback(Success, DataString.s) Protected ID, IsDelete, IsDir, FileID, Root Protected Tag.s, KMID.s If _PSR_ID < 0 : ProcedureReturn : EndIf ID = _PSR_ID Tag = _PSR_KMID _PSR_ID = -1 : _PSR_KMID = "" IsDelete = Bool(Left(Tag, 4) = "DEL:") If IsDelete : KMID = Mid(Tag, 5) : Else : KMID = Tag : EndIf If Not Success : _SendResponse(ID, KMID, #False, "Not found") : ProcedureReturn : EndIf If Not ParseJSON(0, DataString) : _SendResponse(ID, KMID, #False, "Bad stat response") : ProcedureReturn : EndIf Root = JSONValue(0) IsDir = GetJSONInteger(GetJSONMember(Root, "is_dir")) FileID = GetJSONInteger(GetJSONMember(Root, "id")) FreeJSON(0) If FileID <= 0 : _SendResponse(ID, KMID, #False, "Not found") : ProcedureReturn : EndIf If IsDelete If _PW_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf _PW_ID = ID : _PW_KMID = KMID HTTPRequest(#PB_HTTP_Post, "/api/fs/delete", "id=" + FileID, @_WriteCallback()) Else If IsDir : _SendResponse(ID, KMID, #False, "Is a directory") : ProcedureReturn : EndIf _DoRead(ID, KMID, FileID) EndIf EndProcedure Procedure _ReadCallback(Success, DataString.s) Protected ID Protected KMID.s If _PR_ID < 0 : ProcedureReturn : EndIf ID = _PR_ID : KMID = _PR_KMID _PR_ID = -1 : _PR_KMID = "" _SendResponse(ID, KMID, Success, DataString) EndProcedure Procedure _WriteCallback(Success, DataString.s) Protected ID, OK Protected KMID.s If _PW_ID < 0 : ProcedureReturn : EndIf ID = _PW_ID : KMID = _PW_KMID _PW_ID = -1 : _PW_KMID = "" If Not Success : _SendResponse(ID, KMID, #False, "Request failed") : ProcedureReturn : EndIf If ParseJSON(0, DataString) OK = GetJSONBoolean(GetJSONMember(JSONValue(0), "success")) FreeJSON(0) _SendResponse(ID, KMID, OK, "") Else _SendResponse(ID, KMID, #False, "Bad server response") EndIf EndProcedure Procedure _ConfirmCallback(Result) Protected ID Protected KMID.s, BoolStr.s If _PC_ID < 0 : ProcedureReturn : EndIf ID = _PC_ID : KMID = _PC_KMID _PC_ID = -1 : _PC_KMID = "" BoolStr = "false" If Result : BoolStr = "true" : EndIf _SendResponse(ID, KMID, #True, BoolStr) EndProcedure ;- Window event handlers Procedure Handler_Resize() If Not _FindByWindow(EventWindow()) : ProcedureReturn : EndIf ResizeGadget(Instances()\View, 0, 0, WindowWidth(Instances()\Window), WindowHeight(Instances()\Window)) EndProcedure Procedure Handler_Close() If Not _FindByWindow(EventWindow()) : ProcedureReturn : EndIf _Remove(Instances()\InstanceID) EndProcedure EndModule ; IDE Options = SpiderBasic 3.20 (Windows - x86) ; CursorPosition = 2 ; Folding = BAAA9 ; iOSAppOrientation = 0 ; AndroidAppCode = 0 ; AndroidAppOrientation = 0 ; EnableXP ; DPIAware ; CompileSourceDirectory