all(); define('MINTY_ALLOW_OUTPUT', true); // Hard gate: user must be logged in and explicitly allowed to generate access PDFs. Guard::requireLogin(); $authorizationService = app(\MintyPHP\Service\Access\AuthorizationService::class); $decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACCESS_PDF_BULK, [ 'actor_user_id' => (int) ($session['user']['id'] ?? 0), ]); if (!$decision->isAllowed()) { Router::redirect('error/forbidden'); return; } $userAccountService = app(\MintyPHP\Service\User\UserAccountService::class); $userAccessPdfService = app(UserAccessPdfService::class); if (strtoupper((string) (requestInput()->method())) !== 'POST') { Router::redirect('admin/users'); return; } if (!Session::checkCsrfToken()) { Flash::error(t('Form expired, please try again'), 'admin/users', 'csrf_expired'); Router::redirect('admin/users'); return; } $errorBag = formErrors(); $raw = requestInput()->bodyAll()['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) { $errorBag->addGlobal(t('No valid users selected')); flashFormErrors($errorBag, 'admin/users', 'users_access_pdf'); Router::redirect('admin/users'); return; } if (count($uuids) > 100) { // Server-side hard limit to keep synchronous ZIP generation predictable. $errorBag->addGlobal(t('Too many users selected')); flashFormErrors($errorBag, 'admin/users', 'users_access_pdf'); 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 (!app(\MintyPHP\Service\Tenant\TenantScopeService::class)->canAccess('users', $userId, $currentUserId)) { continue; } $users[] = $user; } if (!$users) { $errorBag->addGlobal(t('No valid users selected')); flashFormErrors($errorBag, 'admin/users', 'users_access_pdf'); Router::redirect('admin/users'); return; } if (!class_exists(ZipArchive::class)) { // Explicit runtime dependency check for environments missing ext-zip. $errorBag->addGlobal(t('ZIP support missing (ext-zip)')); flashFormErrors($errorBag, 'admin/users', 'users_access_pdf'); 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) { $errorBag->addGlobal(t('Failed to generate access PDF')); flashFormErrors($errorBag, 'admin/users', 'users_access_pdf'); 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); }