mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-24 11:19:38 +03:00
Checks files within the ZIP again the app upload file limit before using/streaming/extracting, to help ensure that they do no exceed what might be expected on that instance, and to prevent disk exhaustion via things like super high compression ratio files. Thanks to Jeong Woo Lee (eclipse07077-ljw) for reporting.
133 lines
3.6 KiB
PHP
133 lines
3.6 KiB
PHP
<?php
|
|
|
|
namespace BookStack\Exports\ZipExports;
|
|
|
|
use BookStack\Exceptions\ZipExportException;
|
|
use BookStack\Exports\ZipExports\Models\ZipExportBook;
|
|
use BookStack\Exports\ZipExports\Models\ZipExportChapter;
|
|
use BookStack\Exports\ZipExports\Models\ZipExportPage;
|
|
use BookStack\Util\WebSafeMimeSniffer;
|
|
use ZipArchive;
|
|
|
|
class ZipExportReader
|
|
{
|
|
protected ZipArchive $zip;
|
|
protected bool $open = false;
|
|
|
|
public function __construct(
|
|
protected string $zipPath,
|
|
) {
|
|
$this->zip = new ZipArchive();
|
|
}
|
|
|
|
/**
|
|
* @throws ZipExportException
|
|
*/
|
|
protected function open(): void
|
|
{
|
|
if ($this->open) {
|
|
return;
|
|
}
|
|
|
|
// Validate file exists
|
|
if (!file_exists($this->zipPath) || !is_readable($this->zipPath)) {
|
|
throw new ZipExportException(trans('errors.import_zip_cant_read'));
|
|
}
|
|
|
|
// Validate file is valid zip
|
|
$opened = $this->zip->open($this->zipPath, ZipArchive::RDONLY);
|
|
if ($opened !== true) {
|
|
throw new ZipExportException(trans('errors.import_zip_cant_read'));
|
|
}
|
|
|
|
$this->open = true;
|
|
}
|
|
|
|
public function close(): void
|
|
{
|
|
if ($this->open) {
|
|
$this->zip->close();
|
|
$this->open = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @throws ZipExportException
|
|
*/
|
|
public function readData(): array
|
|
{
|
|
$this->open();
|
|
|
|
$info = $this->zip->statName('data.json');
|
|
if ($info === false) {
|
|
throw new ZipExportException(trans('errors.import_zip_cant_decode_data'));
|
|
}
|
|
|
|
$maxSize = max(intval(config()->get('app.upload_limit')), 1) * 1000000;
|
|
if ($info['size'] > $maxSize) {
|
|
throw new ZipExportException(trans('errors.import_zip_data_too_large'));
|
|
}
|
|
|
|
// Validate json data exists, including metadata
|
|
$jsonData = $this->zip->getFromName('data.json') ?: '';
|
|
$importData = json_decode($jsonData, true);
|
|
if (!$importData) {
|
|
throw new ZipExportException(trans('errors.import_zip_cant_decode_data'));
|
|
}
|
|
|
|
return $importData;
|
|
}
|
|
|
|
public function fileExists(string $fileName): bool
|
|
{
|
|
return $this->zip->statName("files/{$fileName}") !== false;
|
|
}
|
|
|
|
public function fileWithinSizeLimit(string $fileName): bool
|
|
{
|
|
$fileInfo = $this->zip->statName("files/{$fileName}");
|
|
if ($fileInfo === false) {
|
|
return false;
|
|
}
|
|
|
|
$maxSize = max(intval(config()->get('app.upload_limit')), 1) * 1000000;
|
|
return $fileInfo['size'] <= $maxSize;
|
|
}
|
|
|
|
/**
|
|
* @return false|resource
|
|
*/
|
|
public function streamFile(string $fileName)
|
|
{
|
|
return $this->zip->getStream("files/{$fileName}");
|
|
}
|
|
|
|
/**
|
|
* Sniff the mime type from the file of given name.
|
|
*/
|
|
public function sniffFileMime(string $fileName): string
|
|
{
|
|
$stream = $this->streamFile($fileName);
|
|
$sniffContent = fread($stream, 2000);
|
|
|
|
return (new WebSafeMimeSniffer())->sniff($sniffContent);
|
|
}
|
|
|
|
/**
|
|
* @throws ZipExportException
|
|
*/
|
|
public function decodeDataToExportModel(): ZipExportBook|ZipExportChapter|ZipExportPage
|
|
{
|
|
$data = $this->readData();
|
|
if (isset($data['book'])) {
|
|
return ZipExportBook::fromArray($data['book']);
|
|
} else if (isset($data['chapter'])) {
|
|
return ZipExportChapter::fromArray($data['chapter']);
|
|
} else if (isset($data['page'])) {
|
|
return ZipExportPage::fromArray($data['page']);
|
|
}
|
|
|
|
throw new ZipExportException("Could not identify content in ZIP file data.");
|
|
}
|
|
}
|