createUserAccountService(); if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) !== 'POST') { Router::redirect('admin/users'); return; } $raw = $_POST['uuids'] ?? ''; $uuids = []; if (is_array($raw)) { $uuids = $raw; } elseif (is_string($raw)) { $uuids = explode(',', $raw); } $uuids = array_values(array_unique(array_filter(array_map('trim', $uuids)))); if (!$uuids) { Flash::error(t('No valid users selected'), 'admin/users', 'users_access_pdf_no_selection'); Router::redirect('admin/users'); return; } if (count($uuids) > 100) { // Server-side hard limit to keep synchronous ZIP generation predictable. Flash::error(t('Too many users selected'), 'admin/users', 'users_access_pdf_too_many'); Router::redirect('admin/users'); return; } $currentUserId = (int) ($_SESSION['user']['id'] ?? 0); $users = []; foreach ($uuids as $uuid) { $user = $userAccountService->findByUuid($uuid); if (!$user) { continue; } $userId = (int) ($user['id'] ?? 0); if ($userId <= 0) { continue; } // Scope check per user prevents cross-tenant exports in mixed selections. if (!directoryServicesFactory()->createDirectoryScopeGateway()->canAccess('users', $userId, $currentUserId)) { continue; } $users[] = $user; } if (!$users) { Flash::error(t('No valid users selected'), 'admin/users', 'users_access_pdf_not_allowed'); Router::redirect('admin/users'); return; } if (!class_exists(ZipArchive::class)) { // Explicit runtime dependency check for environments missing ext-zip. Flash::error(t('ZIP support missing (ext-zip)'), 'admin/users', 'users_access_pdf_zip_missing'); Router::redirect('admin/users'); return; } $zip = UserAccessPdfService::renderUsersAccessZip($users); $zipPath = (string) ($zip['path'] ?? ''); $zipFilename = trim((string) ($zip['filename'] ?? '')); $zipCount = (int) ($zip['count'] ?? 0); if ($zipPath === '' || !is_file($zipPath) || $zipCount <= 0) { Flash::error(t('Failed to generate access PDF'), 'admin/users', 'users_access_pdf_failed'); Router::redirect('admin/users'); return; } if ($zipFilename === '') { $zipFilename = 'user-invitations-' . gmdate('Ymd-His') . '.zip'; } try { header('Content-Type: application/zip'); header('Content-Disposition: attachment; filename="' . $zipFilename . '"'); header('Cache-Control: private, no-store, max-age=0'); header('Pragma: no-cache'); header('X-Content-Type-Options: nosniff'); header('Content-Length: ' . filesize($zipPath)); readfile($zipPath); } finally { // Always delete temporary archive, including partial-failure paths. @unlink($zipPath); }