Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/Exceptions/DrawioPngReaderException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace BookStack\Exceptions;

class DrawioPngReaderException extends \Exception
{
}
122 changes: 122 additions & 0 deletions app/Uploads/DrawioPngReader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php

namespace BookStack\Uploads;

use BookStack\Exceptions\DrawioPngReaderException;

/**
* Reads the PNG file format: https://www.w3.org/TR/2003/REC-PNG-20031110/
* So that it can extract embedded drawing data for alternative use.
*/
class DrawioPngReader
{
/**
* @param resource $fileStream
*/
public function __construct(
protected $fileStream
) {
}

/**
* @throws DrawioPngReaderException
*/
public function extractDrawing(): string
{
$signature = fread($this->fileStream, 8);
$pngSignature = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A";
if ($signature !== $pngSignature) {
throw new DrawioPngReaderException('File does not appear to be a valid PNG file');
}

$offset = 8;
$searching = true;

while ($searching) {
fseek($this->fileStream, $offset);

$lengthBytes = $this->readData(4);
$chunkTypeBytes = $this->readData(4);
$length = unpack('Nvalue', $lengthBytes)['value'];

if ($chunkTypeBytes === 'tEXt') {
fseek($this->fileStream, $offset + 8);
$data = $this->readData($length);
$crc = $this->readData(4);
$drawingData = $this->readTextForDrawing($data);
if ($drawingData !== null) {
$crcResult = $this->calculateCrc($chunkTypeBytes . $data);
if ($crc !== $crcResult) {
throw new DrawioPngReaderException('Drawing data withing PNG file appears to be corrupted');
}
return $drawingData;
}
} else if ($chunkTypeBytes === 'IEND') {
$searching = false;
}

$offset += 12 + $length; // 12 = length + type + crc bytes
}

throw new DrawioPngReaderException('Unable to find drawing data within PNG file');
}

protected function readTextForDrawing(string $data): ?string
{
// Check the keyword is mxfile to ensure we're getting the right data
if (!str_starts_with($data, "mxfile\u{0}")) {
return null;
}

// Extract & cleanup the drawing text
$drawingText = substr($data, 7);
return urldecode($drawingText);
}

protected function readData(int $length): string
{
$bytes = fread($this->fileStream, $length);
if ($bytes === false || strlen($bytes) < $length) {
throw new DrawioPngReaderException('Unable to find drawing data within PNG file');
}
return $bytes;
}

protected function getCrcTable(): array
{
$table = [];

for ($n = 0; $n < 256; $n++) {
$c = $n;
for ($k = 0; $k < 8; $k++) {
if ($c & 1) {
$c = 0xedb88320 ^ ($c >> 1);
} else {
$c = $c >> 1;
}
}
$table[$n] = $c;
}

return $table;
}

/**
* Calculate a CRC for the given bytes following:
* https://www.w3.org/TR/2003/REC-PNG-20031110/#D-CRCAppendix
*/
protected function calculateCrc(string $bytes): string
{
$table = $this->getCrcTable();

$length = strlen($bytes);
$c = 0xffffffff;

for ($n = 0; $n < $length; $n++) {
$tableIndex = ($c ^ ord($bytes[$n])) & 0xff;
$c = $table[$tableIndex] ^ ($c >> 8);
}

return pack('N', $c ^ 0xffffffff);
}
}
56 changes: 56 additions & 0 deletions tests/Uploads/DrawioPngReaderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace Tests\Uploads;

use BookStack\Exceptions\DrawioPngReaderException;
use BookStack\Uploads\DrawioPngReader;
use Tests\TestCase;

class DrawioPngReaderTest extends TestCase
{
public function test_exact_drawing()
{
$file = $this->files->testFilePath('test.drawio.png');
$stream = fopen($file, 'r');

$reader = new DrawioPngReader($stream);
$drawing = $reader->extractDrawing();

$this->assertStringStartsWith('<mxfile ', $drawing);
$this->assertStringEndsWith("</mxfile>\n", $drawing);
}

public function test_extract_drawing_with_non_drawing_image_throws_exception()
{
$file = $this->files->testFilePath('test-image.png');
$stream = fopen($file, 'r');
$reader = new DrawioPngReader($stream);

$exception = null;
try {
$drawing = $reader->extractDrawing();
} catch (\Exception $e) {
$exception = $e;
}

$this->assertInstanceOf(DrawioPngReaderException::class, $exception);
$this->assertEquals($exception->getMessage(), 'Unable to find drawing data within PNG file');
}

public function test_extract_drawing_with_non_png_image_throws_exception()
{
$file = $this->files->testFilePath('test-image.jpg');
$stream = fopen($file, 'r');
$reader = new DrawioPngReader($stream);

$exception = null;
try {
$drawing = $reader->extractDrawing();
} catch (\Exception $e) {
$exception = $e;
}

$this->assertInstanceOf(DrawioPngReaderException::class, $exception);
$this->assertEquals($exception->getMessage(), 'File does not appear to be a valid PNG file');
}
}
Binary file added tests/test-data/test.drawio.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.