Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/backend/src/modules/puterai/AIChatService.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ class AIChatService extends BaseService {
model,
error: e,
});
while ( !! error ) {
while ( error ) {
const fallback = this.get_fallback_model({
model, tried,
});
Expand Down
16 changes: 8 additions & 8 deletions src/backend/src/modules/puterai/PuterAIModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,45 +27,45 @@ class PuterAIModule extends AdvancedBase {
// TODO: services should govern their own availability instead of
// the module deciding what to register

if ( !! config?.services?.['aws-textract']?.aws ) {
if ( config?.services?.['aws-textract']?.aws ) {
const { AWSTextractService } = require('./AWSTextractService');
services.registerService('aws-textract', AWSTextractService);
}

if ( !! config?.services?.['aws-polly']?.aws ) {
if ( config?.services?.['aws-polly']?.aws ) {
const { AWSPollyService } = require('./AWSPollyService');
services.registerService('aws-polly', AWSPollyService);
}

if ( !! config?.openai ) {
if ( config?.openai ) {
const { OpenAICompletionService } = require('./OpenAICompletionService');
services.registerService('openai-completion', OpenAICompletionService);

const { OpenAIImageGenerationService } = require('./OpenAIImageGenerationService');
services.registerService('openai-image-generation', OpenAIImageGenerationService);
}

if ( !! config?.services?.claude ) {
if ( config?.services?.claude ) {
const { ClaudeService } = require('./ClaudeService');
services.registerService('claude', ClaudeService);
}

if ( !! config?.services?.['together-ai'] ) {
if ( config?.services?.['together-ai'] ) {
const { TogetherAIService } = require('./TogetherAIService');
services.registerService('together-ai', TogetherAIService);
}

if ( !! config?.services?.['mistral'] ) {
if ( config?.services?.['mistral'] ) {
const { MistralAIService } = require('./MistralAIService');
services.registerService('mistral', MistralAIService);
}

if ( !! config?.services?.['groq'] ) {
if ( config?.services?.['groq'] ) {
const { GroqAIService } = require('./GroqAIService');
services.registerService('groq', GroqAIService);
}

if ( !! config?.services?.['xai'] ) {
if ( config?.services?.['xai'] ) {
const { XAIService } = require('./XAIService');
services.registerService('xai', XAIService);

Expand Down
2 changes: 1 addition & 1 deletion src/backend/src/modules/web/WebServerService.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ class WebServerService extends BaseService {
*
* @returns {Promise} A promise that resolves when the server is started.
*/
// eslint-disable-next-line no-unused-vars
WebServerService.prototype.__on_start_webserver = async function () {
// ...
};
Expand Down
5 changes: 4 additions & 1 deletion src/gui/src/IPC.js
Original file line number Diff line number Diff line change
Expand Up @@ -1261,7 +1261,10 @@ const ipc_listener = async (event, handled) => {
success: function(res){
},
error: function(err){
UIAlert(err && err.message ? err.message : "Download failed.");
UIAlert({
type: 'error',
message: err && err.message ? err.message : "Download failed."
});
}
});
item_with_same_name_already_exists = false;
Expand Down
181 changes: 159 additions & 22 deletions src/gui/src/UI/UIAlert.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,57 +19,156 @@

import UIWindow from './UIWindow.js'

function UIAlert(options){
// Alert type to icon mapping
const ALERT_TYPE_ICONS = {
'info': 'bell.svg',
'success': 'c-check.svg',
'warning': 'warning-sign.svg',
'error': 'danger.svg',
'question': 'reminder.svg'
};

/**
* Validates and retrieves an icon with fallback logic
* @param {string} customIcon - The custom icon name/path to validate
* @param {string} alertType - The alert type for fallback icon
* @returns {string} The validated icon path or fallback icon
*/
function validateAndGetIcon(customIcon, alertType) {
// First, try to get the custom icon
if (customIcon && window.icons && window.icons[customIcon]) {
return window.icons[customIcon];
}

// If custom icon is a direct path/URL, validate it exists
if (customIcon && (customIcon.startsWith('/') || customIcon.startsWith('http') || customIcon.includes('.'))) {
// For direct paths, we'll trust they exist and return them
// The browser will handle broken images gracefully
return customIcon;
}

// Fallback to type-based icon
const typeIcon = ALERT_TYPE_ICONS[alertType] || ALERT_TYPE_ICONS['warning'];
return window.icons[typeIcon] || window.icons[ALERT_TYPE_ICONS['warning']];
}

/**
* Sanitizes HTML content to prevent XSS attacks while allowing safe HTML elements
* @param {string|HTMLElement} content - The content to sanitize
* @returns {string} The sanitized HTML content
*/
function sanitizeCustomUI(content) {
if (!content) return '';

// If content is an HTMLElement, get its outerHTML
if (content instanceof HTMLElement) {
content = content.outerHTML;
}

// Convert to string if not already
content = String(content);

// Create a temporary div to parse and sanitize the HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = content;

// Define allowed tags and attributes for basic HTML sanitization
const allowedTags = ['div', 'span', 'p', 'br', 'strong', 'b', 'em', 'i', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li', 'a', 'img', 'button', 'input', 'textarea', 'select', 'option', 'label', 'form'];
const allowedAttributes = ['class', 'id', 'style', 'href', 'src', 'alt', 'title', 'type', 'value', 'name', 'placeholder', 'data-*'];

// Remove script tags and event handlers
const scripts = tempDiv.querySelectorAll('script');
scripts.forEach(script => script.remove());

// Remove event handler attributes (onclick, onload, etc.)
const allElements = tempDiv.querySelectorAll('*');
allElements.forEach(element => {
// Remove event handler attributes
Array.from(element.attributes).forEach(attr => {
if (attr.name.startsWith('on')) {
element.removeAttribute(attr.name);
}
});

// Remove javascript: URLs
if (element.href && element.href.startsWith('javascript:')) {
element.removeAttribute('href');
}
if (element.src && element.src.startsWith('javascript:')) {
element.removeAttribute('src');
}
});

return tempDiv.innerHTML;
}

function UIAlert(options) {
// set sensible defaults
if(arguments.length > 0){
if (arguments.length > 0) {
// if first argument is a string, then assume it is the message
if(window.isString(arguments[0])){
if (window.isString(arguments[0])) {
options = {};
options.message = arguments[0];
}
// if second argument is an array, then assume it is the buttons
if(arguments[1] && Array.isArray(arguments[1])){
if (arguments[1] && Array.isArray(arguments[1])) {
options.buttons = arguments[1];
}
}

// ensure options is an object
options = options || {};

return new Promise(async (resolve) => {
// provide an 'OK' button if no buttons are provided
if(!options.buttons || options.buttons.length === 0){
if (!options.buttons || options.buttons.length === 0) {
options.buttons = [
{label: i18n('ok'), value: true, type: 'primary'}
{ label: i18n('ok'), value: true, type: 'primary' }
]
}

// set body icon
options.body_icon = options.body_icon ?? window.icons['warning-sign.svg'];
if(options.type === 'success')
options.body_icon = window.icons['c-check.svg'];
// set default type if not provided (for backward compatibility)
if (!options.type) {
options.type = 'warning';
}

let santized_message = html_encode(options.message);
// validate alert type and fallback to warning if invalid
if (!ALERT_TYPE_ICONS[options.type]) {
options.type = 'warning';
}

// set body icon based on type or custom icon override
if (options.icon) {
// custom icon override with validation and fallback
options.body_icon = validateAndGetIcon(options.icon, options.type);
} else {
// use type-based icon
options.body_icon = window.icons[ALERT_TYPE_ICONS[options.type]];
}

let h = '';

let santized_message = html_encode(options.message);
// replace sanitized <strong> with <strong>
santized_message = santized_message.replace(/&lt;strong&gt;/g, '<strong>');
santized_message = santized_message.replace(/&lt;\/strong&gt;/g, '</strong>');

// replace sanitized <p> with <p>
santized_message = santized_message.replace(/&lt;p&gt;/g, '<p>');
santized_message = santized_message.replace(/&lt;\/p&gt;/g, '</p>');

let h = '';
// icon
h += `<img class="window-alert-icon" src="${html_encode(options.body_icon)}">`;
// message
h += `<div class="window-alert-message">${santized_message}</div>`;

// buttons
if(options.buttons && options.buttons.length > 0){
h += `<div style="overflow:hidden; margin-top:20px;">`;
for(let y=0; y<options.buttons.length; y++){
h += `<button class="button button-block button-${html_encode(options.buttons[y].type)} alert-resp-button"
data-label="${html_encode(options.buttons[y].label)}"
data-value="${html_encode(options.buttons[y].value ?? options.buttons[y].label)}"
${options.buttons[y].type === 'primary' ? 'autofocus' : ''}
>${html_encode(options.buttons[y].label)}</button>`;
h += `<button class="button button-block button-${html_encode(options.buttons[y].type)} alert-resp-button" data-label="${html_encode(options.buttons[y].label)}"
data-value="${html_encode(options.buttons[y].value ?? options.buttons[y].label)}"
${options.buttons[y].type === 'primary' ? 'autofocus' : ''}>${html_encode(options.buttons[y].label)}</button>`;
}
h += `</div>`;
}
Expand Down Expand Up @@ -106,19 +205,57 @@ function UIAlert(options){
'backdrop-filter': 'blur(3px)',
}
});

// focus to primary btn
$(el_window).find('.button-primary').focus();

// --------------------------------------------------------
// Button pressed
// --------------------------------------------------------
$(el_window).find('.alert-resp-button').on('click', async function(event){
event.preventDefault();
$(el_window).find('.alert-resp-button').on('click', async function (event) {
event.preventDefault();
event.stopPropagation();
resolve($(this).attr('data-value'));

const buttonIndex = parseInt($(this).attr('data-button-index'));
const buttonConfig = options.buttons[buttonIndex];
const buttonValue = $(this).attr('data-value');

// Execute callback function if provided
if (buttonConfig && typeof buttonConfig.callback === 'function') {
try {
// Execute the callback function
const callbackResult = await buttonConfig.callback();

// If callback returns a value, use it; otherwise use the button value
const resolveValue = callbackResult !== undefined ? callbackResult : buttonValue;
resolve(resolveValue);
} catch (error) {
console.error('Error executing button callback:', error);
// On callback error, still resolve with button value for backward compatibility
resolve(buttonValue);
}
} else {
// No callback - use existing behavior for backward compatibility
resolve(buttonValue);
}

$(el_window).close();
return false;
})
});

// --------------------------------------------------------
// Custom UI support - provide methods to close dialog programmatically
// --------------------------------------------------------
if (options.customUI) {
// Add a close method to the window element for custom UI to use
el_window.closeAlert = function (value) {
resolve(value);
$(el_window).close();
};

// Also make it available globally for custom UI scripts
window.currentAlertWindow = el_window;
}
})
}

Expand Down
Loading