From cce6531d45cf815e0cf80ba52f48bbcd865aaecd Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Tue, 9 Apr 2024 15:08:36 +0100 Subject: [PATCH 1/3] Notify parents/children when an app closes Send an appClosed message with the instance ID of the app that was closed. This will be picked up by Puter.js's AppConnection and reported as a 'close' event. To make this work, a `data-parent_instance_id` attribute is set on child app windows. This is very similar to the `data-parent_uuid` attribute, which tracks parent windows instead of parent app instances. (Dialogs have a parent window, but are not apps, so don't have a parent app instance.) The difference is subtle, and we may want to combine these in the future, but currently closing an app will close any child windows, which is not behaviour we want for child apps. --- src/UI/UIWindow.js | 28 +++++++++++++++++++++++++++- src/helpers.js | 4 ++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/UI/UIWindow.js b/src/UI/UIWindow.js index a922e4033..3134c23e7 100644 --- a/src/UI/UIWindow.js +++ b/src/UI/UIWindow.js @@ -185,6 +185,7 @@ async function UIWindow(options) { data-uid ="${options.uid}" data-element_uuid="${options.element_uuid}" data-parent_uuid="${options.parent_uuid}" + ${options.parent_instance_id ? `data-parent_instance_id="${options.parent_instance_id}"` : ''} data-id ="${win_id}" data-iframe_msg_uid ="${options.iframe_msg_uid}" data-is_dir ="${options.is_dir}" @@ -2745,8 +2746,9 @@ $.fn.close = async function(options) { options = options || {}; $(this).each(async function() { const el_iframe = $(this).find('.window-app-iframe'); + const app_uses_sdk = el_iframe.length > 0 && el_iframe.attr('data-appUsesSDK') === 'true'; // tell child app that this window is about to close, get its response - if(el_iframe.length > 0 && el_iframe.attr('data-appUsesSDK') === 'true'){ + if(app_uses_sdk){ if(!options.bypass_iframe_messaging){ const resp = await sendWindowWillCloseMsg(el_iframe.get(0)); if(!resp.msg){ @@ -2819,6 +2821,30 @@ $.fn.close = async function(options) { } // close child windows $(`.window[data-parent_uuid="${window_uuid}"]`).close(); + + // notify other apps that we're closing + if (app_uses_sdk) { + // notify parent app, if we have one, that we're closing + const parent_id = this.dataset['parent_instance_id']; + const parent = $(`.window[data-element_uuid="${parent_id}"] .window-app-iframe`).get(0); + if (parent) { + parent.contentWindow.postMessage({ + msg: 'appClosed', + appInstanceID: window_uuid, + }, '*'); + } + + // notify child apps, if we have them, that we're closing + const children = $(`.window[data-parent_instance_id="${window_uuid}"] .window-app-iframe`); + children.each((_, child) => { + child.contentWindow.postMessage({ + msg: 'appClosed', + appInstanceID: window_uuid, + }, '*'); + }); + // TODO: Once other AppConnections exist, those will need notifying too. + } + // remove backdrop $(this).closest('.window-backdrop').remove(); // remove DOM element diff --git a/src/helpers.js b/src/helpers.js index a8486d11e..d977e1b20 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -1853,6 +1853,10 @@ window.launch_app = async (options)=>{ let icon, title, file_signature; const window_options = options.window_options ?? {}; + if (options.parent_instance_id) { + window_options.parent_instance_id = options.parent_instance_id; + } + // try to get 3rd-party app info let app_info = options.app_obj ?? await get_apps(options.name); From c69a0abfa9514cd0e1cf08eba3a02854f4544b2a Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Tue, 9 Apr 2024 14:52:53 +0100 Subject: [PATCH 2/3] Extract a window_for_app_instance() helper function --- src/IPC.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/IPC.js b/src/IPC.js index de771e447..92b0855a0 100644 --- a/src/IPC.js +++ b/src/IPC.js @@ -74,11 +74,15 @@ window.addEventListener('message', async (event) => { return; } - const iframe_for_app_instance = (instanceID) => { - return $(`.window[data-element_uuid="${instanceID}"]`).find('.window-app-iframe').get(0) + const window_for_app_instance = (instance_id) => { + return $(`.window[data-element_uuid="${instance_id}"]`).get(0); }; - const $el_parent_window = $(`.window[data-element_uuid="${event.data.appInstanceID}"]`); + const iframe_for_app_instance = (instance_id) => { + return $(window_for_app_instance(instance_id)).find('.window-app-iframe').get(0); + }; + + const $el_parent_window = $(window_for_app_instance(event.data.appInstanceID)); const parent_window_id = $el_parent_window.attr('data-id'); const $el_parent_disable_mask = $el_parent_window.find('.window-disable-mask'); const target_iframe = iframe_for_app_instance(event.data.appInstanceID); @@ -354,7 +358,7 @@ window.addEventListener('message', async (event) => { // setWindowTitle //-------------------------------------------------------- else if(event.data.msg === 'setWindowTitle' && event.data.new_title !== undefined){ - const el_window = $(`.window[data-element_uuid="${event.data.appInstanceID}"]`).get(0); + const el_window = window_for_app_instance(event.data.appInstanceID); // set window title $(el_window).find(`.window-head-title`).html(html_encode(event.data.new_title)); // send confirmation to requester window @@ -1101,6 +1105,6 @@ window.addEventListener('message', async (event) => { // exit //-------------------------------------------------------- else if(event.data.msg === 'exit'){ - $(`.window[data-element_uuid="${event.data.appInstanceID}"]`).close({bypass_iframe_messaging: true}); + $(window_for_app_instance(event.data.appInstanceID)).close({bypass_iframe_messaging: true}); } }); \ No newline at end of file From 21c64e827bc96dfbf1f314ef8fdc63671f8a9410 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Tue, 9 Apr 2024 15:05:48 +0100 Subject: [PATCH 3/3] Add closeApp message Sending a 'closeApp' message allows an app to close a target app, if it has permission to do so. Currently, permission is granted if the requesting app is the parent of the target app, or has godmode set. --- src/IPC.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/IPC.js b/src/IPC.js index 92b0855a0..54c53e4fc 100644 --- a/src/IPC.js +++ b/src/IPC.js @@ -1100,6 +1100,43 @@ window.addEventListener('message', async (event) => { contents, }, targetAppOrigin); } + //-------------------------------------------------------- + // closeApp + //-------------------------------------------------------- + else if (event.data.msg === 'closeApp') { + const { appInstanceID, targetAppInstanceID } = event.data; + + const target_window = window_for_app_instance(targetAppInstanceID); + if (!target_window) { + console.warn(`Failed to close non-existent app ${targetAppInstanceID}`); + return; + } + + // Check permissions + const allowed = (() => { + // Parents can close their children + if (target_window.dataset['parent_instance_id']) { + console.log(`⚠️ Allowing app ${appInstanceID} to close child app ${targetAppInstanceID}`); + return true; + } + + // God-mode apps can close anything + const app_info = await get_apps(app_name); + if (app_info.godmode === 1) { + console.log(`⚠️ Allowing GODMODE app ${appInstanceID} to close app ${targetAppInstanceID}`); + return true; + } + + // TODO: What other situations should we allow? + return false; + })(); + + if (allowed) { + $(target_window).close(); + } else { + console.warn(`⚠️ App ${appInstanceID} is not permitted to close app ${targetAppInstanceID}`); + } + } //-------------------------------------------------------- // exit