Ext.ns('Proxmox'); Ext.ns('Proxmox.Setup'); if (!Ext.isDefined(Proxmox.Setup.auth_cookie_name)) { throw 'Proxmox library not initialized'; } // avoid errors when running without development tools if (!Ext.isDefined(Ext.global.console)) { let console = { dir: function () { // do nothing }, log: function () { // do nothing }, warn: function () { // do nothing }, }; Ext.global.console = console; } Ext.Ajax.defaultHeaders = { Accept: 'application/json', }; Ext.Ajax.on('beforerequest', function (conn, options) { if (Proxmox.CSRFPreventionToken) { if (!options.headers) { options.headers = {}; } options.headers.CSRFPreventionToken = Proxmox.CSRFPreventionToken; } let storedAuth = Proxmox.Utils.getStoredAuth(); if (storedAuth.token) { options.headers.Authorization = storedAuth.token; } }); Ext.define('Proxmox.Utils', { // a singleton utilities: { yesText: gettext('Yes'), noText: gettext('No'), enabledText: gettext('Enabled'), disabledText: gettext('Disabled'), noneText: gettext('none'), NoneText: gettext('None'), errorText: gettext('Error'), warningsText: ngettext('Warning', 'Warnings', 1), // TODO: make use of count unknownText: gettext('Unknown'), defaultText: gettext('Default'), daysText: gettext('days'), dayText: gettext('day'), runningText: gettext('running'), stoppedText: gettext('stopped'), neverText: gettext('never'), totalText: gettext('Total'), usedText: gettext('Used'), directoryText: gettext('Directory'), stateText: gettext('State'), language_map: { //language map is sorted alphabetically by iso 639-1 ar: `العربية - ${gettext('Arabic')}`, bg: `Български - ${gettext('Bulgarian')}`, bn: `বাংলা - ${gettext('Bengali')}`, ca: `Català - ${gettext('Catalan')}`, cs: `Czech - ${gettext('Czech')}`, da: `Dansk - ${gettext('Danish')}`, de: `Deutsch - ${gettext('German')}`, el_GR: `ελληνικά - ${gettext('Greek')}`, en: `English - ${gettext('English')}`, es: `Español - ${gettext('Spanish')}`, eu: `Euskera (Basque) - ${gettext('Euskera (Basque)')}`, fa: `فارسی - ${gettext('Persian (Farsi)')}`, fr: `Français - ${gettext('French')}`, ga: `Gaeilge - ${gettext('Irish')}`, hr: `Hrvatski - ${gettext('Croatian')}`, he: `עברית - ${gettext('Hebrew')}`, it: `Italiano - ${gettext('Italian')}`, ja: `日本語 - ${gettext('Japanese')}`, ka: `ქართული - ${gettext('Georgian')}`, ko: `한국어 - ${gettext('Korean')}`, lo: `ພາສາລາວ - ${gettext('Lao')}`, mn: `Монгол - ${gettext('Mongolian')}`, nb: `Bokmål - ${gettext('Norwegian (Bokmal)')}`, nl: `Nederlands - ${gettext('Dutch')}`, nn: `Nynorsk - ${gettext('Norwegian (Nynorsk)')}`, pl: `Polski - ${gettext('Polish')}`, pt_BR: `Português Brasileiro - ${gettext('Portuguese (Brazil)')}`, ru: `Русский - ${gettext('Russian')}`, sl: `Slovenščina - ${gettext('Slovenian')}`, sv: `Svenska - ${gettext('Swedish')}`, tr: `Türkçe - ${gettext('Turkish')}`, ukr: `Українська - ${gettext('Ukrainian')}`, zh_CN: `中文(简体)- ${gettext('Chinese (Simplified)')}`, zh_TW: `中文(繁體)- ${gettext('Chinese (Traditional)')}`, }, render_language: function (value) { if (!value || value === '__default__') { return Proxmox.Utils.defaultText + ' (English)'; } if (value === 'kr') { value = 'ko'; // fix-up wrongly used Korean code. FIXME: remove with trixie releases } let text = Proxmox.Utils.language_map[value]; if (text) { return text + ' (' + value + ')'; } return value; }, renderEnabledIcon: (enabled) => ``, language_array: function () { let data = [['__default__', Proxmox.Utils.render_language('')]]; Ext.Object.each(Proxmox.Utils.language_map, function (key, value) { data.push([key, Proxmox.Utils.render_language(value)]); }); return data; }, theme_map: { crisp: 'Light theme', 'proxmox-dark': 'Proxmox Dark', }, render_theme: function (value) { if (!value || value === '__default__') { return Proxmox.Utils.defaultText + ' (auto)'; } let text = Proxmox.Utils.theme_map[value]; if (text) { return text; } return value; }, theme_array: function () { let data = [['__default__', Proxmox.Utils.render_theme('')]]; Ext.Object.each(Proxmox.Utils.theme_map, function (key, value) { data.push([key, Proxmox.Utils.render_theme(value)]); }); return data; }, bond_mode_gettext_map: { '802.3ad': 'LACP (802.3ad)', 'lacp-balance-slb': 'LACP (balance-slb)', 'lacp-balance-tcp': 'LACP (balance-tcp)', }, render_bond_mode: (value) => Proxmox.Utils.bond_mode_gettext_map[value] || value || '', bond_mode_array: function (modes) { return modes.map((mode) => [mode, Proxmox.Utils.render_bond_mode(mode)]); }, getNoSubKeyHtml: function (url) { let html_url = Ext.String.format( 'www.proxmox.com', url || 'https://www.proxmox.com', ); return Ext.String.format( gettext( 'You do not have a valid subscription for this server. Please visit {0} to get a list of available options.', ), html_url, ); }, format_boolean_with_default: function (value) { if (Ext.isDefined(value) && value !== '__default__') { return value ? Proxmox.Utils.yesText : Proxmox.Utils.noText; } return Proxmox.Utils.defaultText; }, format_boolean: function (value) { return value ? Proxmox.Utils.yesText : Proxmox.Utils.noText; }, format_neg_boolean: function (value) { return !value ? Proxmox.Utils.yesText : Proxmox.Utils.noText; }, format_enabled_toggle: function (value) { return value ? Proxmox.Utils.enabledText : Proxmox.Utils.disabledText; }, format_expire: function (date) { if (!date) { return Proxmox.Utils.neverText; } return Ext.Date.format(date, 'Y-m-d'); }, // somewhat like a human would tell durations, omit zero values and do not // give seconds precision if we talk days already format_duration_human: function (ut) { let seconds = 0, minutes = 0, hours = 0, days = 0, years = 0; if (ut <= 0.1) { return '<0.1s'; } let remaining = ut; seconds = Number((remaining % 60).toFixed(1)); remaining = Math.trunc(remaining / 60); if (remaining > 0) { minutes = remaining % 60; remaining = Math.trunc(remaining / 60); if (remaining > 0) { hours = remaining % 24; remaining = Math.trunc(remaining / 24); if (remaining > 0) { days = remaining % 365; remaining = Math.trunc(remaining / 365); // yea, just lets ignore leap years... if (remaining > 0) { years = remaining; } } } } let res = []; let add = (t, unit) => { if (t > 0) { res.push(t + unit); } return t > 0; }; let addMinutes = !add(years, 'y'); let addSeconds = !add(days, 'd'); add(hours, 'h'); if (addMinutes) { add(minutes, 'm'); if (addSeconds) { add(seconds, 's'); } } return res.join(' '); }, format_duration_long: function (ut) { let days = Math.floor(ut / 86400); ut -= days * 86400; let hours = Math.floor(ut / 3600); ut -= hours * 3600; let mins = Math.floor(ut / 60); ut -= mins * 60; let hours_str = '00' + hours.toString(); hours_str = hours_str.substr(hours_str.length - 2); let mins_str = '00' + mins.toString(); mins_str = mins_str.substr(mins_str.length - 2); let ut_str = '00' + ut.toString(); ut_str = ut_str.substr(ut_str.length - 2); if (days) { let ds = days > 1 ? Proxmox.Utils.daysText : Proxmox.Utils.dayText; return days.toString() + ' ' + ds + ' ' + hours_str + ':' + mins_str + ':' + ut_str; } else { return hours_str + ':' + mins_str + ':' + ut_str; } }, format_subscription_level: function (level) { if (level === 'c') { return 'Community'; } else if (level === 'b') { return 'Basic'; } else if (level === 's') { return 'Standard'; } else if (level === 'p') { return 'Premium'; } else { return Proxmox.Utils.noneText; } }, compute_min_label_width: function (text, width) { if (width === undefined) { width = 100; } let tm = new Ext.util.TextMetrics(); let min = tm.getWidth(text + ':'); return min < width ? width : min; }, // returns username + realm parse_userid: function (userid) { if (!Ext.isString(userid)) { return [undefined, undefined]; } let match = userid.match(/^(.+)@([^@]+)$/); if (match !== null) { return [match[1], match[2]]; } return [undefined, undefined]; }, render_username: function (userid) { let username = Proxmox.Utils.parse_userid(userid)[0] || ''; return Ext.htmlEncode(username); }, render_realm: function (userid) { let username = Proxmox.Utils.parse_userid(userid)[1] || ''; return Ext.htmlEncode(username); }, getStoredAuth: function () { let storedAuth = JSON.parse(window.localStorage.getItem('ProxmoxUser')); return storedAuth || {}; }, setAuthData: function (data) { Proxmox.UserName = data.username; Proxmox.LoggedOut = data.LoggedOut; // creates a session cookie (expire = null) // that way the cookie gets deleted after the browser window is closed if (data.ticket) { Proxmox.CSRFPreventionToken = data.CSRFPreventionToken; Ext.util.Cookies.set( Proxmox.Setup.auth_cookie_name, data.ticket, null, '/', null, true, 'lax', ); } if (data.token) { window.localStorage.setItem('ProxmoxUser', JSON.stringify(data)); } }, authOK: function () { if (Proxmox.LoggedOut) { return undefined; } let storedAuth = Proxmox.Utils.getStoredAuth(); let cookie = Ext.util.Cookies.get(Proxmox.Setup.auth_cookie_name); if ( (Proxmox.UserName !== '' && cookie && !cookie.startsWith('PVE:tfa!')) || storedAuth.token ) { return cookie || storedAuth.token; } else { return false; } }, authClear: function () { if (Proxmox.LoggedOut) { return; } // ExtJS clear is basically the same, but browser may complain if any cookie isn't "secure" Ext.util.Cookies.set( Proxmox.Setup.auth_cookie_name, '', new Date(0), null, null, true, 'lax', ); window.localStorage.removeItem('ProxmoxUser'); }, // The End-User gets redirected back here after login on the OpenID auth. portal, and in the // redirection URL the state and auth.code are passed as URL GET params, this helper parses those getOpenIDRedirectionAuthorization: function () { const auth = Ext.Object.fromQueryString(window.location.search); if (auth.state !== undefined && auth.code !== undefined) { return auth; } return undefined; }, // comp.setLoading() is buggy in ExtJS 4.0.7, so we // use el.mask() instead setErrorMask: function (comp, msg) { let el = comp.el; if (!el) { return; } if (!msg) { el.unmask(); } else if (msg === true) { el.mask(gettext('Loading...')); } else { el.mask(msg); } }, getResponseErrorMessage: (err) => { if (!err.statusText) { return gettext('Connection error'); } let msg = [Ext.String.htmlEncode(`${err.statusText} (${err.status})`)]; if (err.response && err.response.responseText) { let txt = err.response.responseText; try { let res = JSON.parse(txt); if (typeof res.message === 'string') { // HTTP reason can only be the first line, get remaining lines from the body let lines = res.message.split('\n').filter((line) => line.length > 0); if (lines[0] === err.statusText) { lines.shift(); // already shown through the reason phrase } for (const line of lines) { msg.push(Ext.String.htmlEncode(line)); } } if (res.errors && typeof res.errors === 'object') { for (let [key, value] of Object.entries(res.errors)) { msg.push(Ext.String.htmlEncode(`${key}: ${value}`)); } } } catch (_e) { // fallback to string msg.push(Ext.String.htmlEncode(txt)); } } return msg.join('
'); }, monStoreErrors: function (component, store, clearMaskBeforeLoad, errorCallback) { if (clearMaskBeforeLoad) { component.mon(store, 'beforeload', function (s, operation, eOpts) { Proxmox.Utils.setErrorMask(component, false); }); } else { component.mon(store, 'beforeload', function (s, operation, eOpts) { if (!component.loadCount) { component.loadCount = 0; // make sure it is nucomponent.ic Proxmox.Utils.setErrorMask(component, true); } }); } // only works with 'proxmox' proxy component.mon(store.proxy, 'afterload', function (proxy, request, success) { component.loadCount++; if (success) { Proxmox.Utils.setErrorMask(component, false); return; } let error = request._operation.getError(); let msg = Proxmox.Utils.getResponseErrorMessage(error); if (!errorCallback || !errorCallback(error, msg)) { Proxmox.Utils.setErrorMask(component, msg); } }); }, extractRequestError: function (result, verbose) { let msg = gettext('Successful'); if (!result.success) { msg = gettext('Unknown error'); if (result.message) { msg = Ext.htmlEncode(result.message); if (result.status) { msg += ` (${result.status})`; } } if (verbose && Ext.isObject(result.errors)) { msg += '
'; Ext.Object.each(result.errors, (prop, desc) => { msg += `
${Ext.htmlEncode(prop)}: ${Ext.htmlEncode(desc)}`; }); } } return msg; }, // Ext.Ajax.request API2Request: function (reqOpts) { let newopts = Ext.apply( { waitMsg: gettext('Please wait...'), }, reqOpts, ); // default to enable if user isn't handling the failure already explicitly let autoErrorAlert = reqOpts.autoErrorAlert ?? (typeof reqOpts.failure !== 'function' && typeof reqOpts.callback !== 'function'); if (!newopts.url.match(/^\/api2/)) { newopts.url = '/api2/extjs' + newopts.url; } delete newopts.callback; let unmask = (target) => { if (target.waitMsgTargetCount === undefined || --target.waitMsgTargetCount <= 0) { target.setLoading(false); delete target.waitMsgTargetCount; } }; let createWrapper = function (successFn, callbackFn, failureFn) { Ext.apply(newopts, { success: function (response, options) { if (options.waitMsgTarget) { if (Proxmox.Utils.toolkit === 'touch') { options.waitMsgTarget.setMasked(false); } else { unmask(options.waitMsgTarget); } } let result = Ext.decode(response.responseText); response.result = result; if (!result.success) { response.htmlStatus = Proxmox.Utils.extractRequestError(result, true); Ext.callback(callbackFn, options.scope, [options, false, response]); Ext.callback(failureFn, options.scope, [response, options]); if (autoErrorAlert) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); } return; } Ext.callback(callbackFn, options.scope, [options, true, response]); Ext.callback(successFn, options.scope, [response, options]); }, failure: function (response, options) { if (options.waitMsgTarget) { if (Proxmox.Utils.toolkit === 'touch') { options.waitMsgTarget.setMasked(false); } else { unmask(options.waitMsgTarget); } } response.result = {}; try { response.result = Ext.decode(response.responseText); } catch (_e) { // ignore } let msg = gettext('Connection error') + ' - server offline?'; if (response.aborted) { msg = gettext('Connection error') + ' - aborted.'; } else if (response.timedout) { msg = gettext('Connection error') + ' - Timeout.'; } else if (response.status && response.statusText) { msg = gettext('Connection error') + ' ' + response.status + ': ' + response.statusText; } response.htmlStatus = Ext.htmlEncode(msg); Ext.callback(callbackFn, options.scope, [options, false, response]); Ext.callback(failureFn, options.scope, [response, options]); }, }); }; createWrapper(reqOpts.success, reqOpts.callback, reqOpts.failure); let target = newopts.waitMsgTarget; if (target) { if (Proxmox.Utils.toolkit === 'touch') { target.setMasked({ xtype: 'loadmask', message: newopts.waitMsg }); } else if (target.rendered) { target.waitMsgTargetCount = (target.waitMsgTargetCount ?? 0) + 1; target.setLoading(newopts.waitMsg); } else { target.waitMsgTargetCount = (target.waitMsgTargetCount ?? 0) + 1; target.on( 'afterlayout', function () { if ((target.waitMsgTargetCount ?? 0) > 0) { target.setLoading(newopts.waitMsg); } }, target, { single: true }, ); } } Ext.Ajax.request(newopts); }, // can be useful for catching displaying errors from the API, e.g.: // Proxmox.Async.api2({ // ... // }).catch(Proxmox.Utils.alertResponseFailure); alertResponseFailure: (res) => Ext.Msg.alert( gettext('Error'), res.htmlStatus || Proxmox.Utils.extractRequestError(res.result ?? {}, true), ), checked_command: function (orig_cmd) { Proxmox.Utils.API2Request({ url: '/nodes/localhost/subscription', method: 'GET', failure: function (response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); }, success: function (response, opts) { let res = response.result; if ( res === null || res === undefined || !res || res.data.status.toLowerCase() !== 'active' ) { Ext.Msg.show({ title: gettext('No valid subscription'), icon: Ext.Msg.WARNING, message: Proxmox.Utils.getNoSubKeyHtml(res.data.url), buttons: Ext.Msg.OK, callback: function (btn) { if (btn !== 'ok') { return; } orig_cmd(); }, }); } else { orig_cmd(); } }, }); }, assemble_field_data: function (values, data) { if (!Ext.isObject(data)) { return; } Ext.Object.each(data, function (name, val) { if (Object.hasOwn(values, name)) { let bucket = values[name]; if (!Ext.isArray(bucket)) { bucket = values[name] = [bucket]; } if (Ext.isArray(val)) { values[name] = bucket.concat(val); } else { bucket.push(val); } } else { values[name] = val; } }); }, updateColumnWidth: function (container, thresholdWidth) { let mode = Ext.state.Manager.get('summarycolumns') || 'auto'; let factor; if (mode !== 'auto') { factor = parseInt(mode, 10); if (Number.isNaN(factor)) { factor = 1; } } else { thresholdWidth = (thresholdWidth || 1400) + 1; factor = Math.ceil(container.getSize().width / thresholdWidth); } if (container.oldFactor === factor) { return; } let items = container.query('>'); // direct children factor = Math.min(factor, items.length); container.oldFactor = factor; items.forEach((item) => { item.columnWidth = 1 / factor; }); // we have to update the layout twice, since the first layout change // can trigger the scrollbar which reduces the amount of space left container.updateLayout(); container.updateLayout(); }, // NOTE: depreacated, use updateColumnWidth updateColumns: (container) => Proxmox.Utils.updateColumnWidth(container), dialog_title: function (subject, create, isAdd) { if (create) { if (isAdd) { return gettext('Add') + ': ' + subject; } else { return gettext('Create') + ': ' + subject; } } else { return gettext('Edit') + ': ' + subject; } }, network_iface_types: { eth: gettext('Network Device'), bridge: 'Linux Bridge', bond: 'Linux Bond', vlan: 'Linux VLAN', OVSBridge: 'OVS Bridge', OVSBond: 'OVS Bond', OVSPort: 'OVS Port', OVSIntPort: 'OVS IntPort', }, render_network_iface_type: function (value) { return Proxmox.Utils.network_iface_types[value] || Proxmox.Utils.unknownText; }, // Only add product-agnostic fields here! notificationFieldName: { type: gettext('Notification Type'), hostname: gettext('Hostname'), }, formatNotificationFieldName: (value) => Proxmox.Utils.notificationFieldName[value] || value, // to add or change existing for product specific ones overrideNotificationFieldName: function (extra) { for (const [key, value] of Object.entries(extra)) { Proxmox.Utils.notificationFieldName[key] = value; } }, // Only add product-agnostic fields here! notificationFieldValue: { 'system-mail': gettext('Forwarded mails to the local root user'), }, formatNotificationFieldValue: (value) => Proxmox.Utils.notificationFieldValue[value] || value, // to add or change existing for product specific ones overrideNotificationFieldValue: function (extra) { for (const [key, value] of Object.entries(extra)) { Proxmox.Utils.notificationFieldValue[key] = value; } }, // NOTE: only add general, product agnostic, ones here! Else use override helper in product repos task_desc_table: { aptupdate: ['', gettext('Update package database')], diskinit: ['Disk', gettext('Initialize Disk with GPT')], spiceshell: ['', gettext('Shell') + ' (Spice)'], srvreload: ['SRV', gettext('Reload')], srvrestart: ['SRV', gettext('Restart')], srvstart: ['SRV', gettext('Start')], srvstop: ['SRV', gettext('Stop')], termproxy: ['', gettext('Console') + ' (xterm.js)'], vncshell: ['', gettext('Shell')], }, // to add or change existing for product specific ones override_task_descriptions: function (extra) { for (const [key, value] of Object.entries(extra)) { Proxmox.Utils.task_desc_table[key] = value; } }, format_task_description: function (type, id) { let farray = Proxmox.Utils.task_desc_table[type]; let text; if (!farray) { text = type; if (id) { text += ' ' + id; } return text; } else if (Ext.isFunction(farray)) { return farray(type, id); } let prefix = farray[0]; text = farray[1]; if (prefix && id !== undefined) { return prefix + ' ' + id + ' - ' + text; } return text; }, format_size: function (size, useSI) { let unitsSI = [ gettext('B'), gettext('KB'), gettext('MB'), gettext('GB'), gettext('TB'), gettext('PB'), gettext('EB'), gettext('ZB'), gettext('YB'), ]; let unitsIEC = [ gettext('B'), gettext('KiB'), gettext('MiB'), gettext('GiB'), gettext('TiB'), gettext('PiB'), gettext('EiB'), gettext('ZiB'), gettext('YiB'), ]; let order = 0; let commaDigits = 2; const baseValue = useSI ? 1000 : 1024; while (size >= baseValue && order < unitsSI.length) { size = size / baseValue; order++; } let unit = useSI ? unitsSI[order] : unitsIEC[order]; if (order === 0) { commaDigits = 0; } return `${size.toFixed(commaDigits)} ${unit}`; }, SizeUnits: { B: 1, KiB: 1024, MiB: 1024 * 1024, GiB: 1024 * 1024 * 1024, TiB: 1024 * 1024 * 1024 * 1024, PiB: 1024 * 1024 * 1024 * 1024 * 1024, KB: 1000, MB: 1000 * 1000, GB: 1000 * 1000 * 1000, TB: 1000 * 1000 * 1000 * 1000, PB: 1000 * 1000 * 1000 * 1000 * 1000, }, parse_size_unit: function (val) { //let m = val.match(/([.\d])+\s?([KMGTP]?)(i?)B?\s*$/i); let m = val.match(/(\d+(?:\.\d+)?)\s?([KMGTP]?)(i?)B?\s*$/i); let size = parseFloat(m[1]); let scale = m[2].toUpperCase(); let binary = m[3].toLowerCase(); let unit = `${scale}${binary}B`; let factor = Proxmox.Utils.SizeUnits[unit]; return { size, factor, unit, binary }; // for convenience return all we got }, size_unit_to_bytes: function (val) { let { size, factor } = Proxmox.Utils.parse_size_unit(val); return size * factor; }, autoscale_size_unit: function (val) { let { size, factor, binary } = Proxmox.Utils.parse_size_unit(val); return Proxmox.Utils.format_size(size * factor, binary !== 'i'); }, size_unit_ratios: function (a, b) { a = typeof a !== 'undefined' ? a : 0; b = typeof b !== 'undefined' ? b : Infinity; let aBytes = typeof a === 'number' ? a : Proxmox.Utils.size_unit_to_bytes(a); let bBytes = typeof b === 'number' ? b : Proxmox.Utils.size_unit_to_bytes(b); return aBytes / (bBytes || Infinity); // avoid division by zero }, render_upid: function (value, metaData, record) { let task = record.data; let type = task.type || task.worker_type; let id = task.id || task.worker_id; return Ext.htmlEncode(Proxmox.Utils.format_task_description(type, id)); }, render_uptime: function (value) { let uptime = value; if (uptime === undefined) { return ''; } if (uptime <= 0) { return '-'; } return Proxmox.Utils.format_duration_long(uptime); }, systemd_unescape: function (string_value) { const charcode_0 = '0'.charCodeAt(0); const charcode_9 = '9'.charCodeAt(0); const charcode_A = 'A'.charCodeAt(0); const charcode_F = 'F'.charCodeAt(0); const charcode_a = 'a'.charCodeAt(0); const charcode_f = 'f'.charCodeAt(0); const charcode_x = 'x'.charCodeAt(0); const charcode_minus = '-'.charCodeAt(0); const charcode_slash = '/'.charCodeAt(0); const charcode_backslash = '\\'.charCodeAt(0); let parse_hex_digit = function (d) { if (d >= charcode_0 && d <= charcode_9) { return d - charcode_0; } if (d >= charcode_A && d <= charcode_F) { return d - charcode_A + 10; } if (d >= charcode_a && d <= charcode_f) { return d - charcode_a + 10; } throw 'got invalid hex digit'; }; let value = new TextEncoder().encode(string_value); let result = new Uint8Array(value.length); let i = 0; let result_len = 0; while (i < value.length) { let c0 = value[i]; if (c0 === charcode_minus) { result.set([charcode_slash], result_len); result_len += 1; i += 1; continue; } if (i + 4 < value.length) { let c1 = value[i + 1]; if (c0 === charcode_backslash && c1 === charcode_x) { let h1 = parse_hex_digit(value[i + 2]); let h0 = parse_hex_digit(value[i + 3]); let ord = h1 * 16 + h0; result.set([ord], result_len); result_len += 1; i += 4; continue; } } result.set([c0], result_len); result_len += 1; i += 1; } return new TextDecoder().decode(result.slice(0, result.len)); }, parse_task_upid: function (upid) { let task = {}; let res = upid.match( /^UPID:([^\s:]+):([0-9A-Fa-f]{8}):([0-9A-Fa-f]{8,9}):(([0-9A-Fa-f]{8,16}):)?([0-9A-Fa-f]{8}):([^:\s]+):([^:\s]*):([^:\s]+):$/, ); if (!res) { throw "unable to parse upid '" + upid + "'"; } task.node = res[1]; task.pid = parseInt(res[2], 16); task.pstart = parseInt(res[3], 16); if (res[5] !== undefined) { task.task_id = parseInt(res[5], 16); } task.starttime = parseInt(res[6], 16); task.type = res[7]; task.id = Proxmox.Utils.systemd_unescape(res[8]); task.user = res[9]; task.desc = Proxmox.Utils.format_task_description(task.type, task.id); return task; }, parse_task_status: function (status) { if (status === 'OK') { return 'ok'; } if (status === 'unknown') { return 'unknown'; } let match = status.match(/^WARNINGS: (.*)$/); if (match) { return 'warning'; } return 'error'; }, format_task_status: function (status) { let parsed = Proxmox.Utils.parse_task_status(status); switch (parsed) { case 'unknown': return Proxmox.Utils.unknownText; case 'error': return Proxmox.Utils.errorText + ': ' + Ext.htmlEncode(status); case 'warning': return status.replace('WARNINGS', Proxmox.Utils.warningsText); case 'ok': // fall-through default: return status; } }, render_duration: function (value) { if (value === undefined) { return '-'; } return Proxmox.Utils.format_duration_human(value); }, render_timestamp: function (value, metaData, record, rowIndex, colIndex, store) { let servertime = new Date(value * 1000); return Ext.Date.format(servertime, 'Y-m-d H:i:s'); }, render_zfs_health: function (value) { if (typeof value === 'undefined') { return ''; } var iconCls = 'question-circle'; switch (value) { case 'AVAIL': case 'ONLINE': iconCls = 'check-circle good'; break; case 'REMOVED': case 'DEGRADED': iconCls = 'exclamation-circle warning'; break; case 'UNAVAIL': case 'FAULTED': case 'OFFLINE': iconCls = 'times-circle critical'; break; default: //unknown } return ' ' + value; }, get_help_info: function (section) { let helpMap; if (typeof proxmoxOnlineHelpInfo !== 'undefined') { helpMap = proxmoxOnlineHelpInfo; } else if (typeof pveOnlineHelpInfo !== 'undefined') { // be backward compatible with older pve-doc-generators helpMap = pveOnlineHelpInfo; } else { throw 'no global OnlineHelpInfo map declared'; } if (helpMap[section]) { return helpMap[section]; } // try to normalize - and _ separators, to support asciidoc and sphinx // references at the same time. let section_minus_normalized = section.replace(/_/g, '-'); if (helpMap[section_minus_normalized]) { return helpMap[section_minus_normalized]; } let section_underscore_normalized = section.replace(/-/g, '_'); return helpMap[section_underscore_normalized]; }, get_help_link: function (section) { let info = Proxmox.Utils.get_help_info(section); if (!info) { return undefined; } return window.location.origin + info.link; }, openXtermJsViewer: function (vmtype, vmid, nodename, vmname, cmd) { let url = Ext.Object.toQueryString({ console: vmtype, // kvm, lxc, upgrade or shell xtermjs: 1, vmid: vmid, vmname: vmname, node: nodename, cmd: cmd, }); let nw = window.open( '?' + url, '_blank', 'toolbar=no,location=no,status=no,menubar=no,resizable=yes,width=800,height=420', ); if (nw) { nw.focus(); } }, render_optional_url: function (value) { if (value && value.match(/^https?:\/\//)) { // normalize and encode to prevent html injection let url = new URL(value).href; let text = Ext.htmlEncode(value); return `${text}`; } return Ext.htmlEncode(value); }, render_san: function (value) { var names = []; if (Ext.isArray(value)) { value.forEach(function (val) { if (!Ext.isNumber(val)) { names.push(Ext.htmlEncode(val)); } }); return names.join('
'); } return value; }, render_usage: (val) => (val * 100).toFixed(2) + '%', render_cpu_usage: function (val, max) { return Ext.String.format( // TRANSLATORS: For example "5% of 24 CPUs" ngettext('{0}% of {1} CPU', '{0}% of {1} CPUs', max), (val * 100).toFixed(2), max, ); }, render_size_usage: function (val, max, useSI) { if (max === 0) { return gettext('N/A'); } let fmt = (v) => Proxmox.Utils.format_size(v, useSI); let ratio = ((val * 100) / max).toFixed(2); return ( ratio + '% (' + Ext.String.format(gettext('{0} of {1}'), fmt(val), fmt(max)) + ')' ); }, render_cpu: function (value, metaData, record, rowIndex, colIndex, store) { if (!(record.data.uptime && Ext.isNumeric(value))) { return ''; } let maxcpu = record.data.maxcpu || 1; if (!Ext.isNumeric(maxcpu) || maxcpu < 1) { return ''; } let cpuText = maxcpu > 1 ? 'CPUs' : 'CPU'; let ratio = (value * 100).toFixed(1); return `${ratio}% of ${maxcpu.toString()} ${cpuText}`; }, render_size: function (value, metaData, record, rowIndex, colIndex, store) { if (!Ext.isNumeric(value)) { return ''; } return Proxmox.Utils.format_size(value); }, render_cpu_model: function (cpu) { let socketText = cpu.sockets > 1 ? gettext('Sockets') : gettext('Socket'); return `${cpu.cpus} x ${cpu.model} (${cpu.sockets.toString()} ${socketText})`; }, /* this is different for nodes */ render_node_cpu_usage: function (value, record) { return Proxmox.Utils.render_cpu_usage(value, record.cpus); }, render_node_size_usage: function (record) { return Proxmox.Utils.render_size_usage(record.used, record.total); }, loadTextFromFile: function (file, callback, maxBytes) { let maxSize = maxBytes || 8192; if (file.size > maxSize) { Ext.Msg.alert(gettext('Error'), gettext('Invalid file size: ') + file.size); return; } let reader = new FileReader(); reader.onload = (evt) => callback(evt.target.result); reader.readAsText(file); }, parsePropertyString: function (value, defaultKey) { var res = {}, error; if (typeof value !== 'string' || value === '') { return res; } Ext.Array.each(value.split(','), function (p) { var kv = p.split('=', 2); if (Ext.isDefined(kv[1])) { res[kv[0]] = kv[1]; } else if (Ext.isDefined(defaultKey)) { if (Ext.isDefined(res[defaultKey])) { error = 'defaultKey may be only defined once in propertyString'; return false; // break } res[defaultKey] = kv[0]; } else { error = 'invalid propertyString, not a key=value pair and no defaultKey defined'; return false; // break } return true; }); if (error !== undefined) { console.error(error); return undefined; } return res; }, printPropertyString: function (data, defaultKey) { var stringparts = [], gotDefaultKeyVal = false, defaultKeyVal; Ext.Object.each(data, function (key, value) { if (defaultKey !== undefined && key === defaultKey) { gotDefaultKeyVal = true; defaultKeyVal = value; } else if (Ext.isArray(value)) { stringparts.push(key + '=' + value.join(';')); } else if (value !== '') { stringparts.push(key + '=' + value); } }); stringparts = stringparts.sort(); if (gotDefaultKeyVal) { stringparts.unshift(defaultKeyVal); } return stringparts.join(','); }, acmedomain_count: 5, parseACMEPluginData: function (data) { let res = {}; let extradata = []; data.split('\n').forEach((line) => { // capture everything after the first = as value let [key, value] = line.split('='); if (value !== undefined) { res[key] = value; } else { extradata.push(line); } }); return [res, extradata]; }, delete_if_default: function (values, fieldname, default_val, create) { if (values[fieldname] === '' || values[fieldname] === default_val) { if (!create) { if (values.delete) { if (Ext.isArray(values.delete)) { values.delete.push(fieldname); } else { values.delete += ',' + fieldname; } } else { values.delete = fieldname; } } delete values[fieldname]; } }, printACME: function (value) { if (Ext.isArray(value.domains)) { value.domains = value.domains.join(';'); } return Proxmox.Utils.printPropertyString(value); }, parseACME: function (value) { if (!value) { return {}; } var res = {}; var error; Ext.Array.each(value.split(','), function (p) { var kv = p.split('=', 2); if (Ext.isDefined(kv[1])) { res[kv[0]] = kv[1]; } else { error = 'Failed to parse key-value pair: ' + p; return false; } return true; }); if (error !== undefined) { console.error(error); return undefined; } if (res.domains !== undefined) { res.domains = res.domains.split(/;/); } return res; }, add_domain_to_acme: function (acme, domain) { if (acme.domains === undefined) { acme.domains = [domain]; } else { acme.domains.push(domain); acme.domains = acme.domains.filter( (value, index, self) => self.indexOf(value) === index, ); } return acme; }, remove_domain_from_acme: function (acme, domain) { if (acme.domains !== undefined) { acme.domains = acme.domains.filter( (value, index, self) => self.indexOf(value) === index && value !== domain, ); } return acme; }, get_health_icon: function (state, circle) { if (circle === undefined) { circle = false; } if (state === undefined) { state = 'uknown'; } var icon = 'faded fa-question'; switch (state) { case 'good': icon = 'good fa-check'; break; case 'upgrade': icon = 'warning fa-upload'; break; case 'old': icon = 'warning fa-refresh'; break; case 'warning': icon = 'warning fa-exclamation'; break; case 'critical': icon = 'critical fa-times'; break; default: break; } if (circle) { icon += '-circle'; } return icon; }, formatNodeRepoStatus: function (status, product) { let fmt = (txt, cls) => `${txt}`; let getUpdates = Ext.String.format(gettext('{0} updates'), product); let noRepo = Ext.String.format(gettext('No {0} repository enabled!'), product); if (status === 'ok') { return ( fmt(getUpdates, 'check-circle good') + ' ' + fmt( gettext('Production-ready Enterprise repository enabled'), 'check-circle good', ) ); } else if (status === 'no-sub') { return ( fmt( gettext('Production-ready Enterprise repository enabled'), 'check-circle good', ) + ' ' + fmt( gettext('Enterprise repository needs valid subscription'), 'exclamation-circle warning', ) ); } else if (status === 'non-production') { return ( fmt(getUpdates, 'check-circle good') + ' ' + fmt( gettext('Non production-ready repository enabled!'), 'exclamation-circle warning', ) ); } else if (status === 'no-repo') { return fmt(noRepo, 'exclamation-circle critical'); } return Proxmox.Utils.unknownText; }, render_u2f_error: function (error) { var ErrorNames = { 1: gettext('Other Error'), 2: gettext('Bad Request'), 3: gettext('Configuration Unsupported'), 4: gettext('Device Ineligible'), 5: gettext('Timeout'), }; return 'U2F Error: ' + ErrorNames[error] || Proxmox.Utils.unknownText; }, // Convert an ArrayBuffer to a base64url encoded string. // A `null` value will be preserved for convenience. bytes_to_base64url: function (bytes) { if (bytes === null) { return null; } return btoa( Array.from(new Uint8Array(bytes)) .map((val) => String.fromCharCode(val)) .join(''), ) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/[=]/g, ''); }, // Convert an a base64url string to an ArrayBuffer. // A `null` value will be preserved for convenience. base64url_to_bytes: function (b64u) { if (b64u === null) { return null; } return new Uint8Array( atob(b64u.replace(/-/g, '+').replace(/_/g, '/')) .split('') .map((val) => val.charCodeAt(0)), ); }, // Convert utf-8 string to base64. // This also escapes unicode characters such as emojis. utf8ToBase64: function (string) { let bytes = new TextEncoder().encode(string); const escapedString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join(''); return btoa(escapedString); }, // Converts a base64 string into a utf8 string. // Decodes escaped unicode characters correctly. base64ToUtf8: function (b64_string) { let string = atob(b64_string); let bytes = Uint8Array.from(string, (m) => m.codePointAt(0)); return new TextDecoder().decode(bytes); }, stringToRGB: function (string) { let hash = 0; if (!string) { return hash; } string += 'prox'; // give short strings more variance for (let i = 0; i < string.length; i++) { hash = string.charCodeAt(i) + ((hash << 5) - hash); hash = hash & hash; // to int } let alpha = 0.7; // make the color a bit brighter let bg = 255; // assume white background return [ (hash & 255) * alpha + bg * (1 - alpha), ((hash >> 8) & 255) * alpha + bg * (1 - alpha), ((hash >> 16) & 255) * alpha + bg * (1 - alpha), ]; }, rgbToCss: function (rgb) { return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`; }, rgbToHex: function (rgb) { let r = Math.round(rgb[0]).toString(16); let g = Math.round(rgb[1]).toString(16); let b = Math.round(rgb[2]).toString(16); return `${r}${g}${b}`; }, hexToRGB: function (hex) { if (!hex) { return undefined; } if (hex.length === 7) { hex = hex.slice(1); } let r = parseInt(hex.slice(0, 2), 16); let g = parseInt(hex.slice(2, 4), 16); let b = parseInt(hex.slice(4, 6), 16); return [r, g, b]; }, // optimized & simplified SAPC function // https://github.com/Myndex/SAPC-APCA getTextContrastClass: function (rgb) { const blkThrs = 0.022; const blkClmp = 1.414; // linearize & gamma correction let r = (rgb[0] / 255) ** 2.4; let g = (rgb[1] / 255) ** 2.4; let b = (rgb[2] / 255) ** 2.4; // relative luminance sRGB let bg = r * 0.2126729 + g * 0.7151522 + b * 0.072175; // black clamp bg = bg > blkThrs ? bg : bg + (blkThrs - bg) ** blkClmp; // SAPC with white text let contrastLight = bg ** 0.65 - 1; // SAPC with black text let contrastDark = bg ** 0.56 - 0.046134502; if (Math.abs(contrastLight) >= Math.abs(contrastDark)) { return 'light'; } else { return 'dark'; } }, getTagElement: function (string, color_overrides) { let rgb = color_overrides?.[string] || Proxmox.Utils.stringToRGB(string); let style = `background-color: ${Proxmox.Utils.rgbToCss(rgb)};`; let cls; if (rgb.length > 3) { style += `color: ${Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]])}`; cls = 'proxmox-tag-dark'; } else { let txtCls = Proxmox.Utils.getTextContrastClass(rgb); cls = `proxmox-tag-${txtCls}`; } return `${string}`; }, // Setting filename here when downloading from a remote url sometimes fails in chromium browsers // because of a bug when using attribute download in conjunction with a self signed certificate. // For more info see https://bugs.chromium.org/p/chromium/issues/detail?id=993362 downloadAsFile: function (source, fileName) { let hiddenElement = document.createElement('a'); hiddenElement.href = source; hiddenElement.target = '_blank'; if (fileName) { hiddenElement.download = fileName; } hiddenElement.click(); }, renderOpenStreetMapLink: function (lat, long) { let link = `https://openstreetmap.org?mlat=${lat}&mlon=${long}#map=20/${lat}/${long}`; return `open on OpenStreetMap`; }, renderLocation: function (value) { if (!value) { return Proxmox.Utils.NoneText; } let location = Proxmox.Utils.parsePropertyString(value); let link = Proxmox.Utils.renderOpenStreetMapLink(location.latitude, location.longitude); let degrees = `${location.latitude} °, ${location.longitude} °, ${link}`; if (location.name) { return `${location.name} (${degrees})`; } return degrees; }, }, singleton: true, constructor: function () { let me = this; Ext.apply(me, me.utilities); let IPV4_OCTET = '(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])'; let IPV4_REGEXP = '(?:(?:' + IPV4_OCTET + '\\.){3}' + IPV4_OCTET + ')'; let IPV6_H16 = '(?:[0-9a-fA-F]{1,4})'; let IPV6_LS32 = '(?:(?:' + IPV6_H16 + ':' + IPV6_H16 + ')|' + IPV4_REGEXP + ')'; let IPV4_CIDR_MASK = '([0-9]{1,2})'; let IPV6_CIDR_MASK = '([0-9]{1,3})'; me.IP4_match = new RegExp('^(?:' + IPV4_REGEXP + ')$'); me.IP4_cidr_match = new RegExp('^(?:' + IPV4_REGEXP + ')/' + IPV4_CIDR_MASK + '$'); let IPV6_REGEXP = '(?:' + '(?:(?:' + '(?:' + IPV6_H16 + ':){6})' + IPV6_LS32 + ')|' + '(?:(?:' + '::' + '(?:' + IPV6_H16 + ':){5})' + IPV6_LS32 + ')|' + '(?:(?:(?:' + IPV6_H16 + ')?::' + '(?:' + IPV6_H16 + ':){4})' + IPV6_LS32 + ')|' + '(?:(?:(?:(?:' + IPV6_H16 + ':){0,1}' + IPV6_H16 + ')?::' + '(?:' + IPV6_H16 + ':){3})' + IPV6_LS32 + ')|' + '(?:(?:(?:(?:' + IPV6_H16 + ':){0,2}' + IPV6_H16 + ')?::' + '(?:' + IPV6_H16 + ':){2})' + IPV6_LS32 + ')|' + '(?:(?:(?:(?:' + IPV6_H16 + ':){0,3}' + IPV6_H16 + ')?::' + '(?:' + IPV6_H16 + ':){1})' + IPV6_LS32 + ')|' + '(?:(?:(?:(?:' + IPV6_H16 + ':){0,4}' + IPV6_H16 + ')?::' + ')' + IPV6_LS32 + ')|' + '(?:(?:(?:(?:' + IPV6_H16 + ':){0,5}' + IPV6_H16 + ')?::' + ')' + IPV6_H16 + ')|' + '(?:(?:(?:(?:' + IPV6_H16 + ':){0,7}' + IPV6_H16 + ')?::' + ')' + ')' + ')'; me.IP6_match = new RegExp('^(?:' + IPV6_REGEXP + ')$'); me.IP6_cidr_match = new RegExp('^(?:' + IPV6_REGEXP + ')/' + IPV6_CIDR_MASK + '$'); me.IP6_bracket_match = new RegExp('^\\[(' + IPV6_REGEXP + ')\\]'); me.IP64_match = new RegExp('^(?:' + IPV6_REGEXP + '|' + IPV4_REGEXP + ')$'); me.IP64_cidr_match = new RegExp( '^(?:' + IPV6_REGEXP + '/' + IPV6_CIDR_MASK + ')|(?:' + IPV4_REGEXP + '/' + IPV4_CIDR_MASK + ')$', ); let DnsName_REGEXP = '(?:(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-]*[a-zA-Z0-9])?)\\.)*(?:[A-Za-z0-9](?:[A-Za-z0-9\\-]*[A-Za-z0-9])?))'; me.DnsName_match = new RegExp('^' + DnsName_REGEXP + '$'); me.DnsName_or_Wildcard_match = new RegExp('^(?:\\*\\.)?' + DnsName_REGEXP + '$'); me.CpuSet_match = /^[0-9]+(?:-[0-9]+)?(?:,[0-9]+(?:-[0-9]+)?)*$/; me.HostPort_match = new RegExp( '^(' + IPV4_REGEXP + '|' + DnsName_REGEXP + ')(?::(\\d+))?$', ); me.HostPortBrackets_match = new RegExp( '^\\[(' + IPV6_REGEXP + '|' + IPV4_REGEXP + '|' + DnsName_REGEXP + ')\\](?::(\\d+))?$', ); me.IP6_dotnotation_match = new RegExp('^(' + IPV6_REGEXP + ')(?:\\.(\\d+))?$'); me.Vlan_match = /^vlan(\d+)/; me.VlanInterface_match = /(\w+)\.(\d+)/; // Taken from proxmox-schema and ported to JS let PORT_REGEX_STR = '(?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])'; let IPRE_BRACKET_STR = '(?:' + IPV4_REGEXP + '|\\[(?:' + IPV6_REGEXP + ')\\])'; let DNS_NAME_STR = '(?:(?:' + DnsName_REGEXP + '\\.)*' + DnsName_REGEXP + ')'; let HTTP_URL_REGEX = '^https?://(?:(?:(?:' + DNS_NAME_STR + '|' + IPRE_BRACKET_STR + ')(?::' + PORT_REGEX_STR + ')?)|' + IPV6_REGEXP + ')(?:/[^\x00-\x1F\x7F]*)?$'; me.httpUrlRegex = new RegExp(HTTP_URL_REGEX); // Same as SAFE_ID_REGEX in proxmox-schema me.safeIdRegex = /^(?:[A-Za-z0-9_][A-Za-z0-9._\-\\]*)$/; }, }); Ext.define('Proxmox.Async', { singleton: true, // Returns a Promise resolving to the result of an `API2Request` or rejecting to the error // response on failure api2: function (reqOpts) { return new Promise((resolve, reject) => { delete reqOpts.callback; // not allowed in this api reqOpts.success = (response) => resolve(response); reqOpts.failure = (response) => reject(response); Proxmox.Utils.API2Request(reqOpts); }); }, // Delay for a number of milliseconds. sleep: function (millis) { return new Promise((resolve, _reject) => setTimeout(resolve, millis)); }, }); Ext.override(Ext.data.Store, { // If the store's proxy is changed while it is waiting for an AJAX // response, `onProxyLoad` will still be called for the outdated response. // To avoid displaying inconsistent information, only process responses // belonging to the current proxy. However, do not apply this workaround // to the mobile UI, as Sencha Touch has an incompatible internal API. onProxyLoad: function (operation) { let me = this; if (Proxmox.Utils.toolkit === 'touch' || operation.getProxy() === me.getProxy()) { me.callParent(arguments); } else { console.log(`ignored outdated response: ${operation.getRequest().getUrl()}`); } }, });