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: gettext('Warnings'), 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'), groupText: gettext('Group'), language_map: { //language map is sorted alphabetically by iso 639-1 ar: `العربية - ${gettext("Arabic")}`, bg: `Български - ${gettext("Bulgarian")}`, ca: `Català - ${gettext("Catalan")}`, da: `Dansk - ${gettext("Danish")}`, de: `Deutsch - ${gettext("German")}`, en: `English - ${gettext("English")}`, es: `Español - ${gettext("Spanish")}`, eu: `Euskera (Basque) - ${gettext("Euskera (Basque)")}`, fa: `فارسی - ${gettext("Persian (Farsi)")}`, fr: `Français - ${gettext("French")}`, hr: `Hrvatski - ${gettext("Croatian")}`, he: `עברית - ${gettext("Hebrew")}`, it: `Italiano - ${gettext("Italian")}`, ja: `日本語 - ${gettext("Japanese")}`, ka: `ქართული - ${gettext("Georgian")}`, ko: `한국어 - ${gettext("Korean")}`, 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 = [`${err.statusText} (${err.status})`]; if (err.response && err.response.responseText) { let txt = err.response.responseText; try { let res = JSON.parse(txt); 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, Ext.htmlEncode(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 || res.result.message), 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.prototype.hasOwnProperty.call(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) { type += ' ' + 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; // eslint-disable-line no-undef } else if (typeof pveOnlineHelpInfo !== 'undefined') { // be backward compatible with older pve-doc-generators helpMap = pveOnlineHelpInfo; // eslint-disable-line no-undef } 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?:\/\//) !== null) { return '' + value + ''; } return value; }, render_san: function(value) { var names = []; if (Ext.isArray(value)) { value.forEach(function(val) { if (!Ext.isNumber(val)) { names.push(val); } }); return names.join('
'); } return value; }, render_usage: val => (val * 100).toFixed(2) + '%', render_cpu_usage: function(val, max) { return Ext.String.format( `${gettext('{0}% of {1}')} ${gettext('CPU(s)')}`, (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.0721750; // 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(); }, }, 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 + "$"); /* eslint-disable no-useless-concat,no-multi-spaces */ 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 + ")?::" + ")" + ")" + ")"; /* eslint-enable no-useless-concat,no-multi-spaces */ 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()}`); } }, });