mirror of
https://github.com/HeyPuter/puter.git
synced 2025-01-31 12:38:43 +08:00
0aa5543397
Apps are not required to use the Puter SDK. If they don't, then we can still launch them, close them, and listen to their close event, but are unable to send messages to them.
1132 lines
47 KiB
JavaScript
1132 lines
47 KiB
JavaScript
/**
|
|
* Copyright (C) 2024 Puter Technologies Inc.
|
|
*
|
|
* This file is part of Puter.
|
|
*
|
|
* Puter is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import UIAlert from './UI/UIAlert.js';
|
|
import UIWindow from './UI/UIWindow.js';
|
|
import UIWindowSignup from './UI/UIWindowSignup.js';
|
|
import UIWindowRequestPermission from './UI/UIWindowRequestPermission.js';
|
|
import UIItem from './UI/UIItem.js'
|
|
import UIWindowFontPicker from './UI/UIWindowFontPicker.js';
|
|
import UIWindowColorPicker from './UI/UIWindowColorPicker.js';
|
|
import UIPrompt from './UI/UIPrompt.js';
|
|
import download from './helpers/download.js';
|
|
import path from "./lib/path.js";
|
|
|
|
/**
|
|
* In Puter, apps are loaded in iframes and communicate with the graphical user interface (GUI) aand each other using the postMessage API.
|
|
* The following sets up an Inter-Process Messaging System between apps and the GUI that enables communication
|
|
* for various tasks such as displaying alerts, prompts, managing windows, handling file operations, and more.
|
|
*
|
|
* The system listens for 'message' events on the window object, handling different types of messages from the app (which is loaded in an iframe),
|
|
* such as ALERT, createWindow, showOpenFilePicker, ...
|
|
* Each message handler performs specific actions, including creating UI windows, handling file saves and reads, and responding to user interactions.
|
|
*
|
|
* Precautions are taken to ensure proper usage of appInstanceIDs and other sensitive information.
|
|
*/
|
|
window.addEventListener('message', async (event) => {
|
|
const app_env = event.data?.env ?? 'app';
|
|
|
|
// Only process messages from apps
|
|
if(app_env !== 'app')
|
|
return;
|
|
|
|
// --------------------------------------------------------
|
|
// A response to a GUI message received from the app.
|
|
// --------------------------------------------------------
|
|
if (typeof event.data.original_msg_id !== "undefined" && typeof appCallbackFunctions[event.data.original_msg_id] !== "undefined") {
|
|
// Execute callback
|
|
appCallbackFunctions[event.data.original_msg_id](event.data);
|
|
// Remove this callback function since it won't be needed again
|
|
delete appCallbackFunctions[event.data.original_msg_id];
|
|
|
|
// Done
|
|
return;
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Message from apps
|
|
// --------------------------------------------------------
|
|
|
|
// `data` and `msg` are required
|
|
if(!event.data || !event.data.msg){
|
|
return;
|
|
}
|
|
|
|
// `appInstanceID` is required
|
|
if(!event.data.appInstanceID){
|
|
console.log(`appInstanceID is needed`);
|
|
return;
|
|
}
|
|
|
|
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);
|
|
const msg_id = event.data.uuid;
|
|
const app_name = $(target_iframe).attr('data-app');
|
|
const app_uuid = $el_parent_window.attr('data-app_uuid');
|
|
|
|
// todo validate all event.data stuff coming from the client (e.g. event.data.message, .msg, ...)
|
|
//-------------------------------------------------
|
|
// READY
|
|
//-------------------------------------------------
|
|
if(event.data.msg === 'READY'){
|
|
$(target_iframe).attr('data-appUsesSDK', 'true');
|
|
|
|
// If we were waiting to launch this as a child app, report to the parent that it succeeded.
|
|
window.report_app_launched(event.data.appInstanceID, { uses_sdk: true });
|
|
|
|
// Send any saved broadcasts to the new app
|
|
globalThis.services.get('broadcast').sendSavedBroadcastsTo(event.data.appInstanceID);
|
|
}
|
|
//-------------------------------------------------
|
|
// windowFocused
|
|
//-------------------------------------------------
|
|
else if(event.data.msg === 'windowFocused'){
|
|
console.log('windowFocused');
|
|
}
|
|
//--------------------------------------------------------
|
|
// ALERT
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'ALERT' && event.data.message !== undefined){
|
|
const alert_resp = await UIAlert({
|
|
message: html_encode(event.data.message),
|
|
buttons: event.data.buttons,
|
|
type: event.data.options?.type,
|
|
window_options: {
|
|
parent_uuid: event.data.appInstanceID,
|
|
disable_parent_window: true,
|
|
}
|
|
})
|
|
|
|
target_iframe.contentWindow.postMessage({
|
|
original_msg_id: msg_id,
|
|
msg: 'alertResponded',
|
|
response: alert_resp,
|
|
}, '*');
|
|
}
|
|
//--------------------------------------------------------
|
|
// PROMPT
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'PROMPT' && event.data.message !== undefined){
|
|
const prompt_resp = await UIPrompt({
|
|
message: html_encode(event.data.message),
|
|
placeholder: html_encode(event.data.placeholder),
|
|
window_options: {
|
|
parent_uuid: event.data.appInstanceID,
|
|
disable_parent_window: true,
|
|
}
|
|
})
|
|
|
|
target_iframe.contentWindow.postMessage({
|
|
original_msg_id: msg_id,
|
|
msg: 'promptResponded',
|
|
response: prompt_resp,
|
|
}, '*');
|
|
}
|
|
//--------------------------------------------------------
|
|
// env
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'env'){
|
|
target_iframe.contentWindow.postMessage({
|
|
original_msg_id: msg_id,
|
|
}, '*');
|
|
}
|
|
//--------------------------------------------------------
|
|
// createWindow
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'createWindow'){
|
|
// todo: validate as many of these as possible
|
|
if(event.data.options){
|
|
UIWindow({
|
|
title: event.data.options.title,
|
|
disable_parent_window: event.data.options.disable_parent_window,
|
|
width: event.data.options.width,
|
|
height: event.data.options.height,
|
|
is_resizable: event.data.options.is_resizable,
|
|
has_head: event.data.options.has_head,
|
|
center: event.data.options.center,
|
|
show_in_taskbar: event.data.options.show_in_taskbar,
|
|
iframe_srcdoc: event.data.options.content,
|
|
iframe_url: event.data.options.url,
|
|
parent_uuid: event.data.appInstanceID,
|
|
})
|
|
}
|
|
}
|
|
//--------------------------------------------------------
|
|
// setItem
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'setItem' && event.data.key && event.data.value){
|
|
// todo: validate key and value to avoid unnecessary api calls
|
|
return await $.ajax({
|
|
url: api_origin + "/setItem",
|
|
type: 'POST',
|
|
data: JSON.stringify({
|
|
app: app_uuid,
|
|
key: event.data.key,
|
|
value: event.data.value,
|
|
}),
|
|
async: true,
|
|
contentType: "application/json",
|
|
headers: {
|
|
"Authorization": "Bearer "+auth_token
|
|
},
|
|
statusCode: {
|
|
401: function () {
|
|
logout();
|
|
},
|
|
},
|
|
success: function (fsentry){
|
|
}
|
|
})
|
|
}
|
|
//--------------------------------------------------------
|
|
// getItem
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'getItem' && event.data.key){
|
|
// todo: validate key to avoid unnecessary api calls
|
|
$.ajax({
|
|
url: api_origin + "/getItem",
|
|
type: 'POST',
|
|
data: JSON.stringify({
|
|
key: event.data.key,
|
|
app: app_uuid,
|
|
}),
|
|
async: true,
|
|
contentType: "application/json",
|
|
headers: {
|
|
"Authorization": "Bearer "+auth_token
|
|
},
|
|
statusCode: {
|
|
401: function () {
|
|
logout();
|
|
},
|
|
},
|
|
success: function (result){
|
|
// send confirmation to requester window
|
|
target_iframe.contentWindow.postMessage({
|
|
original_msg_id: msg_id,
|
|
msg: 'getItemSucceeded',
|
|
value: result ? result.value : null,
|
|
}, '*');
|
|
}
|
|
})
|
|
}
|
|
//--------------------------------------------------------
|
|
// removeItem
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'removeItem' && event.data.key){
|
|
// todo: validate key to avoid unnecessary api calls
|
|
$.ajax({
|
|
url: api_origin + "/removeItem",
|
|
type: 'POST',
|
|
data: JSON.stringify({
|
|
key: event.data.key,
|
|
app: app_uuid,
|
|
}),
|
|
async: true,
|
|
contentType: "application/json",
|
|
headers: {
|
|
"Authorization": "Bearer "+auth_token
|
|
},
|
|
statusCode: {
|
|
401: function () {
|
|
logout();
|
|
},
|
|
},
|
|
success: function (result){
|
|
// send confirmation to requester window
|
|
target_iframe.contentWindow.postMessage({
|
|
original_msg_id: msg_id,
|
|
}, '*');
|
|
}
|
|
})
|
|
}
|
|
//--------------------------------------------------------
|
|
// showOpenFilePicker
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'showOpenFilePicker'){
|
|
// Auth
|
|
if(!is_auth() && !(await UIWindowSignup({referrer: app_name})))
|
|
return;
|
|
|
|
// Disable parent window
|
|
$el_parent_window.addClass('window-disabled')
|
|
$el_parent_disable_mask.show();
|
|
$el_parent_disable_mask.css('z-index', parseInt($el_parent_window.css('z-index')) + 1);
|
|
$(target_iframe).blur();
|
|
|
|
// Allowed_file_types
|
|
let allowed_file_types = "";
|
|
if(event.data.options && event.data.options.accept)
|
|
allowed_file_types = event.data.options.accept;
|
|
|
|
// selectable_body
|
|
let is_selectable_body = false;
|
|
if(event.data.options && event.data.options.multiple && event.data.options.multiple === true)
|
|
is_selectable_body = true;
|
|
|
|
// Open dialog
|
|
UIWindow({
|
|
allowed_file_types: allowed_file_types,
|
|
path: '/' + window.user.username + '/Desktop',
|
|
// this is the uuid of the window to which this dialog will return
|
|
parent_uuid: event.data.appInstanceID,
|
|
show_maximize_button: false,
|
|
show_minimize_button: false,
|
|
title: 'Open',
|
|
is_dir: true,
|
|
is_openFileDialog: true,
|
|
selectable_body: is_selectable_body,
|
|
iframe_msg_uid: msg_id,
|
|
initiating_app_uuid: app_uuid,
|
|
center: true,
|
|
});
|
|
}
|
|
//--------------------------------------------------------
|
|
// showDirectoryPicker
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'showDirectoryPicker'){
|
|
// Auth
|
|
if(!is_auth() && !(await UIWindowSignup({referrer: app_name})))
|
|
return;
|
|
|
|
// Disable parent window
|
|
$el_parent_window.addClass('window-disabled')
|
|
$el_parent_disable_mask.show();
|
|
$el_parent_disable_mask.css('z-index', parseInt($el_parent_window.css('z-index')) + 1);
|
|
$(target_iframe).blur();
|
|
|
|
// allowed_file_types
|
|
let allowed_file_types = "";
|
|
if(event.data.options && event.data.options.accept)
|
|
allowed_file_types = event.data.options.accept;
|
|
|
|
// selectable_body
|
|
let is_selectable_body = false;
|
|
if(event.data.options && event.data.options.multiple && event.data.options.multiple === true)
|
|
is_selectable_body = true;
|
|
|
|
// open dialog
|
|
UIWindow({
|
|
path: '/' + window.user.username + '/Desktop',
|
|
// this is the uuid of the window to which this dialog will return
|
|
parent_uuid: event.data.appInstanceID,
|
|
show_maximize_button: false,
|
|
show_minimize_button: false,
|
|
title: 'Open',
|
|
is_dir: true,
|
|
is_directoryPicker: true,
|
|
selectable_body: is_selectable_body,
|
|
iframe_msg_uid: msg_id,
|
|
center: true,
|
|
initiating_app_uuid: app_uuid,
|
|
});
|
|
}
|
|
//--------------------------------------------------------
|
|
// setWindowTitle
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'setWindowTitle' && event.data.new_title !== undefined){
|
|
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
|
|
target_iframe.contentWindow.postMessage({
|
|
original_msg_id: msg_id,
|
|
}, '*');
|
|
}
|
|
//--------------------------------------------------------
|
|
// setWindowWidth
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'setWindowWidth' && event.data.width !== undefined){
|
|
event.data.width = parseFloat(event.data.width);
|
|
// must be at least 200
|
|
if(event.data.width < 200)
|
|
event.data.width = 200;
|
|
// set window width
|
|
$($el_parent_window).css('width', event.data.width);
|
|
// send confirmation to requester window
|
|
target_iframe.contentWindow.postMessage({
|
|
original_msg_id: msg_id,
|
|
}, '*');
|
|
}
|
|
//--------------------------------------------------------
|
|
// setWindowHeight
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'setWindowHeight' && event.data.height !== undefined){
|
|
event.data.height = parseFloat(event.data.height);
|
|
// must be at least 200
|
|
if(event.data.height < 200)
|
|
event.data.height = 200;
|
|
|
|
// convert to number and set
|
|
$($el_parent_window).css('height', event.data.height);
|
|
|
|
// send confirmation to requester window
|
|
target_iframe.contentWindow.postMessage({
|
|
original_msg_id: msg_id,
|
|
}, '*');
|
|
}
|
|
//--------------------------------------------------------
|
|
// setWindowSize
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'setWindowSize' && (event.data.width !== undefined || event.data.height !== undefined)){
|
|
// convert to number and set
|
|
if(event.data.width !== undefined){
|
|
event.data.width = parseFloat(event.data.width);
|
|
// must be at least 200
|
|
if(event.data.width < 200)
|
|
event.data.width = 200;
|
|
$($el_parent_window).css('width', event.data.width);
|
|
}
|
|
|
|
if(event.data.height !== undefined){
|
|
event.data.height = parseFloat(event.data.height);
|
|
// must be at least 200
|
|
if(event.data.height < 200)
|
|
event.data.height = 200;
|
|
$($el_parent_window).css('height', event.data.height);
|
|
}
|
|
|
|
// send confirmation to requester window
|
|
target_iframe.contentWindow.postMessage({
|
|
original_msg_id: msg_id,
|
|
}, '*');
|
|
}
|
|
//--------------------------------------------------------
|
|
// setWindowPosition
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'setWindowPosition' && (event.data.x !== undefined || event.data.y !== undefined)){
|
|
// convert to number and set
|
|
if(event.data.x !== undefined){
|
|
event.data.x = parseFloat(event.data.x);
|
|
// we don't want the window to go off the left edge of the screen
|
|
if(event.data.x < 0)
|
|
event.data.x = 0;
|
|
// we don't want the window to go off the right edge of the screen
|
|
if(event.data.x > window.innerWidth - 100)
|
|
event.data.x = window.innerWidth - 100;
|
|
// set window left
|
|
$($el_parent_window).css('left', parseFloat(event.data.x));
|
|
}
|
|
|
|
if(event.data.y !== undefined){
|
|
event.data.y = parseFloat(event.data.y);
|
|
// we don't want the window to go off the top edge of the screen
|
|
if(event.data.y < window.taskbar_height)
|
|
event.data.y = window.taskbar_height;
|
|
// we don't want the window to go off the bottom edge of the screen
|
|
if(event.data.y > window.innerHeight - 100)
|
|
event.data.y = window.innerHeight - 100;
|
|
// set window top
|
|
$($el_parent_window).css('top', parseFloat(event.data.y));
|
|
}
|
|
|
|
// send confirmation to requester window
|
|
target_iframe.contentWindow.postMessage({
|
|
original_msg_id: msg_id,
|
|
}, '*');
|
|
}
|
|
//--------------------------------------------------------
|
|
// watchItem
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'watchItem' && event.data.item_uid !== undefined){
|
|
if(!window.watchItems[event.data.item_uid])
|
|
window.watchItems[event.data.item_uid] = [];
|
|
|
|
window.watchItems[event.data.item_uid].push(event.data.appInstanceID);
|
|
}
|
|
//--------------------------------------------------------
|
|
// openItem
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'openItem'){
|
|
// check if readURL returns 200
|
|
$.ajax({
|
|
url: event.data.metadataURL + '&return_suggested_apps=true&return_path=true',
|
|
type: 'GET',
|
|
headers: {
|
|
"Authorization": "Bearer "+auth_token
|
|
},
|
|
success: async function(metadata){
|
|
$.ajax({
|
|
url: api_origin + "/open_item",
|
|
type: 'POST',
|
|
contentType: "application/json",
|
|
data: JSON.stringify({
|
|
uid: metadata.uid ?? undefined,
|
|
path: metadata.path ?? undefined,
|
|
}),
|
|
headers: {
|
|
"Authorization": "Bearer "+auth_token
|
|
},
|
|
statusCode: {
|
|
401: function () {
|
|
logout();
|
|
},
|
|
},
|
|
success: function(open_item_meta){
|
|
setTimeout(function(){
|
|
launch_app({
|
|
name: metadata.name,
|
|
file_path: metadata.path,
|
|
app_obj: open_item_meta.suggested_apps[0],
|
|
window_title: metadata.name,
|
|
file_uid: metadata.uid,
|
|
file_signature: open_item_meta.signature,
|
|
});
|
|
// todo: this is done because sometimes other windows such as openFileDialog
|
|
// bring focus to their apps and steal the focus from the newly-opened app
|
|
}, 800);
|
|
},
|
|
});
|
|
}
|
|
})
|
|
}
|
|
//--------------------------------------------------------
|
|
// launchApp
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'launchApp'){
|
|
// TODO: Determine if the app is allowed to launch child apps? We may want to limit this to prevent abuse.
|
|
// remember app for launch callback later
|
|
const child_instance_id = uuidv4();
|
|
window.child_launch_callbacks[child_instance_id] = {
|
|
parent_instance_id: event.data.appInstanceID,
|
|
launch_msg_id: msg_id,
|
|
};
|
|
// launch child app
|
|
launch_app({
|
|
name: event.data.app_name ?? app_name,
|
|
args: event.data.args ?? {},
|
|
parent_instance_id: event.data.appInstanceID,
|
|
uuid: child_instance_id,
|
|
});
|
|
}
|
|
//--------------------------------------------------------
|
|
// readAppDataFile
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'readAppDataFile' && event.data.path !== undefined){
|
|
// resolve path to absolute
|
|
event.data.path = path.resolve(event.data.path);
|
|
|
|
// join with appdata dir
|
|
const file_path = path.join(appdata_path, app_uuid, event.data.path);
|
|
|
|
puter.fs.sign(app_uuid, {
|
|
path: file_path,
|
|
action: 'write',
|
|
},
|
|
function(signature){
|
|
signature = signature.items;
|
|
signature.signatures = signature.signatures ?? [signature];
|
|
if(signature.signatures.length > 0 && signature.signatures[0].path){
|
|
signature.signatures[0].path = `~/` + signature.signatures[0].path.split('/').slice(2).join('/')
|
|
// send confirmation to requester window
|
|
target_iframe.contentWindow.postMessage({
|
|
msg: "readAppDataFileSucceeded",
|
|
original_msg_id: msg_id,
|
|
item: signature.signatures[0],
|
|
}, '*');
|
|
}else{
|
|
// send error to requester window
|
|
target_iframe.contentWindow.postMessage({
|
|
msg: "readAppDataFileFailed",
|
|
original_msg_id: msg_id,
|
|
}, '*');
|
|
}
|
|
}
|
|
)
|
|
}
|
|
//--------------------------------------------------------
|
|
// getAppData
|
|
//--------------------------------------------------------
|
|
// todo appdata should be provided from the /open_item api call
|
|
else if(event.data.msg === 'getAppData'){
|
|
if(appdata_signatures[app_uuid]){
|
|
target_iframe.contentWindow.postMessage({
|
|
msg: "getAppDataSucceeded",
|
|
original_msg_id: msg_id,
|
|
item: appdata_signatures[app_uuid],
|
|
}, '*');
|
|
}
|
|
// make app directory if it doesn't exist
|
|
puter.fs.mkdir({
|
|
path: path.join( appdata_path, app_uuid),
|
|
rename: false,
|
|
overwrite: false,
|
|
success: function(dir){
|
|
puter.fs.sign(app_uuid, {
|
|
uid: dir.uid,
|
|
action: 'write',
|
|
success: function(signature){
|
|
signature = signature.items;
|
|
appdata_signatures[app_uuid] = signature;
|
|
// send confirmation to requester window
|
|
target_iframe.contentWindow.postMessage({
|
|
msg: "getAppDataSucceeded",
|
|
original_msg_id: msg_id,
|
|
item: signature,
|
|
}, '*');
|
|
}
|
|
})
|
|
},
|
|
error: function(err){
|
|
if(err.existing_fsentry || err.code === 'path_exists'){
|
|
puter.fs.sign(app_uuid, {
|
|
uid: err.existing_fsentry.uid,
|
|
action: 'write',
|
|
success: function(signature){
|
|
signature = signature.items;
|
|
appdata_signatures[app_uuid] = signature;
|
|
// send confirmation to requester window
|
|
target_iframe.contentWindow.postMessage({
|
|
msg: "getAppDataSucceeded",
|
|
original_msg_id: msg_id,
|
|
item: signature,
|
|
}, '*');
|
|
}
|
|
})
|
|
}
|
|
}
|
|
});
|
|
}
|
|
//--------------------------------------------------------
|
|
// requestPermission
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'requestPermission'){
|
|
// auth
|
|
if(!is_auth() && !(await UIWindowSignup({referrer: app_name})))
|
|
return;
|
|
|
|
// options must be an object
|
|
if(event.data.options === undefined || typeof event.data.options !== 'object')
|
|
event.data.options = {};
|
|
|
|
// clear window_options for security reasons
|
|
event.data.options.window_options = {}
|
|
|
|
// Set app as parent window of font picker window
|
|
event.data.options.window_options.parent_uuid = event.data.appInstanceID;
|
|
|
|
// disable parent window
|
|
event.data.options.window_options.disable_parent_window = true;
|
|
|
|
let granted = await UIWindowRequestPermission({
|
|
origin: event.origin,
|
|
permission: event.data.options.permission,
|
|
window_options: event.data.options.window_options,
|
|
});
|
|
|
|
// send selected font to requester window
|
|
target_iframe.contentWindow.postMessage({
|
|
msg: "permissionGranted",
|
|
granted: granted,
|
|
original_msg_id: msg_id,
|
|
}, '*');
|
|
$(target_iframe).get(0).focus({preventScroll:true});
|
|
}
|
|
//--------------------------------------------------------
|
|
// showFontPicker
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'showFontPicker'){
|
|
// auth
|
|
if(!is_auth() && !(await UIWindowSignup({referrer: app_name})))
|
|
return;
|
|
|
|
// set options
|
|
event.data.options = event.data.options ?? {};
|
|
|
|
// clear window_options for security reasons
|
|
event.data.options.window_options = {}
|
|
|
|
// Set app as parent window of font picker window
|
|
event.data.options.window_options.parent_uuid = event.data.appInstanceID;
|
|
|
|
// Open font picker
|
|
let selected_font = await UIWindowFontPicker(event.data.options);
|
|
|
|
// send selected font to requester window
|
|
target_iframe.contentWindow.postMessage({
|
|
msg: "fontPicked",
|
|
original_msg_id: msg_id,
|
|
font: selected_font,
|
|
}, '*');
|
|
$(target_iframe).get(0).focus({preventScroll:true});
|
|
}
|
|
//--------------------------------------------------------
|
|
// showColorPicker
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'showColorPicker'){
|
|
// Auth
|
|
if(!is_auth() && !(await UIWindowSignup({referrer: app_name})))
|
|
return;
|
|
|
|
// set options
|
|
event.data.options = event.data.options ?? {};
|
|
|
|
// Clear window_options for security reasons
|
|
event.data.options.window_options = {}
|
|
|
|
// Set app as parent window of the font picker window
|
|
event.data.options.window_options.parent_uuid = event.data.appInstanceID;
|
|
|
|
// Open color picker
|
|
let selected_color = await UIWindowColorPicker(event.data.options);
|
|
|
|
// Send selected color to requester window
|
|
target_iframe.contentWindow.postMessage({
|
|
msg: "colorPicked",
|
|
original_msg_id: msg_id,
|
|
color: selected_color ? selected_color.color : undefined,
|
|
}, '*');
|
|
$(target_iframe).get(0).focus({preventScroll:true});
|
|
}
|
|
//--------------------------------------------------------
|
|
// setWallpaper
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'setWallpaper'){
|
|
// Auth
|
|
if(!is_auth() && !(await UIWindowSignup({referrer: app_name})))
|
|
return;
|
|
|
|
// No options?
|
|
if(!event.data.options)
|
|
event.data.options = {};
|
|
|
|
// /set-desktop-bg
|
|
try{
|
|
await $.ajax({
|
|
url: api_origin + "/set-desktop-bg",
|
|
type: 'POST',
|
|
data: JSON.stringify({
|
|
url: event.data.readURL,
|
|
fit: event.data.options.fit ?? 'cover',
|
|
color: event.data.options.color,
|
|
}),
|
|
async: true,
|
|
contentType: "application/json",
|
|
headers: {
|
|
"Authorization": "Bearer "+auth_token
|
|
},
|
|
statusCode: {
|
|
401: function () {
|
|
logout();
|
|
},
|
|
},
|
|
});
|
|
|
|
// Set wallpaper
|
|
window.set_desktop_background({
|
|
url: event.data.readURL,
|
|
fit: event.data.options.fit ?? 'cover',
|
|
color: event.data.options.color,
|
|
})
|
|
|
|
// Send success to app
|
|
target_iframe.contentWindow.postMessage({
|
|
msg: "wallpaperSet",
|
|
original_msg_id: msg_id,
|
|
}, '*');
|
|
$(target_iframe).get(0).focus({preventScroll:true});
|
|
}catch(err){
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------
|
|
// showSaveFilePicker
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'showSaveFilePicker'){
|
|
//auth
|
|
if(!is_auth() && !(await UIWindowSignup({referrer: app_name})))
|
|
return;
|
|
|
|
//disable parent window
|
|
$el_parent_window.addClass('window-disabled')
|
|
$el_parent_disable_mask.show();
|
|
$el_parent_disable_mask.css('z-index', parseInt($el_parent_window.css('z-index')) + 1);
|
|
$(target_iframe).blur();
|
|
|
|
await UIWindow({
|
|
path: '/' + window.user.username + '/Desktop',
|
|
// this is the uuid of the window to which this dialog will return
|
|
parent_uuid: event.data.appInstanceID,
|
|
show_maximize_button: false,
|
|
show_minimize_button: false,
|
|
title: 'Save As…',
|
|
is_dir: true,
|
|
is_saveFileDialog: true,
|
|
saveFileDialog_default_filename: event.data.suggestedName ?? '',
|
|
selectable_body: false,
|
|
iframe_msg_uid: msg_id,
|
|
center: true,
|
|
initiating_app_uuid: app_uuid,
|
|
onSaveFileDialogSave: async function(target_path, el_filedialog_window){
|
|
$(el_filedialog_window).find('.window-disable-mask, .busy-indicator').show();
|
|
let busy_init_ts = Date.now();
|
|
|
|
// -------------------------------------
|
|
// URL
|
|
// -------------------------------------
|
|
if(event.data.url){
|
|
// download progress tracker
|
|
let dl_op_id = operation_id++;
|
|
|
|
// upload progress tracker defaults
|
|
window.progress_tracker[dl_op_id] = [];
|
|
window.progress_tracker[dl_op_id][0] = {};
|
|
window.progress_tracker[dl_op_id][0].total = 0;
|
|
window.progress_tracker[dl_op_id][0].ajax_uploaded = 0;
|
|
window.progress_tracker[dl_op_id][0].cloud_uploaded = 0;
|
|
|
|
let item_with_same_name_already_exists = true;
|
|
while(item_with_same_name_already_exists){
|
|
await download({
|
|
url: event.data.url,
|
|
name: path.basename(target_path),
|
|
dest_path: path.dirname(target_path),
|
|
auth_token: auth_token,
|
|
api_origin: api_origin,
|
|
dedupe_name: false,
|
|
overwrite: false,
|
|
operation_id: dl_op_id,
|
|
item_upload_id: 0,
|
|
success: function(res){
|
|
},
|
|
error: function(err){
|
|
UIAlert(err && err.message ? err.message : "Download failed.");
|
|
}
|
|
});
|
|
item_with_same_name_already_exists = false;
|
|
}
|
|
}
|
|
// -------------------------------------
|
|
// File
|
|
// -------------------------------------
|
|
else{
|
|
let overwrite = false;
|
|
let file_to_upload = new File([event.data.content], path.basename(target_path));
|
|
let item_with_same_name_already_exists = true;
|
|
while(item_with_same_name_already_exists){
|
|
// overwrite?
|
|
if(overwrite)
|
|
item_with_same_name_already_exists = false;
|
|
// upload
|
|
try{
|
|
const res = await puter.fs.write(
|
|
target_path,
|
|
file_to_upload,
|
|
{
|
|
dedupeName: false,
|
|
overwrite: overwrite
|
|
}
|
|
);
|
|
|
|
let file_signature = await puter.fs.sign(app_uuid, {uid: res.uid, action: 'write'});
|
|
file_signature = file_signature.items;
|
|
|
|
item_with_same_name_already_exists = false;
|
|
target_iframe.contentWindow.postMessage({
|
|
msg: "fileSaved",
|
|
original_msg_id: msg_id,
|
|
filename: res.name,
|
|
saved_file: {
|
|
name: file_signature.fsentry_name,
|
|
readURL: file_signature.read_url,
|
|
writeURL: file_signature.write_url,
|
|
metadataURL: file_signature.metadata_url,
|
|
type: file_signature.type,
|
|
uid: file_signature.uid,
|
|
path: `~/` + res.path.split('/').slice(2).join('/'),
|
|
},
|
|
}, '*');
|
|
|
|
$(target_iframe).get(0).focus({preventScroll:true});
|
|
// Update matching items on open windows
|
|
// todo don't blanket-update, mostly files with thumbnails really need to be updated
|
|
// first remove overwritten items
|
|
$(`.item[data-uid="${res.uid}"]`).removeItems();
|
|
// now add new items
|
|
UIItem({
|
|
appendTo: $(`.item-container[data-path="${html_encode(path.dirname(target_path))}" i]`),
|
|
immutable: res.immutable,
|
|
associated_app_name: res.associated_app?.name,
|
|
path: target_path,
|
|
icon: await item_icon(res),
|
|
name: path.basename(target_path),
|
|
uid: res.uid,
|
|
size: res.size,
|
|
modified: res.modified,
|
|
type: res.type,
|
|
is_dir: false,
|
|
is_shared: res.is_shared,
|
|
suggested_apps: res.suggested_apps,
|
|
});
|
|
// sort each window
|
|
$(`.item-container[data-path="${html_encode(path.dirname(target_path))}" i]`).each(function(){
|
|
sort_items(this, $(this).attr('data-sort_by'), $(this).attr('data-sort_order'))
|
|
});
|
|
$(el_filedialog_window).close();
|
|
show_save_account_notice_if_needed();
|
|
}
|
|
catch(err){
|
|
// item with same name exists
|
|
if(err.code === 'item_with_same_name_exists'){
|
|
const alert_resp = await UIAlert({
|
|
message: `<strong>${html_encode(err.entry_name)}</strong> already exists.`,
|
|
buttons:[
|
|
{
|
|
label: i18n('replace'),
|
|
value: 'replace',
|
|
type: 'primary',
|
|
},
|
|
{
|
|
label: i18n('cancel'),
|
|
value: 'cancel',
|
|
},
|
|
],
|
|
parent_uuid: $(el_filedialog_window).attr('data-element_uuid'),
|
|
})
|
|
if(alert_resp === 'replace'){
|
|
overwrite = true;
|
|
}else if(alert_resp === 'cancel'){
|
|
// enable parent window
|
|
$(el_filedialog_window).find('.window-disable-mask, .busy-indicator').hide();
|
|
return;
|
|
}
|
|
}
|
|
else{
|
|
// show error
|
|
await UIAlert({
|
|
message: err.message ?? "Upload failed.",
|
|
parent_uuid: $(el_filedialog_window).attr('data-element_uuid'),
|
|
});
|
|
// enable parent window
|
|
$(el_filedialog_window).find('.window-disable-mask, .busy-indicator').hide();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// done
|
|
let busy_duration = (Date.now() - busy_init_ts);
|
|
if( busy_duration >= busy_indicator_hide_delay){
|
|
$(el_filedialog_window).close();
|
|
}else{
|
|
setTimeout(() => {
|
|
// close this dialog
|
|
$(el_filedialog_window).close();
|
|
}, Math.abs(busy_indicator_hide_delay - busy_duration));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
//--------------------------------------------------------
|
|
// saveToPictures/Desktop/Documents/Videos/Audio/AppData
|
|
//--------------------------------------------------------
|
|
else if((event.data.msg === 'saveToPictures' || event.data.msg === 'saveToDesktop' || event.data.msg === 'saveToAppData' ||
|
|
event.data.msg === 'saveToDocuments' || event.data.msg === 'saveToVideos' || event.data.msg === 'saveToAudio')){
|
|
let target_path;
|
|
let create_missing_ancestors = false;
|
|
|
|
if(event.data.msg === 'saveToPictures')
|
|
target_path = path.join(pictures_path, event.data.filename);
|
|
else if(event.data.msg === 'saveToDesktop')
|
|
target_path = path.join(desktop_path, event.data.filename);
|
|
else if(event.data.msg === 'saveToDocuments')
|
|
target_path = path.join(documents_path, event.data.filename);
|
|
else if(event.data.msg === 'saveToVideos')
|
|
target_path = path.join(videos_path, event.data.filename);
|
|
else if(event.data.msg === 'saveToAudio')
|
|
target_path = path.join(audio_path, event.data.filename);
|
|
else if(event.data.msg === 'saveToAppData'){
|
|
target_path = path.join(appdata_path, app_uuid, event.data.filename);
|
|
create_missing_ancestors = true;
|
|
}
|
|
//auth
|
|
if(!is_auth() && !(await UIWindowSignup({referrer: app_name})))
|
|
return;
|
|
|
|
let item_with_same_name_already_exists = true;
|
|
let overwrite = false;
|
|
|
|
// -------------------------------------
|
|
// URL
|
|
// -------------------------------------
|
|
if(event.data.url){
|
|
let overwrite = false;
|
|
// download progress tracker
|
|
let dl_op_id = operation_id++;
|
|
|
|
// upload progress tracker defaults
|
|
window.progress_tracker[dl_op_id] = [];
|
|
window.progress_tracker[dl_op_id][0] = {};
|
|
window.progress_tracker[dl_op_id][0].total = 0;
|
|
window.progress_tracker[dl_op_id][0].ajax_uploaded = 0;
|
|
window.progress_tracker[dl_op_id][0].cloud_uploaded = 0;
|
|
|
|
let item_with_same_name_already_exists = true;
|
|
while(item_with_same_name_already_exists){
|
|
const res = await download({
|
|
url: event.data.url,
|
|
name: path.basename(target_path),
|
|
dest_path: path.dirname(target_path),
|
|
auth_token: auth_token,
|
|
api_origin: api_origin,
|
|
dedupe_name: true,
|
|
overwrite: false,
|
|
operation_id: dl_op_id,
|
|
item_upload_id: 0,
|
|
success: function(res){
|
|
},
|
|
error: function(err){
|
|
UIAlert(err && err.message ? err.message : "Download failed.");
|
|
}
|
|
});
|
|
item_with_same_name_already_exists = false;
|
|
}
|
|
}
|
|
// -------------------------------------
|
|
// File
|
|
// -------------------------------------
|
|
else{
|
|
let file_to_upload = new File([event.data.content], path.basename(target_path));
|
|
|
|
while(item_with_same_name_already_exists){
|
|
if(overwrite)
|
|
item_with_same_name_already_exists = false;
|
|
try{
|
|
const res = await puter.fs.write(target_path, file_to_upload, {
|
|
dedupeName: true,
|
|
overwrite: false,
|
|
createMissingAncestors: create_missing_ancestors,
|
|
});
|
|
item_with_same_name_already_exists = false;
|
|
let file_signature = await puter.fs.sign(app_uuid, {uid: res.uid, action: 'write'});
|
|
file_signature = file_signature.items;
|
|
|
|
target_iframe.contentWindow.postMessage({
|
|
msg: "fileSaved",
|
|
original_msg_id: msg_id,
|
|
filename: res.name,
|
|
saved_file: {
|
|
name: file_signature.fsentry_name,
|
|
readURL: file_signature.read_url,
|
|
writeURL: file_signature.write_url,
|
|
metadataURL: file_signature.metadata_url,
|
|
uid: file_signature.uid,
|
|
path: `~/` + res.path.split('/').slice(2).join('/'),
|
|
},
|
|
}, '*');
|
|
$(target_iframe).get(0).focus({preventScroll:true});
|
|
}
|
|
catch(err){
|
|
if(err.code === 'item_with_same_name_exists'){
|
|
const alert_resp = await UIAlert({
|
|
message: `<strong>${html_encode(err.entry_name)}</strong> already exists.`,
|
|
buttons:[
|
|
{
|
|
label: i18n('replace'),
|
|
type: 'primary',
|
|
},
|
|
{
|
|
label: i18n('cancel'),
|
|
value: 'cancel'
|
|
},
|
|
],
|
|
parent_uuid: event.data.appInstanceID,
|
|
})
|
|
if(alert_resp === 'Replace'){
|
|
overwrite = true;
|
|
}else if(alert_resp === 'cancel'){
|
|
item_with_same_name_already_exists = false;
|
|
}
|
|
}else{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//--------------------------------------------------------
|
|
// messageToApp
|
|
//--------------------------------------------------------
|
|
else if (event.data.msg === 'messageToApp') {
|
|
const { appInstanceID, targetAppInstanceID, targetAppOrigin, contents } = event.data;
|
|
// TODO: Determine if we should allow the message
|
|
// TODO: Track message traffic between apps
|
|
|
|
// pass on the message
|
|
const target_iframe = iframe_for_app_instance(targetAppInstanceID);
|
|
if (!target_iframe) {
|
|
console.error('Failed to send message to non-existent app', event);
|
|
return;
|
|
}
|
|
target_iframe.contentWindow.postMessage({
|
|
msg: 'messageToApp',
|
|
appInstanceID,
|
|
targetAppInstanceID,
|
|
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 = await (async () => {
|
|
// Parents can close their children
|
|
if (target_window.dataset['parent_instance_id'] === appInstanceID) {
|
|
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
|
|
//--------------------------------------------------------
|
|
else if(event.data.msg === 'exit'){
|
|
$(window_for_app_instance(event.data.appInstanceID)).close({bypass_iframe_messaging: true});
|
|
}
|
|
}); |