// integrations/cisco-mpp-phone/statusXml.js // // Parser for MPP /admin/status.xml and /admin/cfg.xml snapshots. // Uses the same nested-xml reader as DECT (phones use a similar dump shape). import { xmlToObject } from '../cisco-dect/statusXml.js'; const MAC_RE = /^[0-9a-f]{12}$/i; /** * @param {string} rawXml * @returns {object} */ export function parseStatusXml(rawXml) { if (!rawXml || typeof rawXml !== 'string') { return { fields: {}, device: {}, lines: [], rebootHistory: [], network: {}, provisioning: {} }; } let tree; try { tree = xmlToObject(rawXml); } catch { return { fields: {}, device: {}, lines: [], rebootHistory: [], network: {}, provisioning: {}, parseError: 'xml parse failed' }; } const root = pickRoot(tree); const fields = flattenLeaves(root); return { fields, device: extractDevice(fields, root), lines: extractLines(fields, root), rebootHistory: extractRebootHistory(fields, root), network: extractNetwork(fields), provisioning: extractProvisioning(fields), sip: extractSip(fields), // Back-compat aliases used by summarizePhoneHealth + collector macAddress: extractDevice(fields, root).mac || null, firmware: extractDevice(fields, root).firmware || null, ipAddress: extractDevice(fields, root).ip || extractNetwork(fields).ipv4 || null, registration: extractPrimaryRegistration(fields, root), }; } /** * Parse cfg.xml — passwords are redacted by the phone; useful for SIP/provision context. * @param {string} rawXml * @returns {object} */ export function parseCfgXml(rawXml) { if (!rawXml || typeof rawXml !== 'string') { return { fields: {}, highlights: {} }; } let tree; try { tree = xmlToObject(rawXml); } catch { return { fields: {}, highlights: {}, parseError: 'xml parse failed' }; } const root = pickRoot(tree); const fields = flattenLeaves(root); return { fields, highlights: { webServer: pickField(fields, ['Enable_Web_Server', 'System.Enable_Web_Server']), webAdminAccess: pickField(fields, ['Enable_Web_Admin_Access', 'System.Enable_Web_Admin_Access']), sipProxy: pickField(fields, ['Proxy', 'SIP_Proxy', 'Line_1.Proxy', 'Line_1.SIP_Proxy']), registrar: pickField(fields, ['Registrar', 'Line_1.Registrar']), userId: pickField(fields, ['User_ID', 'Line_1.User_ID', 'Line_1.Auth_User_ID']), displayName: pickField(fields, ['Display_Name', 'Line_1.Display_Name']), upgradeRule: pickField(fields, ['Upgrade_Rule', 'Upgrade_Enable']), profileRule: pickField(fields, ['Profile_Rule']), }, }; } export function summarizePhoneHealth(parsed) { const warnings = []; const info = []; if (!parsed?.fields || Object.keys(parsed.fields).length === 0) { return { healthy: false, warnings: ['status.xml parsed empty or missing'], info }; } const fieldCount = Object.keys(parsed.fields).length; info.push(`${fieldCount} status field(s) extracted`); const reg = parsed.registration || parsed.lines?.[0]?.state || parsed.lines?.[0]?.registration; if (reg) { const regStr = String(reg).toLowerCase(); if (regStr.includes('unreg') || regStr.includes('fail') || regStr === '0' || regStr.includes('not registered')) { warnings.push(`registration: ${reg}`); } else { info.push(`registration: ${reg}`); } } else { warnings.push('no line registration state found in status.xml'); } if (parsed.device?.firmware) info.push(`firmware: ${parsed.device.firmware}`); if (parsed.device?.mac) info.push(`mac: ${parsed.device.mac}`); if (parsed.network?.ipv4 || parsed.ipAddress) { info.push(`ip: ${parsed.network?.ipv4 || parsed.ipAddress}`); } if (parsed.network?.vlan) info.push(`vlan: ${parsed.network.vlan}`); const reboots = parsed.rebootHistory || []; if (reboots.length > 0) { info.push(`reboot history: ${reboots.length} entr${reboots.length === 1 ? 'y' : 'ies'} (latest: ${reboots[0]})`); } return { healthy: warnings.length === 0, warnings, info }; } function pickRoot(tree) { if (!tree || typeof tree !== 'object') return {}; return tree.Status || tree.status || tree['flat-profile'] || tree['flat_profile'] || tree.device || tree.Device || tree; } export function flattenLeaves(node, prefix = '', out = {}) { if (node == null) return out; if (typeof node === 'string') { const val = node.trim(); if (val && prefix) out[prefix] = val; return out; } if (typeof node !== 'object') return out; for (const [k, v] of Object.entries(node)) { const key = prefix ? `${prefix}.${k}` : k; if (typeof v === 'string') { const val = v.trim(); if (val) out[key] = val; } else if (v && typeof v === 'object') { flattenLeaves(v, key, out); } } return out; } function pickField(fields, candidates) { for (const c of candidates) { if (fields[c]) return fields[c]; } const lower = Object.fromEntries(Object.entries(fields).map(([k, v]) => [k.toLowerCase(), v])); for (const c of candidates) { const hit = lower[c.toLowerCase()]; if (hit) return hit; } return null; } function normalizeMac(raw) { if (!raw || typeof raw !== 'string') return null; const hex = raw.replace(/[^0-9a-fA-F]/g, '').toLowerCase(); if (hex.length !== 12) return null; return hex.match(/../g).join(':'); } function extractDevice(fields, root) { const mac = normalizeMac(pickField(fields, [ 'MAC_Address', 'MACAddress', 'Mac_Address', 'Network.MAC_Address', 'Product_information.MAC_Address', ])); return { mac, ip: pickField(fields, ['IP_Address', 'IPAddress', 'Network.IP_Address', 'IPv4_Address', 'Network.IPv4_Address']), firmware: pickField(fields, [ 'Firmware_Version', 'Software_Version', 'SoftwareVersion', 'Product_information.Software_Version', ]), serial: pickField(fields, ['Serial_Number', 'SerialNumber', 'Product_information.Serial_Number']), product: pickField(fields, ['Product_Name', 'Phone_Type', 'Product_information.Product_Name']), uptime: pickField(fields, ['Operating_Time', 'Elapsed_Time', 'Up_Time']), }; } function extractNetwork(fields) { return { ipv4: pickField(fields, ['IP_Address', 'IPAddress', 'Network.IP_Address', 'IPv4_Address', 'Network.IPv4_Address', 'Current_IP']), ipv6: pickField(fields, ['IPv6_Address', 'Network.IPv6_Address']), vlan: pickField(fields, ['VLAN_ID', 'Vlan_ID', 'Network.VLAN_ID']), hostname: pickField(fields, ['Host_Name', 'HostName', 'Network.Host_Name']), switchPort: pickField(fields, ['Switch_Port_Config', 'Switch_Port_Link', 'Network.Switch_Port_Config']), dns: pickField(fields, ['DNS_1', 'DNS1', 'Network.DNS_1']), gateway: pickField(fields, ['Default_Router', 'Default_Gateway', 'Network.Default_Router']), }; } function extractProvisioning(fields) { return { state: pickField(fields, [ 'Customization', 'Customization_State', 'Provisioning_Status', 'Provision_Status', ]), profile: pickField(fields, ['Profile_Rule', 'Provisioning_Profile']), lastResync: pickField(fields, ['Last_Resync', 'Last_Resync_Time']), }; } function extractSip(fields) { return { proxy: pickField(fields, ['Proxy', 'SIP_Proxy', 'Line_1.Proxy']), registrar: pickField(fields, ['Registrar', 'Line_1.Registrar']), outboundProxy: pickField(fields, ['Outbound_Proxy', 'Line_1.Outbound_Proxy']), }; } function extractRebootHistory(fields, root) { const out = []; const history = root?.Reboot_History || root?.RebootHistory; if (history && typeof history === 'object') { for (let i = 1; i <= 5; i += 1) { const v = history[`Reboot_Reason_${i}`] || history[`RebootReason${i}`]; if (v && String(v).trim()) out.push(String(v).trim()); } } for (let i = 1; i <= 5; i += 1) { const v = fields[`Reboot_Reason_${i}`] || fields[`Reboot_History.Reboot_Reason_${i}`]; if (v && !out.includes(v)) out.push(v); } return out; } function extractLines(fields, root) { const lines = []; for (let i = 1; i <= 8; i += 1) { const prefix = `Line_${i}`; const state = pickField(fields, [ `${prefix}.Registration`, `${prefix}.Line_State`, `${prefix}.State`, `Registration_${i}`, `Line_State_${i}`, ]); const user = pickField(fields, [`${prefix}.User_ID`, `${prefix}.Auth_User_ID`, `${prefix}.Display_Name`]); const active = pickField(fields, [`${prefix}.Active`, `${prefix}.Call_State`]); if (state || user || active) { lines.push({ index: i, userId: user, state: state || active, registration: state, }); } } // Fallback: scan for any registration-ish keys if (lines.length === 0) { for (const [k, v] of Object.entries(fields)) { if (/line.*regist|registration/i.test(k) && v) { lines.push({ index: lines.length + 1, state: v, registration: v, key: k }); } } } return lines; } function extractPrimaryRegistration(fields, root) { const line = extractLines(fields, root)[0]; if (line?.registration || line?.state) return line.registration || line.state; return pickField(fields, [ 'Registration', 'Line_State', 'SIP_Registration', 'Phone_State', ]); }