105 lines
3.3 KiB
PHP
105 lines
3.3 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
use MintyPHP\Http\Input\RequestInput;
|
||
|
|
use MintyPHP\Service\Export\CsvExportService;
|
||
|
|
use MintyPHP\Service\Export\ExportColumn;
|
||
|
|
|
||
|
|
if (!function_exists('exportRequireGetRequest')) {
|
||
|
|
/**
|
||
|
|
* Enforce GET for export endpoints. Exports are stateless and must not
|
||
|
|
* start a write transaction — GET matches semantics, keeps them
|
||
|
|
* bookmarkable and CSRF-free. Mirrors `gridRequireGetRequest()` and
|
||
|
|
* reads the method through `requestInput()` for GR-CORE-003 compliance.
|
||
|
|
*/
|
||
|
|
function exportRequireGetRequest(): void
|
||
|
|
{
|
||
|
|
if (requestInput()->isMethod('GET')) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
header('Allow: GET');
|
||
|
|
http_response_code(405);
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!function_exists('exportResolveFlavor')) {
|
||
|
|
/**
|
||
|
|
* Reads `format=csv|excel` from the request. Unknown values fall
|
||
|
|
* back to 'csv'. Accepts legacy alias 'excel-csv'.
|
||
|
|
*/
|
||
|
|
function exportResolveFlavor(RequestInput $request, string $default = CsvExportService::FLAVOR_CSV): string
|
||
|
|
{
|
||
|
|
$raw = strtolower(trim((string) $request->query('format', $default)));
|
||
|
|
if ($raw === 'excel' || $raw === 'excel-csv' || $raw === 'xls') {
|
||
|
|
return CsvExportService::FLAVOR_EXCEL;
|
||
|
|
}
|
||
|
|
return CsvExportService::FLAVOR_CSV;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!function_exists('exportCapLimit')) {
|
||
|
|
/**
|
||
|
|
* Bounds a user-supplied limit to [1, $max]. Non-numeric / <=0
|
||
|
|
* values collapse to $max.
|
||
|
|
*/
|
||
|
|
function exportCapLimit(mixed $requested, int $max = 5000): int
|
||
|
|
{
|
||
|
|
$value = is_numeric($requested) ? (int) $requested : 0;
|
||
|
|
if ($value < 1 || $value > $max) {
|
||
|
|
return $max;
|
||
|
|
}
|
||
|
|
return $value;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!function_exists('exportSendCsv')) {
|
||
|
|
/**
|
||
|
|
* Streams rows as a CSV download and ends the response.
|
||
|
|
*
|
||
|
|
* IMPORTANT: this helper does NOT enforce authentication or authorization.
|
||
|
|
* Callers MUST run `Guard::requireLogin()` and the appropriate
|
||
|
|
* `Guard::requireAbilityOrForbidden(...)` check BEFORE invoking it —
|
||
|
|
* see `modules/helpdesk/pages/helpdesk/domains/export().php` for the
|
||
|
|
* reference pattern.
|
||
|
|
*
|
||
|
|
* @param iterable<array<string,mixed>> $rows
|
||
|
|
* @param list<ExportColumn> $columns
|
||
|
|
*/
|
||
|
|
function exportSendCsv(
|
||
|
|
string $filename,
|
||
|
|
iterable $rows,
|
||
|
|
array $columns,
|
||
|
|
string $flavor = CsvExportService::FLAVOR_CSV
|
||
|
|
): void {
|
||
|
|
$safeName = preg_replace('/[^A-Za-z0-9._-]+/', '-', $filename) ?: 'export.csv';
|
||
|
|
// Clamp total filename to 100 chars while preserving the .csv suffix.
|
||
|
|
if (strlen($safeName) > 100) {
|
||
|
|
$safeName = substr($safeName, 0, 96);
|
||
|
|
}
|
||
|
|
if (!str_ends_with(strtolower($safeName), '.csv')) {
|
||
|
|
$safeName .= '.csv';
|
||
|
|
}
|
||
|
|
|
||
|
|
while (ob_get_level() > 0) {
|
||
|
|
ob_end_clean();
|
||
|
|
}
|
||
|
|
|
||
|
|
header('Content-Type: text/csv; charset=utf-8');
|
||
|
|
header('Content-Disposition: attachment; filename="' . $safeName . '"');
|
||
|
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||
|
|
header('Pragma: no-cache');
|
||
|
|
header('X-Content-Type-Options: nosniff');
|
||
|
|
|
||
|
|
$handle = fopen('php://output', 'w');
|
||
|
|
if ($handle === false) {
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
|
||
|
|
$service = new CsvExportService();
|
||
|
|
$service->writeTo($handle, $rows, $columns, $flavor);
|
||
|
|
|
||
|
|
fclose($handle);
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
}
|