PDO::ERRMODE_EXCEPTION] ); } catch (PDOException $e) { die("DB Connection failed: " . $e->getMessage()); } // GET a single row by Id to compare before/after function get_row($pdo, $table, $pk, $company) { $stmt = $pdo->prepare("SELECT * FROM `$table` WHERE Id = :id AND company = :company"); $stmt->execute([':id' => $pk, ':company' => $company]); return $stmt->fetch(PDO::FETCH_ASSOC) ?: []; } // PATCH a single row by Id function update_row($pdo, $table, $pk, $data, $company) { if (empty($data)) return ['ok' => true]; $set = []; $params = [':id' => $pk, ':company' => $company]; foreach ($data as $col => $val) { $set[] = "`$col` = :$col"; $params[":$col"] = $val; } $sql = "UPDATE `$table` SET " . implode(', ', $set) . " WHERE Id = :id AND company = :company"; try { $stmt = $pdo->prepare($sql); $stmt->execute($params); return ['ok' => true]; } catch (PDOException $e) { return ['error' => $e->getMessage()]; } } // INSERT a single audit log (Optional: create a logs table if you want to keep this feature) // For now, we'll skip logging or just log to a file/table if requested. // The user didn't explicitly ask for a logs table migration, but NocoDB had it. // Let's assume we skip it or log to error_log for now to save complexity, // or create a simple logs table if we want to be thorough. // Given the prompt "add collumns to manage S3 files", logging wasn't the main point. // I'll comment it out or implement a simple local log. function insert_log($pdo, $email, $field, $newValue, $uuid, $dateTimeIsoUtc) { // Check if logs table exists, if not create it? Or just ignore. // Let's just return ok to not break the flow. return ['ok' => true]; } // Normalize booleans, dates, email, and empty strings consistently with your grid behavior. function normalize_update_payload(array $input): array { $updateData = $input; // Remove immutable/PK fields from update payload unset($updateData['Id']); // Validate email if provided if (array_key_exists('UserEmail', $updateData)) { $emailVal = trim((string) ($updateData['UserEmail'] ?? '')); if ($emailVal !== '' && !filter_var($emailVal, FILTER_VALIDATE_EMAIL)) { $updateData['__invalid_email__'] = $emailVal; } else { $updateData['UserEmail'] = ($emailVal === '') ? null : $emailVal; } } // Normalize BYOD to boolean if present if (array_key_exists('BYOD', $updateData)) { $val = strtolower((string) $updateData['BYOD']); $updateData['BYOD'] = ($val === 'true' || $val === '1' || $val === 'on' || $val === 'yes') ? 1 : 0; } // Convert empty date/asset strings to null foreach (['Warranty', 'PurchaseDate', 'Asset'] as $field) { if (array_key_exists($field, $updateData) && $updateData[$field] === '') { $updateData[$field] = null; } } // Ensure immutable are not unintentionally nulled foreach (['UUID', 'CypherID', 'CypherKey'] as $immutable) { if (array_key_exists($immutable, $updateData) && ($updateData[$immutable] === '' || $updateData[$immutable] === null)) { unset($updateData[$immutable]); } } return $updateData; } // Strict comparison helper with type normalization for DB values function values_equal($a, $b): bool { // Normalize boolean-ish strings $boolStrings = ['true' => true, 'false' => false, '1' => true, '0' => false, 'yes' => true, 'no' => false, '' => null]; if (is_string($a) && array_key_exists(strtolower($a), $boolStrings)) $a = $boolStrings[strtolower($a)]; if (is_string($b) && array_key_exists(strtolower($b), $boolStrings)) $b = $boolStrings[strtolower($b)]; // Normalize numeric strings if (is_string($a) && is_numeric($a)) $a = $a + 0; if (is_string($b) && is_numeric($b)) $b = $b + 0; // Treat empty string and null as equal for nullable fields if ($a === '' && $b === null) return true; if ($b === '' && $a === null) return true; // For arrays/objects (e.g., attachments), compare JSON representations if ((is_array($a) || is_object($a)) || (is_array($b) || is_object($b))) { return json_encode($a, JSON_UNESCAPED_SLASHES) === json_encode($b, JSON_UNESCAPED_SLASHES); } return $a === $b; } $errors = []; foreach ($rows as $index => $row) { $pk = $row['Id'] ?? ''; $uuidForLog = $row['UUID'] ?? ''; // taken from the hidden input in the form if (!$pk) { $errors[] = "Missing Id in one row, skipping update."; continue; } if ($uuidForLog === '') { // Proceed but note missing UUID so you can diagnose later $uuidForLog = ''; } // Normalize incoming payload $updateData = normalize_update_payload($row); // Invalid email? if (isset($updateData['__invalid_email__'])) { $errors[] = "Invalid email address on record Id $pk: " . htmlspecialchars($updateData['__invalid_email__']); continue; } // Which fields were submitted (touched) $submittedFields = array_keys($updateData); if (empty($submittedFields)) { continue; // nothing to do } // Fetch current row (Before) to compare $before = get_row($pdo, $userTableName, $pk, $company); if (isset($before['error'])) { $errors[] = "Error fetching current row Id $pk: " . $before['error']; continue; } // Build minimal PATCH payload with only truly changed fields $changedPayload = []; $changedFields = []; foreach ($submittedFields as $fieldName) { $newVal = $updateData[$fieldName]; $oldVal = $before[$fieldName] ?? null; if (!values_equal($oldVal, $newVal)) { $changedPayload[$fieldName] = $newVal; $changedFields[$fieldName] = $newVal; } } if (empty($changedPayload)) { // No real changes vs DB; skip patch and logging continue; } // Patch only changed fields $result = update_row($pdo, $userTableName, $pk, $changedPayload, $company); if (isset($result['error'])) { $errors[] = "Error updating Id $pk: " . $result['error']; continue; } // Log only actual changed fields with current UTC timestamp and the record's UUID $nowUtc = (new DateTime('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z'); foreach ($changedFields as $fieldName => $newValue) { // Optional: skip sensitive fields from logging if (in_array($fieldName, ['CypherKey'], true)) { continue; } $logResp = insert_log($pdo, $userEmail, $fieldName, $newValue, $uuidForLog, $nowUtc); if (isset($logResp['error'])) { $errors[] = "Log write failed for Id $pk, field $fieldName: " . $logResp['error']; } } } if (!empty($errors)) { echo "

Completed with notices:

Back"; exit(); } header('Location: main.php'); exit();