loadConfig(); $ipServer = $config['ipserver'] ?? 'localhost'; $apiUrl = "https://$ipServer/crm/api/v1.0/"; $token = $config['apitoken'] ?? ''; if (empty($token)) { logMessage("Error: API Token is missing in plugin configuration."); exit(1); } // Initialize UCRM Client $client = new Client([ 'base_uri' => $apiUrl, 'verify' => false, 'headers' => [ 'X-Auth-App-Key' => $token, 'Content-Type' => 'application/json', ], ]); $ucrmApi = new UcrmApi($client, $token); // Initialize UNMS Client $unmsClient = new Client([ 'base_uri' => "https://{$ipServer}/nms/api/v2.1/", 'verify' => false, 'headers' => [ 'X-Auth-Token' => $config['unmsApiToken'] ?? '' ] ]); function fetchAllClients($client) { $allClients = []; $page = 1; $limit = 500; do { try { $response = $client->get('clients', [ 'query' => [ 'limit' => $limit, 'offset' => ($page - 1) * $limit, 'isArchived' => 0, ] ]); $data = json_decode($response->getBody()->getContents(), true); if (empty($data)) break; $allClients = array_merge($allClients, $data); if (count($data) < $limit) break; $page++; } catch (\Exception $e) { logMessage("Error fetching page $page: " . $e->getMessage()); break; } } while (true); return $allClients; } function generateStrongPassword($length = 16) { $lower = 'abcdefghijkmnopqrstuvwxyz'; $upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; $digits = '23456789'; $symbols = '!@#$%&*()-_=+[]{};:,.<>?'; $all = $lower . $upper . $digits . $symbols; $pwChars = []; $pwChars[] = $lower[random_int(0, strlen($lower) - 1)]; $pwChars[] = $upper[random_int(0, strlen($upper) - 1)]; $pwChars[] = $digits[random_int(0, strlen($digits) - 1)]; $pwChars[] = $symbols[random_int(0, strlen($symbols) - 1)]; for ($i = count($pwChars); $i < $length; $i++) { $pwChars[] = $all[random_int(0, strlen($all) - 1)]; } shuffle($pwChars); return implode('', $pwChars); } /** * Fix client password AND extract network data (Site, Antena/Sectorial). * Returns an associative array with 'password', 'siteName', 'deviceInfo'. * * Edge cases handled: * - Client with tag 'NS REPETIDOR' → password = 'Este cliente funciona como Repetidor' * - Service status Ended(2) or Obsolete(5) → password = 'Servicio Finalizado' * - Device disconnected → password = 'Antena desconectada al momento de obtener la contraseña' */ function fixClientData($clientId, $ucrmApi, $unmsClient, $attributeIds, $config, $currentPassword, $logger = null) { $passwordAttributeId = $attributeIds['password']; $siteAttributeId = $attributeIds['site'] ?? null; $antenaSectorialAttributeId = $attributeIds['antenaSectorial'] ?? null; $result = [ 'password' => '', 'siteName' => null, 'deviceInfo' => null, ]; // Local helper for unified logging $logMsg = function ($msg, $level = 'info') use ($logger, $clientId) { $fullMsg = "[Client $clientId] " . $msg; logMessage($fullMsg); if ($logger) { $logger->log($level, $fullMsg); } }; try { $isTest = isTestEnvironment($config); // Fetch client attributes from CRM to check current site & antenna details $currentSite = null; $currentAntenna = null; try { $clientData = $ucrmApi->get("clients/$clientId"); $clientTags = $clientData['tags'] ?? []; foreach ($clientData['attributes'] ?? [] as $attr) { if ($attr['key'] === 'site' || $attr['customAttributeId'] == $siteAttributeId) { $currentSite = trim((string)$attr['value']); } if ($attr['key'] === 'antenaSectorial' || $attr['customAttributeId'] == $antenaSectorialAttributeId) { $currentAntenna = trim((string)$attr['value']); } } // ── Edge Case 1: Check client tags for "NS REPETIDOR" ── foreach ($clientTags as $tag) { if (stripos($tag['name'] ?? '', 'NS REPETIDOR') !== false) { $msg = 'Este cliente funciona como Repetidor'; $result['password'] = $msg; $result['deviceInfo'] = 'REPETIDOR'; $attrUpdates = []; if ($currentPassword !== $msg) { $attrUpdates[$passwordAttributeId] = $msg; } if ($antenaSectorialAttributeId && $currentAntenna !== 'REPETIDOR') { $attrUpdates[$antenaSectorialAttributeId] = 'REPETIDOR'; } if (!empty($attrUpdates)) { patchClientAttributes($ucrmApi, $clientId, $attrUpdates); $result['changed'] = true; } else { $result['changed'] = false; } $logMsg("Tag NS REPETIDOR detected → skipping"); return $result; } } } catch (\Exception $e) { $logMsg("Warning: Could not fetch client details/tags: " . $e->getMessage(), 'warning'); } // ── Get Services ── $svcs = $ucrmApi->get('clients/services', ['clientId' => $clientId]); if (empty($svcs)) { $msg = '⚠️ Cliente sin servicios/antenas'; $result['password'] = $msg; if ($currentPassword !== $msg) { patchClientAttributes($ucrmApi, $clientId, [ $passwordAttributeId => $msg, ]); $result['changed'] = true; } else { $result['changed'] = false; } $logMsg($msg, 'warning'); return $result; } $allServicePasswords = []; $numServices = count($svcs); $siteName = null; $deviceInfo = null; foreach ($svcs as $index => $svc) { $label = ($numServices > 1) ? "Servicio " . ($index + 1) . ":" : ""; $siteId = $svc['unmsClientSiteId'] ?? null; $serviceStatus = $svc['status'] ?? null; $passwordValue = ""; // ── Edge Case 2: Service Ended (2) or Obsolete (5) ── if ($serviceStatus === 2 || $serviceStatus === 5) { $passwordValue = 'Servicio Finalizado'; $logMsg("Service " . ($index + 1) . " has status $serviceStatus (Ended/Obsolete)"); $allServicePasswords[] = trim("$label $passwordValue"); continue; } if (!$siteId) { $passwordValue = "⚠️ Sin sitio"; $logMsg("Service " . ($index + 1) . " has no unmsClientSiteId", 'warning'); } else { if ($isTest) { // Test Env Logic: Preserve existing if valid $foundInCRM = false; if (!empty($currentPassword)) { if ($numServices > 1) { if (preg_match('/Servicio ' . ($index + 1) . ':\s*([^⚠️\s]+)/', $currentPassword, $matches)) { $passwordValue = $matches[1]; $foundInCRM = true; } } else { if (strpos($currentPassword, '⚠️') === false && strpos($currentPassword, 'Servicio') === false) { $passwordValue = trim($currentPassword); $foundInCRM = true; } } } if (!$foundInCRM) { $passwordValue = generateStrongPassword(20); // Static Test Data $siteName = 'VENUS'; $deviceInfo = 'Sectorial de pruebas 172.16.5.134'; $logMsg("Test Env: Generated new password (Service $index) and set static network data."); } else { $siteName = 'VENUS'; $deviceInfo = 'Sectorial de pruebas 172.16.5.134'; $logMsg("Test Env: Preserved existing password but ensured static network data."); } } else { // Production Logic try { $respDev = $unmsClient->get("devices?siteId=$siteId"); $devs = json_decode($respDev->getBody()->getContents(), true); if (empty($devs)) { $passwordValue = "⚠️ Sin antena"; $logMsg("No devices found for siteId: $siteId", 'warning'); } else { $passVault = null; // Find first client-like device or fallback to first device $firstDev = null; foreach ($devs as $d) { $role = $d['identification']['role'] ?? ''; if ($role === 'station' || $role === 'cpe' || $role === 'client') { $firstDev = $d; break; } } if ($firstDev === null) { $firstDev = $devs[0]; } // Extract network data (always execute for first device if index is 0) if ($firstDev && $index === 0) { $siteName = $firstDev['identification']['site']['parent']['name'] ?? null; $apDeviceName = $firstDev['attributes']['apDevice']['name'] ?? null; $apDeviceId = $firstDev['attributes']['apDevice']['id'] ?? null; if ($apDeviceName && $apDeviceId) { try { $respApDev = $unmsClient->get("devices/$apDeviceId"); $apDevData = json_decode($respApDev->getBody()->getContents(), true); $apDeviceIP = $apDevData['ipAddress'] ?? ''; $deviceInfo = trim("$apDeviceName $apDeviceIP"); // AP Device Site Name Fallback: if (empty($siteName)) { $siteName = $apDevData['identification']['site']['name'] ?? null; } } catch (\Exception $e) { $deviceInfo = $apDeviceName; $logMsg("Warning: Could not fetch AP device IP for $apDeviceId: " . $e->getMessage(), 'warning'); } } elseif (!$apDeviceName) { $deviceInfo = 'REPETIDOR'; } } // ── Extraer contraseña previa de este servicio en el CRM si existe ── $prevServicePassword = null; $hasValidPrevPassword = false; if (!empty($currentPassword)) { if ($numServices > 1) { if (preg_match('/Servicio ' . ($index + 1) . ':\s*([^⚠️\s]+)/', $currentPassword, $matches)) { $tempPass = trim($matches[1]); // Validar que no sea una leyenda de error conocida o un emoji if (stripos($tempPass, 'desconectada') === false && stripos($tempPass, 'Finalizado') === false && stripos($tempPass, 'Error') === false && stripos($tempPass, 'Repetidor') === false && stripos($tempPass, 'sin') === false) { $prevServicePassword = $tempPass; $hasValidPrevPassword = true; } } } else { $tempPass = trim($currentPassword); if (strpos($tempPass, '⚠️') === false && strpos($tempPass, 'Servicio') === false && stripos($tempPass, 'desconectada') === false && stripos($tempPass, 'Finalizado') === false && stripos($tempPass, 'Error') === false && stripos($tempPass, 'Repetidor') === false && stripos($tempPass, 'sin') === false) { $prevServicePassword = $tempPass; $hasValidPrevPassword = true; } } } // ── Edge Case 3: Check if device is disconnected ── $deviceStatus = $firstDev['overview']['status'] ?? 'unknown'; if ($deviceStatus === 'disconnected') { if ($hasValidPrevPassword) { // Preservamos la contraseña real para que el instalador pueda usarla $passwordValue = $prevServicePassword; $logMsg("Device is disconnected (siteId=$siteId), but preserved existing valid password from CRM: '$passwordValue'."); } else { $passwordValue = 'Antena desconectada al momento de obtener la contraseña'; $logMsg("Device is disconnected (siteId=$siteId) and no valid password was found in CRM.", 'warning'); } $allServicePasswords[] = trim("$label $passwordValue"); continue; // Skip vault check for this service } $firstDeviceId = null; foreach ($devs as $dev) { $deviceId = $dev['identification']['id'] ?? null; if (!$deviceId) continue; if (!$firstDeviceId) $firstDeviceId = $deviceId; try { $respVault = $unmsClient->get("vault/$deviceId/credentials"); $vault = json_decode($respVault->getBody()->getContents(), true); if (isset($vault['credentials'][0]['password'])) { $passVault = $vault['credentials'][0]['password']; break; } } catch (\Exception $e) { $logMsg("Could not fetch credentials from device $deviceId: " . $e->getMessage(), 'debug'); continue; } } if ($passVault) { $passwordValue = $passVault; } elseif ($firstDeviceId) { // Regenerate on Device $newPass = generateStrongPassword(16); try { $unmsClient->post("vault/$firstDeviceId/credentials/regenerate", [ 'json' => [['username' => 'ubnt', 'password' => $newPass, 'readOnly' => true]] ]); $passwordValue = $newPass; $logMsg("Regenerated password on UNMS Device $firstDeviceId"); } catch (\Exception $e) { $passwordValue = "⚠️ Error Regenerating: " . $e->getMessage(); $logMsg("Error regenerating password on device $firstDeviceId: " . $e->getMessage(), 'error'); } } else { $passwordValue = "⚠️ Sin dispositivo válido"; $logMsg("No valid device ID for credentials", 'warning'); } } } catch (\Exception $e) { $passwordValue = "⚠️ Error API UNMS: " . $e->getMessage(); $logMsg("Exception during UNMS API query: " . $e->getMessage(), 'error'); } } } $allServicePasswords[] = trim("$label $passwordValue"); } $finalValue = implode(' ', $allServicePasswords); $result['password'] = $finalValue; $result['siteName'] = $siteName; $result['deviceInfo'] = $deviceInfo; // Build attribute updates (always patch all available fields) $attrUpdates = []; // Password: only if changed if ($finalValue !== $currentPassword) { $attrUpdates[$passwordAttributeId] = $finalValue; } // Site: patch if we got data and it is different if ($siteAttributeId) { if ($siteName !== null) { if (!empty($siteName) || empty($currentSite)) { if ($siteName !== $currentSite) { $attrUpdates[$siteAttributeId] = $siteName; $logMsg("Site to update resolved: '$siteName' (previous was: '" . ($currentSite ?: 'empty') . "')"); } else { $logMsg("Site name matches current CRM value '$currentSite'. Patch skipped."); } } else { $logMsg("Site name resolved from UISP is empty and CRM already has a value '$currentSite'. Prevented overwriting with empty value."); } } else { $logMsg("UISP did not return a parent site name (siteName is null)."); } } else { $logMsg("Site attribute ID ('site') was not resolved."); } // Antena/Sectorial: patch if we got data and it is different if ($antenaSectorialAttributeId) { if ($deviceInfo !== null) { $isNewValueRepeater = (strtoupper($deviceInfo) === 'REPETIDOR'); $isCurrentValueValuable = (!empty($currentAntenna) && strtoupper($currentAntenna) !== 'REPETIDOR' && stripos($currentAntenna, '⚠️') === false); // Only update if we're not overwriting a valid sector name with a generic 'REPETIDOR' if (!($isNewValueRepeater && $isCurrentValueValuable)) { if ($deviceInfo !== $currentAntenna) { $attrUpdates[$antenaSectorialAttributeId] = $deviceInfo; $logMsg("Antenna/Sectorial to update resolved: '$deviceInfo' (previous was: '" . ($currentAntenna ?: 'empty') . "')"); } else { $logMsg("Antenna info matches current CRM value '$currentAntenna'. Patch skipped."); } } else { $logMsg("Prevented overwriting valuable CRM antenna sector '$currentAntenna' with generic 'REPETIDOR'."); } } else { $logMsg("UISP did not return antenna info (deviceInfo is null)."); } } else { $logMsg("Antenna attribute ID ('antenaSectorial') was not resolved."); } if (!empty($attrUpdates)) { patchClientAttributes($ucrmApi, $clientId, $attrUpdates); $logMsg("Patched " . count($attrUpdates) . " attribute(s): " . json_encode($attrUpdates)); } $result['changed'] = ($finalValue !== $currentPassword) || !empty($attrUpdates); return $result; } catch (\Exception $e) { $logMsg("Fatal error in fixClientData: " . $e->getMessage(), 'error'); $result['password'] = "Error fixing: " . $e->getMessage(); return $result; } } /** * Patch multiple client custom attributes in a single API call. */ function patchClientAttributes($ucrmApi, $clientId, array $attributeUpdates) { $attributes = []; foreach ($attributeUpdates as $attrId => $value) { if ($attrId && $value !== null) { $attributes[] = ['customAttributeId' => (int)$attrId, 'value' => (string)$value]; } } if (!empty($attributes)) { $ucrmApi->patch("clients/$clientId", ['attributes' => $attributes]); } } /** * Resolves UCRM custom attribute IDs based on keys. */ function resolveAttributeIds($ucrmApi, $customAttributeKey, $siteAttributeKey, $antenaSectorialAttributeKey) { $customAttributeId = null; $siteAttributeId = null; $antenaSectorialAttributeId = null; try { $attributes = $ucrmApi->get('custom-attributes', ['attributeType' => 'client']); foreach ($attributes as $attr) { switch ($attr['key']) { case $customAttributeKey: $customAttributeId = $attr['id']; break; case $siteAttributeKey: $siteAttributeId = $attr['id']; break; case $antenaSectorialAttributeKey: $antenaSectorialAttributeId = $attr['id']; break; } } } catch (\Exception $e) { logMessage("Error fetching attributes: " . $e->getMessage()); return null; // Return null on failure } if (!$customAttributeId) { logMessage("Error: Custom attribute '$customAttributeKey' not found."); return null; } // Log resolved IDs logMessage("Resolved attribute IDs: password=$customAttributeId, site=" . ($siteAttributeId ?? 'N/A') . ", antenaSectorial=" . ($antenaSectorialAttributeId ?? 'N/A')); return [ 'password' => $customAttributeId, 'site' => $siteAttributeId, 'antenaSectorial' => $antenaSectorialAttributeId, ]; } // -- Main Execution -- if (!defined('INCLUDED_AS_LIBRARY')) { // Initialization $config = PluginConfigManager::create()->loadConfig(); $ipServer = $config['ipserver'] ?? 'localhost'; $apiUrl = "https://$ipServer/crm/api/v1.0/"; $token = $config['apitoken'] ?? ''; if (empty($token)) { logMessage("Error: API Token is missing in plugin configuration."); exit(1); } // Initialize UCRM Client $client = new Client([ 'base_uri' => $apiUrl, 'verify' => false, 'headers' => [ 'X-Auth-App-Key' => $token, 'Content-Type' => 'application/json', ], ]); $ucrmApi = new UcrmApi($client, $token); // Initialize UNMS Client $unmsClient = new Client([ 'base_uri' => "https://{$ipServer}/nms/api/v2.1/", 'verify' => false, 'headers' => [ 'X-Auth-Token' => $config['unmsApiToken'] ?? '' ] ]); // Resolve IDs $attributeIds = resolveAttributeIds($ucrmApi, $customAttributeKey, $siteAttributeKey, $antenaSectorialAttributeKey); if (!$attributeIds) { exit(1); } if ($fixOption === null) { // Check for --file argument $fileOption = null; foreach ($argv as $arg) { if (strpos($arg, '--file=') === 0) { $fileOption = substr($arg, 7); } } if ($fileOption) { if ($fileOption[0] !== '/') { $fileOption = $initialDir . '/' . $fileOption; } if (!file_exists($fileOption)) { logMessage("Error: File not found: $fileOption"); exit(1); } logMessage("Processing from file: $fileOption"); // Read all lines $lines = file($fileOption, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); // Process lines $originalCount = count($lines); $processedCount = 0; // Helper to rewrite file $rewriteFile = function ($remainingLines) use ($fileOption) { file_put_contents($fileOption, implode("\n", $remainingLines) . "\n"); }; $header = null; if (!empty($lines)) { $firstLine = $lines[0]; $parts = explode(',', $firstLine); if (!is_numeric($parts[0])) { $header = array_shift($lines); // Remove header from processing list } } $queue = $lines; // Working copy while (!empty($queue)) { $line = array_shift($queue); // Take next if (empty(trim($line))) continue; $clientId = trim($line); if (!is_numeric($clientId)) { logMessage("Skipping invalid line: $line"); continue; } $clientId = (int)$clientId; logMessage("Processing ID from file: $clientId"); try { $clientDataResponse = $ucrmApi->get("clients/$clientId"); $currentAttributes = $clientDataResponse['attributes'] ?? []; $currentPass = null; foreach ($currentAttributes as $attr) { if ($attr['key'] === $customAttributeKey) { $currentPass = $attr['value']; break; } } $fixResult = fixClientData($clientId, $ucrmApi, $unmsClient, $attributeIds, $config, $currentPass); $newPass = $fixResult['password']; $siteInfo = $fixResult['siteName'] ? " | Site: {$fixResult['siteName']}" : ''; $devInfo = $fixResult['deviceInfo'] ? " | Antena: {$fixResult['deviceInfo']}" : ''; logMessage("Client $clientId result: $newPass$siteInfo$devInfo"); } catch (\Exception $e) { logMessage("Error processing Client $clientId: " . $e->getMessage()); } // Rewrite file $validLines = $queue; if ($header) { array_unshift($validLines, $header); } $rewriteFile($validLines); } logMessage("File processing complete."); exit(0); } } if ($fixLimit > 0) { logMessage("Fix mode enabled. Limit: " . ($fixOption === 'all' ? 'ALL' : $fixLimit)); } // 1. Fetch all clients logMessage("Fetching clients..."); $clients = fetchAllClients($client); logMessage("Total active clients fetched: " . count($clients)); // 2. Filter & Fix $results = []; $fixedCount = 0; foreach ($clients as $clientData) { $password = null; $attributes = $clientData['attributes'] ?? []; foreach ($attributes as $attr) { if ($attr['key'] === $customAttributeKey) { $password = $attr['value']; break; } } // Validation Logic $isValid = true; $reason = ""; // Detect explicit error states that should be retried (e.g. "⚠️ Error Regenerating...") $isError = (stripos((string)$password, 'Error') !== false); $isManaged = (strpos((string)$password, 'Servicio') !== false || strpos((string)$password, '⚠️') !== false); if (empty($password)) { $isValid = false; $reason = "Missing"; } elseif ($isError) { $isValid = false; $reason = "Error (Retry)"; } elseif (!$isManaged) { $len = strlen($password); if ($len < 8 || $len > 20) { $isValid = false; $reason = "Length ($len)"; } elseif (strpos($password, ' ') !== false) { $isValid = false; $reason = "Spaces"; } } if (!$isValid) { $newPassword = ""; $actionTaken = "None"; // Fix Logic if ($fixLimit > 0 && $fixedCount < $fixLimit) { logMessage("Fixing client {$clientData['id']}..."); $fixResult = fixClientData($clientData['id'], $ucrmApi, $unmsClient, $attributeIds, $config, $password); $newPassword = $fixResult['password']; // Basic check if fix resulted in a change/value if ($fixResult['changed'] ?? false) { $actionTaken = "Updated"; $fixedCount++; } else { $actionTaken = "Attempted (No Change/Error)"; } } $results[] = [ 'clientId' => $clientData['id'], 'name' => ($clientData['firstName'] ?? '') . ' ' . ($clientData['lastName'] ?? ''), 'userIdent' => $clientData['userIdent'] ?? '', 'currentPassword' => $password ?? '', 'action' => $actionTaken, 'newPassword' => $newPassword ]; } } // 3. Output CSV logMessage("Found " . count($results) . " clients with invalid passwords."); if ($fixLimit > 0) { logMessage("Fixed $fixedCount clients."); } echo "clientId,name,userIdent,currentPassword,action,newPassword\n"; foreach ($results as $row) { echo sprintf( "%s,\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n", $row['clientId'], str_replace('"', '""', $row['name']), str_replace('"', '""', $row['userIdent']), str_replace('"', '""', $row['currentPassword']), $row['action'], str_replace('"', '""', $row['newPassword']) ); } logMessage("Done."); }