Skip to content

Commit 056dce7

Browse files
Merge pull request phpmyadmin#20208 from MoonE/gis-file-download
Rework GIS file export
2 parents ab773cb + 7778e3a commit 056dce7

8 files changed

Lines changed: 92 additions & 122 deletions

File tree

phpstan-baseline.neon

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2865,12 +2865,6 @@ parameters:
28652865
count: 3
28662866
path: src/Controllers/Table/GisVisualizationController.php
28672867

2868-
-
2869-
message: '#^Parameter \#2 \$format of method PhpMyAdmin\\Gis\\GisVisualization\:\:toFile\(\) expects string, mixed given\.$#'
2870-
identifier: argument.type
2871-
count: 1
2872-
path: src/Controllers/Table/GisVisualizationController.php
2873-
28742868
-
28752869
message: '#^Cannot access offset ''handler'' on mixed\.$#'
28762870
identifier: offsetAccess.nonOffsetAccessible

psalm-baseline.xml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2198,12 +2198,6 @@
21982198
<code><![CDATA[$_SESSION['tmpval']['max_rows']]]></code>
21992199
<code><![CDATA[$_SESSION['tmpval']['pos']]]></code>
22002200
</MixedArrayAccess>
2201-
<PossiblyInvalidArgument>
2202-
<code><![CDATA[$_GET['fileFormat']]]></code>
2203-
</PossiblyInvalidArgument>
2204-
<PossiblyInvalidCast>
2205-
<code><![CDATA[$_GET['fileFormat']]]></code>
2206-
</PossiblyInvalidCast>
22072201
<RiskyCast>
22082202
<code><![CDATA[$_POST['pos'] ?? $_GET['pos'] ?? $_SESSION['tmpval']['pos']]]></code>
22092203
<code><![CDATA[$_POST['session_max_rows'] ?? $_GET['session_max_rows']]]></code>

src/Controllers/Table/GisVisualizationController.php

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace PhpMyAdmin\Controllers\Table;
66

7+
use Fig\Http\Message\StatusCodeInterface;
78
use PhpMyAdmin\Config;
89
use PhpMyAdmin\Controllers\InvocableController;
910
use PhpMyAdmin\Core;
@@ -34,11 +35,11 @@
3435
use function assert;
3536
use function class_exists;
3637
use function extension_loaded;
38+
use function implode;
3739
use function in_array;
3840
use function is_array;
3941
use function is_string;
40-
use function ob_get_clean;
41-
use function ob_start;
42+
use function strlen;
4243

4344
/**
4445
* Handles creation of the GIS visualizations.
@@ -112,14 +113,28 @@ public function __invoke(ServerRequest $request): Response
112113

113114
$downloadOptions = $this->getDownloadOptions();
114115

115-
if (isset($_GET['saveToFile'])) {
116+
if ($request->hasQueryParam('saveToFile')) {
117+
$fileFormat = $request->getQueryParam('fileFormat');
118+
if (! in_array($fileFormat, $downloadOptions, true)) {
119+
return $this->responseFactory->createResponse(
120+
StatusCodeInterface::STATUS_BAD_REQUEST,
121+
__('Invalid file format, expected one of: ') . implode(', ', $downloadOptions),
122+
);
123+
}
124+
125+
$data = $visualization->toFile($fileFormat);
126+
if ($data === null) {
127+
return $this->responseFactory->createResponse(
128+
StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR,
129+
__('Failed to create image'),
130+
);
131+
}
132+
133+
$fileName = $visualization->getSpatialColumn() . '.' . $data->extension;
116134
$response = $this->responseFactory->createResponse();
117-
$filename = $visualization->getSpatialColumn();
118-
ob_start();
119-
$visualization->toFile($filename, $_GET['fileFormat']);
120-
$output = ob_get_clean();
135+
Core::downloadHeader($fileName, $data->mime, strlen($data->blob));
121136

122-
return $response->write((string) $output);
137+
return $response->write($data->blob);
123138
}
124139

125140
$this->response->addScriptFiles(['vendor/openlayers/openlayers.js', 'table/gis_visualization.js']);

src/Gis/Ds/Extent.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@
99

1010
use const INF;
1111

12-
class Extent
12+
final readonly class Extent
1313
{
1414
public static function empty(): Extent
1515
{
1616
return new Extent(minX: +INF, minY: +INF, maxX: -INF, maxY: -INF);
1717
}
1818

1919
public function __construct(
20-
public readonly float $minX,
21-
public readonly float $minY,
22-
public readonly float $maxX,
23-
public readonly float $maxY,
20+
public float $minX,
21+
public float $minY,
22+
public float $maxX,
23+
public float $maxY,
2424
) {
2525
}
2626

src/Gis/Ds/FileDownload.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMyAdmin\Gis\Ds;
6+
7+
final readonly class FileDownload
8+
{
9+
public function __construct(
10+
public string $blob,
11+
public string $mime,
12+
public string $extension,
13+
) {
14+
}
15+
}

src/Gis/Ds/ScaleData.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
namespace PhpMyAdmin\Gis\Ds;
66

7-
class ScaleData
7+
final readonly class ScaleData
88
{
99
public function __construct(
10-
public readonly float $scale,
11-
public readonly float $offsetX,
12-
public readonly float $offsetY,
13-
public readonly int $height,
10+
public float $scale,
11+
public float $offsetX,
12+
public float $offsetY,
13+
public int $height,
1414
) {
1515
}
1616
}

src/Gis/GisVisualization.php

Lines changed: 29 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@
88
namespace PhpMyAdmin\Gis;
99

1010
use PhpMyAdmin\Config;
11-
use PhpMyAdmin\Core;
1211
use PhpMyAdmin\Dbal\DatabaseInterface;
1312
use PhpMyAdmin\Gis\Ds\Extent;
13+
use PhpMyAdmin\Gis\Ds\FileDownload;
1414
use PhpMyAdmin\Gis\Ds\ScaleData;
1515
use PhpMyAdmin\Image\ImageWrapper;
16-
use PhpMyAdmin\Sanitize;
1716
use PhpMyAdmin\Util;
1817
use TCPDF;
1918

@@ -22,9 +21,8 @@
2221
use function htmlspecialchars;
2322
use function is_string;
2423
use function max;
25-
use function mb_strlen;
26-
use function mb_strtolower;
27-
use function mb_substr;
24+
use function ob_get_clean;
25+
use function ob_start;
2826
use function rtrim;
2927
use function trim;
3028

@@ -246,43 +244,6 @@ private function fetchRawData(string $modifiedSql): array
246244
return $modifiedResult->fetchAllAssoc();
247245
}
248246

249-
/**
250-
* Sanitizes the file name.
251-
*
252-
* @param string $fileName file name
253-
* @param string $ext extension of the file
254-
*
255-
* @return string the sanitized file name
256-
*/
257-
private function sanitizeName(string $fileName, string $ext): string
258-
{
259-
$fileName = Sanitize::sanitizeFilename($fileName);
260-
261-
// Check if the user already added extension;
262-
// get the substring where the extension would be if it was included
263-
$requiredExtension = '.' . $ext;
264-
$extensionLength = mb_strlen($requiredExtension);
265-
$userExtension = mb_substr($fileName, -$extensionLength);
266-
if (mb_strtolower($userExtension) !== $requiredExtension) {
267-
$fileName .= $requiredExtension;
268-
}
269-
270-
return $fileName;
271-
}
272-
273-
/**
274-
* Handles common tasks of writing the visualization to file for various formats.
275-
*
276-
* @param string $fileName file name
277-
* @param string $type mime type
278-
* @param string $ext extension of the file
279-
*/
280-
private function writeToFile(string $fileName, string $type, string $ext): void
281-
{
282-
$fileName = $this->sanitizeName($fileName, $ext);
283-
Core::downloadHeader($fileName, $type);
284-
}
285-
286247
/**
287248
* Generate the visualization in SVG format.
288249
*
@@ -312,16 +273,10 @@ public function asSVG(): string
312273
return $this->svg();
313274
}
314275

315-
/**
316-
* Saves as a SVG image to a file.
317-
*
318-
* @param string $fileName File name
319-
*/
320-
public function toFileAsSvg(string $fileName): void
276+
/** Get SVG image as string + type infos. */
277+
private function asSvgFile(): FileDownload
321278
{
322-
$img = $this->svg();
323-
$this->writeToFile($fileName, 'image/svg+xml', 'svg');
324-
echo $img;
279+
return new FileDownload(mime: 'image/svg+xml', extension: 'svg', blob: $this->svg());
325280
}
326281

327282
/**
@@ -345,20 +300,22 @@ private function png(): ImageWrapper|null
345300
return $image;
346301
}
347302

348-
/**
349-
* Saves as a PNG image to a file.
350-
*
351-
* @param string $fileName File name
352-
*/
353-
public function toFileAsPng(string $fileName): void
303+
/** Get PNG image as string (blob) + type infos. */
304+
private function asPngFile(): FileDownload|null
354305
{
355306
$image = $this->png();
356307
if ($image === null) {
357-
return;
308+
return null;
309+
}
310+
311+
ob_start();
312+
$ok = $image->png(null, 9, PNG_ALL_FILTERS);
313+
$output = ob_get_clean();
314+
if (! $ok || $output === false) {
315+
return null;
358316
}
359317

360-
$this->writeToFile($fileName, 'image/png', 'png');
361-
$image->png(null, 9, PNG_ALL_FILTERS);
318+
return new FileDownload(mime: 'image/png', extension: 'png', blob: $output);
362319
}
363320

364321
/**
@@ -371,18 +328,15 @@ public function asOl(): array
371328
return $this->prepareDataSet($this->data, 'ol');
372329
}
373330

374-
/**
375-
* Saves as a PDF to a file.
376-
*
377-
* @param string $fileName File name
378-
*/
379-
public function toFileAsPdf(string $fileName): void
331+
/** Get image PDF as string (blob) + type infos. */
332+
private function asPdfFile(): FileDownload
380333
{
381-
$fileName = $this->sanitizeName($fileName, 'pdf');
382334
$pdf = $this->createEmptyPdf(Config::getInstance()->config->PDFDefaultPageSize ?? 'A4');
383335
$this->prepareDataSet($this->data, 'pdf', $pdf);
384336

385-
$pdf->Output($fileName, 'D');
337+
$blob = $pdf->Output('', 'S');
338+
339+
return new FileDownload(mime: 'application/pdf', extension: 'pdf', blob: $blob);
386340
}
387341

388342
private function createEmptyPdf(string $format): TCPDF
@@ -406,18 +360,15 @@ private function createEmptyPdf(string $format): TCPDF
406360
/**
407361
* Convert file to given format
408362
*
409-
* @param string $filename Filename
410-
* @param string $format Output format
363+
* @param 'svg'|'png'|'pdf' $format Output format
411364
*/
412-
public function toFile(string $filename, string $format): void
365+
public function toFile(string $format): FileDownload|null
413366
{
414-
if ($format === 'svg') {
415-
$this->toFileAsSvg($filename);
416-
} elseif ($format === 'png') {
417-
$this->toFileAsPng($filename);
418-
} elseif ($format === 'pdf') {
419-
$this->toFileAsPdf($filename);
420-
}
367+
return match ($format) {
368+
'svg' => $this->asSvgFile(),
369+
'png' => $this->asPngFile(),
370+
'pdf' => $this->asPdfFile(),
371+
};
421372
}
422373

423374
/**

tests/unit/Controllers/Table/GisVisualizationControllerTest.php

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use PhpMyAdmin\Url;
2121
use PhpMyAdmin\UrlParams;
2222
use PHPUnit\Framework\Attributes\CoversClass;
23+
use ReflectionMethod;
2324

2425
use const MYSQLI_TYPE_GEOMETRY;
2526
use const MYSQLI_TYPE_VAR_STRING;
@@ -79,6 +80,19 @@ public function testGisVisualizationController(): void
7980

8081
$config = new Config();
8182
$template = new Template($config);
83+
$responseRenderer = new ResponseRenderer();
84+
$controller = new GisVisualizationController(
85+
$responseRenderer,
86+
$template,
87+
$dbi,
88+
new DbTableExists($dbi),
89+
ResponseFactory::create(),
90+
$config,
91+
);
92+
93+
/** @var list<string> $downloadOptions */
94+
$downloadOptions = (new ReflectionMethod(GisVisualizationController::class, 'getDownloadOptions'))
95+
->invoke($controller);
8296
$expected = $template->render('table/gis_visualization/gis_visualization', [
8397
'url_params' => $params,
8498
'download_url' => $downloadUrl,
@@ -110,25 +124,12 @@ public function testGisVisualizationController(): void
110124
],
111125
],
112126
],
113-
'download_options' => [
114-
'pdf',
115-
'png',
116-
'svg',
117-
],
127+
'download_options' => $downloadOptions,
118128
]);
119129

120130
$request = ServerRequestFactory::create()->createServerRequest('POST', 'http://example.com/')
121131
->withQueryParams(['db' => 'test_db', 'table' => 'test_table']);
122132

123-
$responseRenderer = new ResponseRenderer();
124-
$controller = new GisVisualizationController(
125-
$responseRenderer,
126-
$template,
127-
$dbi,
128-
new DbTableExists($dbi),
129-
ResponseFactory::create(),
130-
$config,
131-
);
132133
$response = $controller($request);
133134

134135
self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode());

0 commit comments

Comments
 (0)