getMessage(), $e->status, $e->errorCode); } $clientId = (int)$tenant['client_id']; $userId = (int)($tenant['client_user_id'] ?? 0); $tenantRole = (string)($tenant['role'] ?? 'editor'); $db = dbnToolsDb(); $input = dbnToolsJsonInput(200_000); $op = (string)($input['op'] ?? ''); $ids = $input['ids'] ?? []; if (!is_array($ids) || !$ids) { dbnToolsError('ids array is required.', 400, 'missing_ids'); } $ids = array_values(array_unique(array_filter(array_map('intval', $ids), fn($v) => $v > 0))); if (!$ids) { dbnToolsError('No valid ids.', 400, 'invalid_ids'); } if (count($ids) > 500) { dbnToolsError('Maximum 500 ids per bulk operation.', 422, 'too_many'); } $ph = implode(',', array_fill(0, count($ids), '?')); // Load + ACL-check $rows = $db->prepare("SELECT id, folder_id FROM client_documents WHERE client_id = ? AND id IN ({$ph}) AND deleted_at IS NULL"); $rows->execute(array_merge([$clientId], $ids)); $allowedIds = []; $perDoc = []; foreach ($rows->fetchAll() as $r) { $fid = $r['folder_id'] ? (int)$r['folder_id'] : 0; if (dbnDmsUserCanAccessFolder($fid ?: null, 'write', $clientId, $userId, $tenantRole)) { $allowedIds[] = (int)$r['id']; $perDoc[(int)$r['id']] = $fid; } } if (!$allowedIds) { dbnToolsError('No accessible documents in selection.', 403, 'forbidden'); } try { switch ($op) { case 'move': $result = opMove($db, $clientId, $userId, $tenantRole, $allowedIds, $input); break; case 'retag': $result = opRetag($db, $clientId, $userId, $allowedIds, $input); break; case 'recategorize': $result = opRecategorize($db, $clientId, $userId, $allowedIds, $input); break; case 'trash': $result = opTrash($db, $clientId, $userId, $allowedIds); break; case 'restore': $result = opRestore($db, $clientId, $userId, $allowedIds); break; default: dbnToolsError('Unknown op: ' . $op, 400, 'unknown_op'); } } catch (DbnToolsHttpException $e) { dbnToolsError($e->getMessage(), $e->status, $e->errorCode, $e->extra); } catch (Throwable $e) { error_log('[dbn-dms/bulk] ' . $e->getMessage()); dbnToolsError('Bulk operation failed.', 500, 'bulk_failed'); } dbnToolsRespond($result); function opMove(PDO $db, int $clientId, int $userId, string $tenantRole, array $ids, array $input): array { $dest = $input['folder_id'] ?? null; $destId = ($dest === null || $dest === '' || $dest === 'unassigned' || (int)$dest === 0) ? null : (int)$dest; if ($destId !== null && !dbnDmsUserCanAccessFolder($destId, 'write', $clientId, $userId, $tenantRole)) { dbnToolsError('Forbidden on destination folder.', 403, 'forbidden_dest'); } if ($destId !== null) { $check = $db->prepare('SELECT id FROM client_folders WHERE id = ? AND client_id = ? AND deleted_at IS NULL'); $check->execute([$destId, $clientId]); if (!$check->fetchColumn()) { dbnToolsError('Destination folder not found.', 404, 'folder_not_found'); } } $ph = implode(',', array_fill(0, count($ids), '?')); $stmt = $db->prepare("UPDATE client_documents SET folder_id = ?, updated_at = NOW() WHERE client_id = ? AND id IN ({$ph})"); $stmt->execute(array_merge([$destId, $clientId], $ids)); dbnDmsLogAudit($clientId, $userId ?: null, 'bulk_move', ['dest' => $destId, 'count' => count($ids), 'ids' => $ids]); return ['ok' => true, 'op' => 'move', 'affected' => $stmt->rowCount()]; } function opRetag(PDO $db, int $clientId, int $userId, array $ids, array $input): array { $mode = strtolower((string)($input['mode'] ?? 'replace')); if (!in_array($mode, ['replace','append','remove'], true)) { dbnToolsError('Invalid mode (replace|append|remove).', 422, 'invalid_mode'); } $raw = (string)($input['tags'] ?? ''); $newTags = array_values(array_filter(array_map('trim', explode(',', $raw)))); $newTags = array_map(fn($t) => substr($t, 0, 32), $newTags); $newTags = array_slice($newTags, 0, 20); if ($mode === 'replace') { $ph = implode(',', array_fill(0, count($ids), '?')); $stmt = $db->prepare("UPDATE client_documents SET tags = ?, updated_at = NOW() WHERE client_id = ? AND id IN ({$ph})"); $stmt->execute(array_merge([implode(',', $newTags), $clientId], $ids)); $affected = $stmt->rowCount(); } else { // Per-row merge. $affected = 0; $ph = implode(',', array_fill(0, count($ids), '?')); $cur = $db->prepare("SELECT id, tags FROM client_documents WHERE client_id = ? AND id IN ({$ph})"); $cur->execute(array_merge([$clientId], $ids)); $upd = $db->prepare("UPDATE client_documents SET tags = ?, updated_at = NOW() WHERE id = ? AND client_id = ?"); foreach ($cur->fetchAll() as $row) { $existing = array_values(array_filter(array_map('trim', explode(',', (string)$row['tags'])))); if ($mode === 'append') { $merged = array_values(array_unique(array_merge($existing, $newTags))); } else { // remove $merged = array_values(array_diff($existing, $newTags)); } $merged = array_slice($merged, 0, 20); $upd->execute([implode(',', $merged), (int)$row['id'], $clientId]); $affected++; } } dbnDmsLogAudit($clientId, $userId ?: null, 'bulk_retag', ['mode' => $mode, 'tags' => $newTags, 'count' => count($ids), 'ids' => $ids]); return ['ok' => true, 'op' => 'retag', 'mode' => $mode, 'affected' => $affected]; } function opRecategorize(PDO $db, int $clientId, int $userId, array $ids, array $input): array { $cat = strtolower(trim((string)($input['category'] ?? ''))); $cat = preg_replace('/[^a-z0-9\-_]/', '', $cat) ?: 'uncategorized'; $cat = substr($cat, 0, 50); $ph = implode(',', array_fill(0, count($ids), '?')); $stmt = $db->prepare("UPDATE client_documents SET category = ?, updated_at = NOW() WHERE client_id = ? AND id IN ({$ph})"); $stmt->execute(array_merge([$cat, $clientId], $ids)); dbnDmsLogAudit($clientId, $userId ?: null, 'bulk_recategorize', ['category' => $cat, 'count' => count($ids), 'ids' => $ids]); return ['ok' => true, 'op' => 'recategorize', 'category' => $cat, 'affected' => $stmt->rowCount()]; } function opTrash(PDO $db, int $clientId, int $userId, array $ids): array { $ph = implode(',', array_fill(0, count($ids), '?')); $stmt = $db->prepare( "UPDATE client_documents SET deleted_at = NOW(), deleted_by = ?, updated_at = NOW() WHERE client_id = ? AND id IN ({$ph}) AND deleted_at IS NULL" ); $stmt->execute(array_merge([$userId ?: null, $clientId], $ids)); dbnDmsLogAudit($clientId, $userId ?: null, 'bulk_trash', ['count' => count($ids), 'ids' => $ids]); return ['ok' => true, 'op' => 'trash', 'affected' => $stmt->rowCount()]; } function opRestore(PDO $db, int $clientId, int $userId, array $ids): array { $ph = implode(',', array_fill(0, count($ids), '?')); $stmt = $db->prepare( "UPDATE client_documents SET deleted_at = NULL, deleted_by = NULL, updated_at = NOW() WHERE client_id = ? AND id IN ({$ph}) AND deleted_at IS NOT NULL" ); $stmt->execute(array_merge([$clientId], $ids)); dbnDmsLogAudit($clientId, $userId ?: null, 'bulk_restore', ['count' => count($ids), 'ids' => $ids]); return ['ok' => true, 'op' => 'restore', 'affected' => $stmt->rowCount()]; }