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) { $passwordAttributeId = $attributeIds['password']; $siteAttributeId = $attributeIds['site'] ?? null; $antenaSectorialAttributeId = $attributeIds['antenaSectorial'] ?? null; $result = [ 'password' => '', 'siteName' => null, 'deviceInfo' => null, ]; try { $isTest = isTestEnvironment($config); // ── Edge Case 1: Check client tags for "NS REPETIDOR" ── try { $clientData = $ucrmApi->get("clients/$clientId"); $clientTags = $clientData['tags'] ?? []; foreach ($clientTags as $tag) { if (stripos($tag['name'] ?? '', 'NS REPETIDOR') !== false) { $msg = 'Este cliente funciona como Repetidor'; $result['password'] = $msg; $result['deviceInfo'] = 'REPETIDOR'; if ($currentPassword !== $msg) { $attrUpdates = [$passwordAttributeId => $msg]; if ($antenaSectorialAttributeId) { $attrUpdates[$antenaSectorialAttributeId] = 'REPETIDOR'; } patchClientAttributes($ucrmApi, $clientId, $attrUpdates); $result['changed'] = true; } else { $result['changed'] = false; } logMessage("Client $clientId: Tag NS REPETIDOR detected → skipping"); return $result; } } } catch (\Exception $e) { logMessage("Warning: Could not fetch client tags for $clientId: " . $e->getMessage()); } // ── 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; } 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'; logMessage("Client $clientId: Service " . ($index + 1) . " has status $serviceStatus (Ended/Obsolete)"); $allServicePasswords[] = trim("$label $passwordValue"); continue; } if (!$siteId) { $passwordValue = "⚠️ Sin sitio"; } 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); logMessage("Test Env: Generated new password for Client $clientId (Service $index)"); } else { logMessage("Test Env: Preserved existing password for Client $clientId"); } } else { // Production Logic try { $respDev = $unmsClient->get("devices?siteId=$siteId"); $devs = json_decode($respDev->getBody()->getContents(), true); if (empty($devs)) { $passwordValue = "⚠️ Sin antena"; } else { $passVault = null; $firstDeviceId = null; // ── Edge Case 3: Check if device is disconnected ── $firstDev = $devs[0] ?? null; $deviceStatus = $firstDev['overview']['status'] ?? 'unknown'; if ($deviceStatus === 'disconnected') { $passwordValue = 'Antena desconectada al momento de obtener la contraseña'; logMessage("Client $clientId: Device is disconnected (siteId=$siteId)"); // Still extract network data even if disconnected 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"); } catch (\Exception $e) { $deviceInfo = $apDeviceName; } } elseif (!$apDeviceName) { $deviceInfo = 'REPETIDOR'; } } $allServicePasswords[] = trim("$label $passwordValue"); continue; // Skip vault/regenerate for this service } // $firstDev already set above (line 258) if ($firstDev && $index === 0) { // Site name from parent site $siteName = $firstDev['identification']['site']['parent']['name'] ?? null; // AP Device name and IP $apDeviceName = $firstDev['attributes']['apDevice']['name'] ?? null; $apDeviceId = $firstDev['attributes']['apDevice']['id'] ?? null; if ($apDeviceName && $apDeviceId) { // Fetch AP device IP try { $respApDev = $unmsClient->get("devices/$apDeviceId"); $apDevData = json_decode($respApDev->getBody()->getContents(), true); $apDeviceIP = $apDevData['ipAddress'] ?? ''; $deviceInfo = trim("$apDeviceName $apDeviceIP"); } catch (\Exception $e) { $deviceInfo = $apDeviceName; logMessage("Warning: Could not fetch AP device IP for $apDeviceId: " . $e->getMessage()); } } elseif (!$apDeviceName) { // No apDevice = possibly a repeater $deviceInfo = 'REPETIDOR'; logMessage("Client $clientId: Device is a repeater (no apDevice)"); } } 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) { 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; logMessage("Prod Env: Regenerated password on UNMS Device $firstDeviceId"); } catch (\Exception $e) { $passwordValue = "⚠️ Error Regenerating: " . $e->getMessage(); } } else { $passwordValue = "⚠️ Sin dispositivo válido"; } } } catch (\Exception $e) { $passwordValue = "⚠️ Error API UNMS: " . $e->getMessage(); } } } $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 attribute ID exists if ($siteName !== null && $siteAttributeId) { $attrUpdates[$siteAttributeId] = $siteName; } // Antena/Sectorial: patch if we got data and attribute ID exists if ($deviceInfo !== null && $antenaSectorialAttributeId) { $attrUpdates[$antenaSectorialAttributeId] = $deviceInfo; } if (!empty($attrUpdates)) { patchClientAttributes($ucrmApi, $clientId, $attrUpdates); logMessage("Client $clientId: Patched " . count($attrUpdates) . " attribute(s)"); } $result['changed'] = ($finalValue !== $currentPassword) || !empty($attrUpdates); return $result; } catch (\Exception $e) { $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]); } } // 0. Resolve Attribute IDs $customAttributeId = null; $siteAttributeId = null; $antenaSectorialAttributeId = null; try { $attributes = $ucrmApi->get('custom-attributes', ['attributeType' => 'client']); foreach ($attributes as $attr) { match ($attr['key']) { $customAttributeKey => $customAttributeId = $attr['id'], $siteAttributeKey => $siteAttributeId = $attr['id'], $antenaSectorialAttributeKey => $antenaSectorialAttributeId = $attr['id'], default => null, }; } } catch (\Exception $e) { logMessage("Error fetching attributes: " . $e->getMessage()); exit(1); } if (!$customAttributeId) { logMessage("Error: Custom attribute '$customAttributeKey' not found."); exit(1); } // Log resolved IDs logMessage("Resolved attribute IDs: password=$customAttributeId, site=" . ($siteAttributeId ?? 'N/A') . ", antenaSectorial=" . ($antenaSectorialAttributeId ?? 'N/A')); // Build attribute IDs map for fixClientData $attributeIds = [ 'password' => $customAttributeId, 'site' => $siteAttributeId, 'antenaSectorial' => $antenaSectorialAttributeId, ]; // -- Main Execution -- 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] !== '/') { global $initialDir; $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 // We need to loop, process, remove, rewrite. // Ideally, we process the list in memory and rewrite the file after EACH success to satisfy "removing it as we go". $originalCount = count($lines); $processedCount = 0; // Helper to rewrite file $rewriteFile = function ($remainingLines) use ($fileOption) { file_put_contents($fileOption, implode("\n", $remainingLines) . "\n"); }; // We allow a header row if it starts with "clientId" or "id" // If we remove the header, we lose it for subsequent runs? No, we should probably keep header if present? // Or just assume data. // Let's assume the user might have a header. // If the first line is non-numeric, treat as header? $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; } // Process Client $clientId = (int)$clientId; logMessage("Processing ID from file: $clientId"); // Use fetchAllClients filtered by ID? No, too slow. // Directly use fixClientPassword which fetches services inside. // BUT we need the current password to decide if we fix? // `fixClientPassword` takes `$currentPassword`. // We can fetch the specific client first. try { $clientDataResponse = $ucrmApi->get("clients/$clientId"); $currentAttributes = $clientDataResponse['attributes'] ?? []; $currentPass = null; foreach ($currentAttributes as $attr) { if ($attr['key'] === $customAttributeKey) { $currentPass = $attr['value']; break; } } // Determine if valid (same logic as audit) // Actually, if it's in the list, we assume it's invalid? // Or should we re-validate? safer to re-validate slightly to get current state. $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()); // Do we remove it if error? // User said "que cada que procese un id lo quite". // Usually specific errors might warrant keeping, but if we want to "resume", maybe we remove logic? // Let's assuming "Processed" means attempted. } // Rewrite file // Content = Header (if any) + Remaining Queue $validLines = $queue; if ($header) { array_unshift($validLines, $header); } $rewriteFile($validLines); // Sleep slightly to avoid hammering? // usleep(100000); } 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 = ""; // Strict validation for "invalid" detection to trigger fix, // BUT we need to be careful not to flag "valid" generated strings (like "Servicio 1: ...") as invalid if we just want to audit "missing". // Facade generates complex strings. If we flag them all as invalid because of spaces, we will loop forever fixing them (if generating same structure). // Logic: If it contains "Servicio" or "⚠️", it is "Managed". // Only flag if Empty OR (Length check fail AND Not Managed). // 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.");