getMessage(), $e->status, $e->errorCode); } $clientId = (int)$tenant['client_id']; $userId = (int)($tenant['client_user_id'] ?? 0); $tenantRole = (string)($tenant['role'] ?? 'editor'); $db = dbnToolsDb(); $method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')); $action = (string)($_GET['action'] ?? ($method === 'POST' ? '' : 'list')); try { switch ($action) { case 'list': dbnToolsRequireMethod('GET'); listVersions($db, $clientId, $userId, $tenantRole); break; case 'get': dbnToolsRequireMethod('GET'); getVersion($db, $clientId, $userId, $tenantRole); break; case 'restore': dbnToolsRequireMethod('POST'); restoreVersion($db, $clientId, $userId, $tenantRole); break; case 'delete': dbnToolsRequireMethod('POST'); deleteVersion($db, $clientId, $userId, $tenantRole); break; default: dbnToolsError('Unknown action.', 400, 'unknown_action'); } } catch (DbnToolsHttpException $e) { dbnToolsError($e->getMessage(), $e->status, $e->errorCode, $e->extra); } catch (Throwable $e) { error_log('[dbn-dms/versions] ' . $e->getMessage()); dbnToolsError('Version operation failed.', 500, 'version_op_failed'); } function assertDocumentReadable(PDO $db, int $clientId, int $userId, string $tenantRole, int $docId): array { $stmt = $db->prepare('SELECT id, folder_id, title, current_version FROM client_documents WHERE id = ? AND client_id = ?'); $stmt->execute([$docId, $clientId]); $row = $stmt->fetch(); if (!$row) { dbnToolsError('Document not found.', 404, 'not_found'); } $fid = $row['folder_id'] ? (int)$row['folder_id'] : 0; if (!dbnDmsUserCanAccessFolder($fid ?: null, 'read', $clientId, $userId, $tenantRole)) { dbnToolsError('Forbidden.', 403, 'forbidden'); } return $row; } function listVersions(PDO $db, int $clientId, int $userId, string $tenantRole): void { $docId = (int)($_GET['document_id'] ?? 0); if ($docId <= 0) { dbnToolsError('document_id is required.', 400, 'missing_document_id'); } assertDocumentReadable($db, $clientId, $userId, $tenantRole, $docId); $stmt = $db->prepare( 'SELECT v.id, v.version_number, v.title, v.original_filename, v.file_size_bytes, v.word_count, v.notes, v.uploaded_by, v.created_at, u.email AS uploaded_email, u.full_name AS uploaded_name FROM client_document_versions v LEFT JOIN client_users u ON u.id = v.uploaded_by WHERE v.document_id = ? AND v.client_id = ? ORDER BY v.version_number DESC' ); $stmt->execute([$docId, $clientId]); dbnToolsRespond(['ok' => true, 'versions' => $stmt->fetchAll()]); } function getVersion(PDO $db, int $clientId, int $userId, string $tenantRole): void { $vid = (int)($_GET['version_id'] ?? 0); if ($vid <= 0) { dbnToolsError('version_id is required.', 400, 'missing_version_id'); } $stmt = $db->prepare( 'SELECT * FROM client_document_versions WHERE id = ? AND client_id = ?' ); $stmt->execute([$vid, $clientId]); $row = $stmt->fetch(); if (!$row) { dbnToolsError('Version not found.', 404, 'not_found'); } assertDocumentReadable($db, $clientId, $userId, $tenantRole, (int)$row['document_id']); dbnToolsRespond(['ok' => true, 'version' => $row]); } function restoreVersion(PDO $db, int $clientId, int $userId, string $tenantRole): void { $input = dbnToolsJsonInput(5_000); $docId = (int)($input['document_id'] ?? 0); $vid = (int)($input['version_id'] ?? 0); if ($docId <= 0 || $vid <= 0) { dbnToolsError('document_id and version_id are required.', 400, 'missing_args'); } $cur = assertDocumentReadable($db, $clientId, $userId, $tenantRole, $docId); $fid = $cur['folder_id'] ? (int)$cur['folder_id'] : 0; if (!dbnDmsUserCanAccessFolder($fid ?: null, 'write', $clientId, $userId, $tenantRole)) { dbnToolsError('Forbidden.', 403, 'forbidden'); } $vstmt = $db->prepare('SELECT * FROM client_document_versions WHERE id = ? AND document_id = ? AND client_id = ?'); $vstmt->execute([$vid, $docId, $clientId]); $ver = $vstmt->fetch(); if (!$ver) { dbnToolsError('Version not found.', 404, 'not_found'); } // Snapshot current state before restoring so we can roll-forward later. dbnDmsSnapshotVersion($docId, $clientId, $userId, "Snapshot before restoring v{$ver['version_number']}"); $next = (int)$db->query("SELECT current_version FROM client_documents WHERE id = {$docId}")->fetchColumn(); $next = max($next, (int)$ver['version_number']) + 1; $upd = $db->prepare( "UPDATE client_documents SET title = ?, content = ?, file_size_bytes = ?, word_count = ?, original_filename = ?, storage_path = ?, current_version = ?, status = 'pending', error_message = NULL, updated_at = NOW() WHERE id = ? AND client_id = ?" ); $upd->execute([ (string)$ver['title'], (string)$ver['content'], (int)$ver['file_size_bytes'], (int)$ver['word_count'], $ver['original_filename'], $ver['storage_path'], $next, $docId, $clientId, ]); try { $db->prepare('DELETE FROM client_chunks WHERE client_id = ? AND document_id = ?')->execute([$clientId, $docId]); } catch (Throwable $e) { /* tolerated */ } $chunks = 0; try { $rag = new ClientRagPipeline($clientId); $chunks = (int)$rag->ingestDocument($docId); } catch (Throwable $e) { $db->prepare("UPDATE client_documents SET status='error', error_message=? WHERE id=?") ->execute([substr($e->getMessage(), 0, 1000), $docId]); } dbnDmsLogAudit($clientId, $userId ?: null, 'restore_version', ['version_id' => $vid, 'restored_to' => $next], $docId, $fid ?: null); dbnToolsRespond([ 'ok' => true, 'document_id' => $docId, 'new_version_number' => $next, 'chunks' => $chunks, ]); } function deleteVersion(PDO $db, int $clientId, int $userId, string $tenantRole): void { $input = dbnToolsJsonInput(2_000); $vid = (int)($input['version_id'] ?? 0); if ($vid <= 0) { dbnToolsError('version_id is required.', 400, 'missing_version_id'); } $stmt = $db->prepare('SELECT document_id, storage_path FROM client_document_versions WHERE id = ? AND client_id = ?'); $stmt->execute([$vid, $clientId]); $ver = $stmt->fetch(); if (!$ver) { dbnToolsError('Version not found.', 404, 'not_found'); } $cur = assertDocumentReadable($db, $clientId, $userId, $tenantRole, (int)$ver['document_id']); $fid = $cur['folder_id'] ? (int)$cur['folder_id'] : 0; if (!dbnDmsUserCanAccessFolder($fid ?: null, 'write', $clientId, $userId, $tenantRole)) { dbnToolsError('Forbidden.', 403, 'forbidden'); } $del = $db->prepare('DELETE FROM client_document_versions WHERE id = ? AND client_id = ?'); $del->execute([$vid, $clientId]); if (!empty($ver['storage_path']) && is_file($ver['storage_path'])) { @unlink($ver['storage_path']); } dbnDmsLogAudit($clientId, $userId ?: null, 'delete_version', ['version_id' => $vid], (int)$ver['document_id']); dbnToolsRespond(['ok' => true]); }