Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
83.33% |
90 / 108 |
|
63.64% |
7 / 11 |
CRAP | |
0.00% |
0 / 1 |
| PdfWebController | |
83.18% |
89 / 107 |
|
63.64% |
7 / 11 |
44.87 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| download | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
| exportRecord | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
| bulkDownload | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
4.01 | |||
| plainTextResponse | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| checkRecordPermission | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
| generateDownloadResponse | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
| checkBulkRecordPermissions | |
22.22% |
2 / 9 |
|
0.00% |
0 / 1 |
22.94 | |||
| generateBulkResponse | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
| builder | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
2 | |||
| saveBlocks | |
93.10% |
27 / 29 |
|
0.00% |
0 / 1 |
10.03 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | /** @license For full copyright and license information, please see the LICENSE.md file. */ |
| 6 | |
| 7 | namespace App\Modules\Pdf\Presentation\Web; |
| 8 | |
| 9 | defined('AMMONLY_APP') || exit('Direct script access is forbidden.'); |
| 10 | |
| 11 | use App\Modules\Pdf\Application\Service\PdfGeneratorServiceInterface; |
| 12 | use App\Modules\Pdf\Domain\Repository\PdfTemplateRepositoryInterface; |
| 13 | use Psr\Http\Message\ResponseFactoryInterface; |
| 14 | use Psr\Http\Message\ResponseInterface; |
| 15 | use Psr\Http\Message\ServerRequestInterface; |
| 16 | use Throwable; |
| 17 | use Twig\Environment as TwigEnvironment; |
| 18 | |
| 19 | use App\Core\Database\FallbackPdoResolver; |
| 20 | use App\Core\Engine\Application\Security\PermissionContextFactory; |
| 21 | use App\Core\Engine\Application\Service\UniversalCrudService; |
| 22 | use App\Core\Template\Application\Service\TemplateCompilerService; |
| 23 | use PDO; |
| 24 | |
| 25 | /** |
| 26 | * Web Controller for Streaming and Downloading Generated PDF Documents. |
| 27 | * |
| 28 | * @package App\Modules\Pdf\Presentation\Web |
| 29 | */ |
| 30 | final readonly class PdfWebController |
| 31 | { |
| 32 | private const string HEADER_CACHE_NO_STORE = 'no-store, no-cache, must-revalidate, max-age=0'; |
| 33 | private const string CONTENT_TYPE_PLAIN = 'text/plain; charset=utf-8'; |
| 34 | private const string MSG_AUTH_REQUIRED = 'Uwierzytelnienie jest wymagane.'; |
| 35 | |
| 36 | private ?PDO $pdo; |
| 37 | |
| 38 | /** |
| 39 | * PdfWebController constructor. |
| 40 | * |
| 41 | * @param ResponseFactoryInterface $responseFactory PSR-17 response factory. |
| 42 | * @param PdfGeneratorServiceInterface $generator PDF generator service. |
| 43 | * @param TwigEnvironment|null $twig Twig template engine. |
| 44 | * @param PdfTemplateRepositoryInterface|null $repository Template repository. |
| 45 | * @param PDO|null $pdo Optional database handle. |
| 46 | * @param UniversalCrudService|null $crudService Optional CRUD service for permission checks. |
| 47 | * @param PermissionContextFactory|null $contextFactory Optional permission context factory. |
| 48 | */ |
| 49 | public function __construct( |
| 50 | private ResponseFactoryInterface $responseFactory, |
| 51 | private PdfGeneratorServiceInterface $generator, |
| 52 | private ?TwigEnvironment $twig = null, |
| 53 | private ?PdfTemplateRepositoryInterface $repository = null, |
| 54 | ?PDO $pdo = null, |
| 55 | private ?UniversalCrudService $crudService = null, |
| 56 | private ?PermissionContextFactory $contextFactory = null |
| 57 | ) { |
| 58 | $this->pdo = $pdo ?? FallbackPdoResolver::resolveDefaultConnection(); |
| 59 | } |
| 60 | |
| 61 | /** |
| 62 | * Handles GET /pdf/download/{templateId}/{recordId}?module={moduleName} |
| 63 | * |
| 64 | * @param ServerRequestInterface $request Incoming request. |
| 65 | * @param int $templateId Target template ID. |
| 66 | * @param int $recordId Primary record identifier. |
| 67 | * @return ResponseInterface Binary PDF file download stream. |
| 68 | */ |
| 69 | public function download( |
| 70 | ServerRequestInterface $request, |
| 71 | int $templateId, |
| 72 | int $recordId |
| 73 | ): ResponseInterface { |
| 74 | $queryParams = $request->getQueryParams(); |
| 75 | $module = (string)($queryParams['module'] ?? 'tickets'); |
| 76 | |
| 77 | $permError = $this->checkRecordPermission($request, $module, $recordId); |
| 78 | if ($permError !== null) { |
| 79 | return $permError; |
| 80 | } |
| 81 | |
| 82 | return $this->generateDownloadResponse($templateId, $module, $recordId); |
| 83 | } |
| 84 | |
| 85 | /** |
| 86 | * Handles GET /pdf/export/{moduleName}/{recordId} |
| 87 | * |
| 88 | * Finds active template for module and generates direct PDF download. |
| 89 | * |
| 90 | * @param ServerRequestInterface $request Incoming request. |
| 91 | * @param string $moduleName Target module name. |
| 92 | * @param int $recordId Target record ID. |
| 93 | * @return ResponseInterface Binary PDF file download stream. |
| 94 | */ |
| 95 | public function exportRecord( |
| 96 | ServerRequestInterface $request, |
| 97 | string $moduleName, |
| 98 | int $recordId |
| 99 | ): ResponseInterface { |
| 100 | $permError = $this->checkRecordPermission($request, $moduleName, $recordId); |
| 101 | if ($permError !== null) { |
| 102 | return $permError; |
| 103 | } |
| 104 | |
| 105 | $templates = $this->repository?->findAllActiveByModule($moduleName) ?? []; |
| 106 | if ($templates === []) { |
| 107 | return $this->plainTextResponse("No active PDF template found for module '{$moduleName}'.", 404); |
| 108 | } |
| 109 | |
| 110 | $template = $templates[0]; |
| 111 | return $this->generateDownloadResponse($template->id, $moduleName, $recordId); |
| 112 | } |
| 113 | |
| 114 | /** |
| 115 | * Handles POST /pdf/bulk-download |
| 116 | * |
| 117 | * @param ServerRequestInterface $request Incoming form POST request. |
| 118 | * @return ResponseInterface Binary consolidated PDF file download stream. |
| 119 | */ |
| 120 | public function bulkDownload(ServerRequestInterface $request): ResponseInterface |
| 121 | { |
| 122 | $body = (array)$request->getParsedBody(); |
| 123 | $templateId = (int)($body['template_id'] ?? 0); |
| 124 | $module = (string)($body['module_name'] ?? 'tickets'); |
| 125 | $rawIds = (string)($body['record_ids'] ?? ''); |
| 126 | $recordIds = array_values(array_filter(array_map('intval', explode(',', $rawIds)))); |
| 127 | |
| 128 | if ($templateId <= 0 || $recordIds === []) { |
| 129 | return $this->plainTextResponse('Invalid bulk download parameters.', 400); |
| 130 | } |
| 131 | |
| 132 | $permError = $this->checkBulkRecordPermissions($request, $module, $recordIds); |
| 133 | if ($permError !== null) { |
| 134 | return $permError; |
| 135 | } |
| 136 | |
| 137 | return $this->generateBulkResponse($templateId, $module, $recordIds); |
| 138 | } |
| 139 | /** |
| 140 | * Builds plain text error or info response. |
| 141 | * |
| 142 | * @param string $message Response text message. |
| 143 | * @param int $status HTTP status code. |
| 144 | * @return ResponseInterface Formatted plain text response. |
| 145 | */ |
| 146 | private function plainTextResponse(string $message, int $status): ResponseInterface |
| 147 | { |
| 148 | $response = $this->responseFactory->createResponse($status) |
| 149 | ->withHeader('Content-Type', self::CONTENT_TYPE_PLAIN); |
| 150 | $response->getBody()->write($message); |
| 151 | |
| 152 | return $response; |
| 153 | } |
| 154 | |
| 155 | /** |
| 156 | * Verifies read permission for a single record. |
| 157 | * |
| 158 | * @param ServerRequestInterface $request Incoming request. |
| 159 | * @param string $module Module name. |
| 160 | * @param int $recordId Record ID. |
| 161 | * @return ResponseInterface|null Null if allowed, or error response. |
| 162 | */ |
| 163 | private function checkRecordPermission( |
| 164 | ServerRequestInterface $request, |
| 165 | string $module, |
| 166 | int $recordId |
| 167 | ): ?ResponseInterface { |
| 168 | if ($this->crudService !== null && $this->contextFactory !== null) { |
| 169 | $context = $this->contextFactory->createFromRequest($request); |
| 170 | if (!$context->isAuthenticated()) { |
| 171 | return $this->plainTextResponse(self::MSG_AUTH_REQUIRED, 401); |
| 172 | } |
| 173 | |
| 174 | try { |
| 175 | $this->crudService->read($module, $recordId, $context); |
| 176 | } catch (Throwable) { |
| 177 | return $this->plainTextResponse('Permission denied for the requested record.', 403); |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | return null; |
| 182 | } |
| 183 | |
| 184 | /** |
| 185 | * Generates PDF binary download response. |
| 186 | * |
| 187 | * @param int $templateId Template ID. |
| 188 | * @param string $module Module name. |
| 189 | * @param int $recordId Record ID. |
| 190 | * @return ResponseInterface Binary PDF response. |
| 191 | */ |
| 192 | private function generateDownloadResponse(int $templateId, string $module, int $recordId): ResponseInterface |
| 193 | { |
| 194 | try { |
| 195 | $pdf = $this->generator->generateForRecord($templateId, $module, $recordId); |
| 196 | |
| 197 | $response = $this->responseFactory->createResponse(200) |
| 198 | ->withHeader('Content-Type', 'application/pdf') |
| 199 | ->withHeader('Content-Disposition', 'attachment; filename="' . $pdf['filename'] . '"') |
| 200 | ->withHeader('Cache-Control', self::HEADER_CACHE_NO_STORE); |
| 201 | $response->getBody()->write($pdf['content']); |
| 202 | |
| 203 | return $response; |
| 204 | } catch (Throwable $e) { |
| 205 | return $this->plainTextResponse('Error generating PDF document: ' . $e->getMessage(), 500); |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | /** |
| 210 | * Verifies read permission for multiple records. |
| 211 | * |
| 212 | * @param ServerRequestInterface $request Incoming request. |
| 213 | * @param string $module Module name. |
| 214 | * @param list<int> $recordIds Record IDs. |
| 215 | * @return ResponseInterface|null Null if allowed, or error response. |
| 216 | */ |
| 217 | private function checkBulkRecordPermissions( |
| 218 | ServerRequestInterface $request, |
| 219 | string $module, |
| 220 | array $recordIds |
| 221 | ): ?ResponseInterface { |
| 222 | if ($this->crudService !== null && $this->contextFactory !== null) { |
| 223 | $context = $this->contextFactory->createFromRequest($request); |
| 224 | if (!$context->isAuthenticated()) { |
| 225 | return $this->plainTextResponse(self::MSG_AUTH_REQUIRED, 401); |
| 226 | } |
| 227 | |
| 228 | try { |
| 229 | foreach ($recordIds as $rid) { |
| 230 | $this->crudService->read($module, $rid, $context); |
| 231 | } |
| 232 | } catch (Throwable) { |
| 233 | return $this->plainTextResponse('Permission denied for one or more records.', 403); |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | return null; |
| 238 | } |
| 239 | |
| 240 | /** |
| 241 | * Generates bulk PDF binary download response. |
| 242 | * |
| 243 | * @param int $templateId Template ID. |
| 244 | * @param string $module Module name. |
| 245 | * @param list<int> $recordIds Record IDs. |
| 246 | * @return ResponseInterface Binary consolidated PDF response. |
| 247 | */ |
| 248 | private function generateBulkResponse(int $templateId, string $module, array $recordIds): ResponseInterface |
| 249 | { |
| 250 | try { |
| 251 | $pdf = $this->generator->generateBulk($templateId, $module, $recordIds); |
| 252 | |
| 253 | $response = $this->responseFactory->createResponse(200) |
| 254 | ->withHeader('Content-Type', 'application/pdf') |
| 255 | ->withHeader('Content-Disposition', 'attachment; filename="' . $pdf['filename'] . '"') |
| 256 | ->withHeader('Cache-Control', self::HEADER_CACHE_NO_STORE); |
| 257 | $response->getBody()->write($pdf['content']); |
| 258 | |
| 259 | return $response; |
| 260 | } catch (Throwable $e) { |
| 261 | return $this->plainTextResponse('Error generating bulk PDF: ' . $e->getMessage(), 500); |
| 262 | } |
| 263 | } |
| 264 | |
| 265 | /** |
| 266 | * Handles GET /pdf/builder/{templateId} |
| 267 | * |
| 268 | * @param int $templateId Target template ID. |
| 269 | * @return ResponseInterface Rendered visual block builder view. |
| 270 | */ |
| 271 | public function builder(int $templateId): ResponseInterface |
| 272 | { |
| 273 | if ($this->twig === null) { |
| 274 | return $this->plainTextResponse('Twig template engine is not configured.', 500); |
| 275 | } |
| 276 | |
| 277 | $template = $this->repository?->findById($templateId); |
| 278 | $html = $this->twig->render('modules/pdf_templates/builder.twig', [ |
| 279 | 'template' => $template, |
| 280 | 'is_email' => false, |
| 281 | 'save_url' => '/pdf/templates/save-blocks/' . $templateId, |
| 282 | 'return_url' => '/pdf_templates/' . $templateId, |
| 283 | ]); |
| 284 | |
| 285 | $response = $this->responseFactory->createResponse(200) |
| 286 | ->withHeader('Content-Type', 'text/html; charset=utf-8'); |
| 287 | $response->getBody()->write($html); |
| 288 | |
| 289 | return $response; |
| 290 | } |
| 291 | |
| 292 | /** |
| 293 | * Handles POST /pdf/templates/save-blocks/{templateId} |
| 294 | * |
| 295 | * @param ServerRequestInterface $request Incoming request. |
| 296 | * @param int $templateId Target template ID. |
| 297 | * @return ResponseInterface Redirect to template detail view. |
| 298 | */ |
| 299 | public function saveBlocks(ServerRequestInterface $request, int $templateId): ResponseInterface |
| 300 | { |
| 301 | if ($this->crudService !== null && $this->contextFactory !== null) { |
| 302 | $context = $this->contextFactory->createFromRequest($request); |
| 303 | if (!$context->isAuthenticated()) { |
| 304 | return $this->plainTextResponse(self::MSG_AUTH_REQUIRED, 401); |
| 305 | } |
| 306 | |
| 307 | try { |
| 308 | $modMeta = $this->crudService->getMetadataRepository()->findModule('pdf_templates'); |
| 309 | $this->crudService->getGuard()->assertWriteAccess($modMeta, $context); |
| 310 | } catch (Throwable) { |
| 311 | return $this->plainTextResponse('Permission denied to edit PDF templates.', 403); |
| 312 | } |
| 313 | } |
| 314 | |
| 315 | $body = $request->getParsedBody(); |
| 316 | $data = is_array($body) ? $body : []; |
| 317 | |
| 318 | $rawBlocks = (string)($data['blocks_json'] ?? ''); |
| 319 | $cssStyles = (string)($data['css_styles'] ?? ''); |
| 320 | |
| 321 | $compiler = new TemplateCompilerService(); |
| 322 | $compiledHtml = $rawBlocks !== '' ? $compiler->compileBlocks($rawBlocks) : (string)($data['body_html'] ?? ''); |
| 323 | |
| 324 | if ($this->pdo !== null) { |
| 325 | $stmt = $this->pdo->prepare( |
| 326 | 'UPDATE `a_mod_pdf_template_records` SET `blocks_json` = :blocks, `body_html` = :html, ' |
| 327 | . '`css_styles` = :css, `updated_at` = CURRENT_TIMESTAMP WHERE `id` = :id' |
| 328 | ); |
| 329 | $stmt->execute([ |
| 330 | ':blocks' => $rawBlocks !== '' ? $rawBlocks : null, |
| 331 | ':html' => $compiledHtml, |
| 332 | ':css' => $cssStyles !== '' ? $cssStyles : null, |
| 333 | ':id' => $templateId, |
| 334 | ]); |
| 335 | } |
| 336 | |
| 337 | $returnUrl = (string)($data['return_url'] ?? ('/pdf_templates/' . $templateId)); |
| 338 | |
| 339 | return $this->responseFactory->createResponse(302) |
| 340 | ->withHeader('Location', $returnUrl) |
| 341 | ->withHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0'); |
| 342 | } |
| 343 | } |