siip-whatsapp-notifications.../scripts-uisp/audit_client_passwords.php

796 lines
32 KiB
PHP
Executable File

<?php
$initialDir = getcwd();
chdir(__DIR__ . '/../');
require_once __DIR__ . '/../vendor/autoload.php';
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use GuzzleHttp\Client;
// -- Configuration --
$customAttributeKey = 'passwordAntenaCliente';
$siteAttributeKey = 'site';
$antenaSectorialAttributeKey = 'antenaSectorial';
$logFile = __DIR__ . '/audit_passwords.log';
// -- Argument Parsing --
$fixOption = null;
foreach ($argv as $arg) {
if (strpos($arg, '--fix=') === 0) {
$fixOption = substr($arg, 6);
}
}
$fixLimit = 0;
if ($fixOption === 'all') {
$fixLimit = PHP_INT_MAX;
} elseif (is_numeric($fixOption)) {
$fixLimit = (int)$fixOption;
}
// -- Helpers --
function logMessage($message)
{
$logFile = __DIR__ . '/audit_passwords.log';
$timestamp = date('Y-m-d H:i:s');
file_put_contents($logFile, "[$timestamp] $message\n", FILE_APPEND);
// Only write to STDERR if running in CLI mode, not web server (optional, but good practice)
if (php_sapi_name() === 'cli') {
fwrite(STDERR, "[$timestamp] $message\n");
}
}
function isTestEnvironment($config)
{
$ipServer = $config['ipserver'] ?? '';
return ($ipServer === '172.16.5.134' || $ipServer === 'venus.siip.mx' || $ipServer === 'pruebas.internet.mx');
}
// -- 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'] ?? ''
]
]);
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.");
}