Fix CSV export to include UTF-8 BOM.

This commit is contained in:
Buster "Silver Eagle" Neece 2022-05-19 22:53:53 -05:00
parent 910aa3b501
commit 41ac7bd810
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
7 changed files with 76 additions and 106 deletions

View File

@ -8,9 +8,8 @@ use App\Entity\Repository\CustomFieldRepository;
use App\Entity\Repository\StationPlaylistRepository;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Utilities\File;
use App\Service\CsvWriterTempFile;
use Doctrine\ORM\EntityManagerInterface;
use League\Csv\Writer;
use Psr\Http\Message\ResponseInterface;
class BulkDownloadAction
@ -46,9 +45,9 @@ class BulkDownloadAction
)->setParameter('storageLocation', $station->getMediaStorageLocation());
$filename = $station->getShortName() . '_all_media.csv';
$tempFile = File::generateTempPath($filename);
$csv = Writer::createFromPath($tempFile, 'w+');
$csv->setOutputBOM($csv::BOM_UTF8);
$tempFile = new CsvWriterTempFile();
$csv = $tempFile->getWriter();
/*
* NOTE: These field names should correspond with DB property names when converted into short_names.
@ -116,10 +115,6 @@ class BulkDownloadAction
$csv->insertOne($bodyRow);
}
try {
return $response->withFileDownload($tempFile, $filename, 'text/csv');
} finally {
@unlink($filename);
}
return $response->withFileDownload($tempFile->getTempPath(), $filename, 'text/csv');
}
}

View File

@ -10,12 +10,11 @@ use App\Environment;
use App\Http\Response;
use App\Http\ServerRequest;
use App\OpenApi;
use App\Utilities\File;
use App\Service\CsvWriterTempFile;
use Azura\DoctrineBatchUtils\ReadOnlyBatchIteratorAggregate;
use Carbon\CarbonImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use League\Csv\Writer;
use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface;
@ -150,22 +149,19 @@ class HistoryController
Query $query,
string $filename
): ResponseInterface {
$tempFile = File::generateTempPath($filename);
$tempFile = new CsvWriterTempFile();
$csv = $tempFile->getWriter();
$csv = Writer::createFromPath($tempFile, 'w+');
$csv->insertOne(
[
'Date',
'Time',
'Listeners',
'Delta',
'Track',
'Artist',
'Playlist',
'Streamer',
]
);
$csv->insertOne([
'Date',
'Time',
'Listeners',
'Delta',
'Track',
'Artist',
'Playlist',
'Streamer',
]);
/** @var Entity\SongHistory $sh */
foreach (ReadOnlyBatchIteratorAggregate::fromQuery($query, 100) as $sh) {
@ -196,6 +192,6 @@ class HistoryController
]);
}
return $response->withFileDownload($tempFile, $filename, 'text/csv');
return $response->withFileDownload($tempFile->getTempPath(), $filename, 'text/csv');
}
}

View File

@ -9,13 +9,12 @@ use App\Environment;
use App\Http\Response;
use App\Http\ServerRequest;
use App\OpenApi;
use App\Service\CsvWriterTempFile;
use App\Service\DeviceDetector;
use App\Service\IpGeolocation;
use App\Utilities\File;
use Carbon\CarbonImmutable;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use League\Csv\Writer;
use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface;
@ -207,9 +206,8 @@ class ListenersAction
array $listeners,
string $filename
): ResponseInterface {
$tempFile = File::generateTempPath($filename);
$csv = Writer::createFromPath($tempFile, 'w+');
$tempFile = new CsvWriterTempFile();
$csv = $tempFile->getWriter();
$tz = $station->getTimezoneObject();
@ -266,6 +264,6 @@ class ListenersAction
$csv->insertOne($exportRow);
}
return $response->withFileDownload($tempFile, $filename, 'text/csv');
return $response->withFileDownload($tempFile->getTempPath(), $filename, 'text/csv');
}
}

View File

@ -7,9 +7,8 @@ namespace App\Controller\Api\Stations\Reports;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Paginator;
use App\Service\CsvWriterTempFile;
use App\Sync\Task\RunAutomatedAssignmentTask;
use App\Utilities\File;
use League\Csv\Writer;
use Psr\Http\Message\ResponseInterface;
class PerformanceAction
@ -59,9 +58,8 @@ class PerformanceAction
array $reportData,
string $filename
): ResponseInterface {
$tempFile = File::generateTempPath($filename);
$csv = Writer::createFromPath($tempFile, 'w+');
$tempFile = new CsvWriterTempFile();
$csv = $tempFile->getWriter();
$csv->insertOne(
[
@ -95,6 +93,6 @@ class PerformanceAction
]);
}
return $response->withFileDownload($tempFile, $filename, 'text/csv');
return $response->withFileDownload($tempFile->getTempPath(), $filename, 'text/csv');
}
}

View File

@ -9,7 +9,6 @@ use Azura\Files\ExtendedFilesystemInterface;
use InvalidArgumentException;
use League\Flysystem\FileAttributes;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
final class Response extends \Slim\Http\Response
{
@ -80,36 +79,6 @@ final class Response extends \Slim\Http\Response
return parent::withJson($data, $status, $options, $depth);
}
/**
* Stream the contents of a file directly through to the response.
*
* @param string $file_path
* @param null $file_name
*
* @return static
*/
public function renderFile(string $file_path, $file_name = null): Response
{
set_time_limit(600);
if (null === $file_name) {
$file_name = basename($file_path);
}
$stream = $this->streamFactory->createStreamFromFile($file_path);
$response = $this->response
->withHeader('Pragma', 'public')
->withHeader('Expires', '0')
->withHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
->withHeader('Content-Type', mime_content_type($file_path) ?: '')
->withHeader('Content-Length', (string)filesize($file_path))
->withHeader('Content-Disposition', 'attachment; filename=' . $file_name)
->withBody($stream);
return new Response($response, $this->streamFactory);
}
/**
* Write a string of file data to the response as if it is a file for download.
*
@ -136,37 +105,6 @@ final class Response extends \Slim\Http\Response
return new Response($response, $this->streamFactory);
}
/**
* Write a stream to the response as if it is a file for download.
*
* @param StreamInterface $fileStream
* @param string $contentType
* @param string|null $fileName
*
* @return static
*/
public function renderStreamAsFile(
StreamInterface $fileStream,
string $contentType,
?string $fileName = null
): Response {
set_time_limit(600);
$response = $this->response
->withHeader('Pragma', 'public')
->withHeader('Expires', '0')
->withHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
->withHeader('Content-Type', $contentType);
if ($fileName !== null) {
$response = $response->withHeader('Content-Disposition', 'attachment; filename=' . $fileName);
}
$response = $response->withBody($fileStream);
return new Response($response, $this->streamFactory);
}
public function streamFilesystemFile(
ExtendedFilesystemInterface $filesystem,
string $path,

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Utilities\File;
use League\Csv\AbstractCsv;
use League\Csv\Writer;
class CsvWriterTempFile
{
protected string $tempPath;
protected Writer $writer;
public function __construct()
{
$this->tempPath = File::generateTempPath('temp_file.csv');
// Append UTF-8 BOM to temp file.
file_put_contents($this->tempPath, AbstractCsv::BOM_UTF8);
$this->writer = Writer::createFromPath($this->tempPath, 'a+');
}
public function getWriter(): Writer
{
return $this->writer;
}
public function getTempPath(): string
{
return $this->tempPath;
}
public function __destruct()
{
@unlink($this->tempPath);
}
}

View File

@ -3,6 +3,7 @@
namespace Functional;
use Codeception\Util\Shared\Asserts;
use League\Csv\Reader;
class Api_Stations_ReportsCest extends CestAbstract
{
@ -153,18 +154,21 @@ class Api_Stations_ReportsCest extends CestAbstract
$response = $I->grabResponse();
$responseCsv = str_getcsv($response);
$csvReader = Reader::createFromString($response);
$csvReader->setHeaderOffset(0);
$this->assertIsArray($responseCsv);
$csvHeaders = $csvReader->getHeader();
$this->assertIsArray($csvHeaders);
$this->assertTrue(
count($responseCsv) > 0,
count($csvHeaders) > 0,
'CSV is not empty'
);
foreach ($headerFields as $csvHeaderField) {
$this->assertContains(
$csvHeaderField,
$responseCsv,
$csvHeaders,
'CSV has header field'
);
}