Skip to content
Merged
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
21 changes: 21 additions & 0 deletions app/Exports/ZipExports/ZipExportReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ 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);
Expand All @@ -73,6 +83,17 @@ 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
*/
Expand Down
8 changes: 7 additions & 1 deletion app/Exports/ZipExports/ZipFileReferenceRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ public function __construct(
) {
}


/**
* @inheritDoc
*/
Expand All @@ -23,6 +22,13 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
$fail('validation.zip_file')->translate();
}

if (!$this->context->zipReader->fileWithinSizeLimit($value)) {
$fail('validation.zip_file_size')->translate([
'attribute' => $value,
'size' => config('app.upload_limit'),
]);
}

if (!empty($this->acceptedMimes)) {
$fileMime = $this->context->zipReader->sniffFileMime($value);
if (!in_array($fileMime, $this->acceptedMimes)) {
Expand Down
6 changes: 6 additions & 0 deletions app/Exports/ZipExports/ZipImportRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,12 @@ protected function exportTagsToInputArray(array $exportTags): array

protected function zipFileToUploadedFile(string $fileName, ZipExportReader $reader): UploadedFile
{
if (!$reader->fileWithinSizeLimit($fileName)) {
throw new ZipImportException([
"File $fileName exceeds app upload limit."
]);
}

$tempPath = tempnam(sys_get_temp_dir(), 'bszipextract');
$fileStream = $reader->streamFile($fileName);
$tempStream = fopen($tempPath, 'wb');
Expand Down
5 changes: 3 additions & 2 deletions app/Search/SearchController.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ public function searchForSelector(Request $request, QueryPopular $queryPopular)

// Search for entities otherwise show most popular
if ($searchTerm !== false) {
$searchTerm .= ' {type:' . implode('|', $entityTypes) . '}';
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20)['results'];
$options = SearchOptions::fromString($searchTerm);
$options->setFilter('type', implode('|', $entityTypes));
$entities = $this->searchRunner->searchEntities($options, 'all', 1, 20)['results'];
} else {
$entities = $queryPopular->run(20, 0, $entityTypes);
}
Expand Down
8 changes: 8 additions & 0 deletions app/Search/SearchOptionSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,12 @@ public function nonNegated(): self
$values = array_values(array_filter($this->options, fn (SearchOption $option) => !$option->negated));
return new self($values);
}

/**
* @return self<T>
*/
public function limit(int $limit): self
{
return new self(array_slice(array_values($this->options), 0, $limit));
}
}
22 changes: 22 additions & 0 deletions app/Search/SearchOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public static function fromString(string $search): self
{
$instance = new self();
$instance->addOptionsFromString($search);
$instance->limitOptions();
return $instance;
}

Expand Down Expand Up @@ -87,6 +88,8 @@ public static function fromRequest(Request $request): self
$instance->filters = $instance->filters->merge($extras->filters);
}

$instance->limitOptions();

return $instance;
}

Expand Down Expand Up @@ -147,6 +150,25 @@ protected function addOptionsFromString(string $searchString): void
$this->filters = $this->filters->merge(new SearchOptionSet($terms['filters']));
}

/**
* Limit the amount of search options to reasonable levels.
* Provides higher limits to logged-in users since that signals a slightly
* higher level of trust.
*/
protected function limitOptions(): void
{
$userLoggedIn = !user()->isGuest();
$searchLimit = $userLoggedIn ? 10 : 5;
$exactLimit = $userLoggedIn ? 4 : 2;
$tagLimit = $userLoggedIn ? 8 : 4;
$filterLimit = $userLoggedIn ? 10 : 5;

$this->searches = $this->searches->limit($searchLimit);
$this->exacts = $this->exacts->limit($exactLimit);
$this->tags = $this->tags->limit($tagLimit);
$this->filters = $this->filters->limit($filterLimit);
}

/**
* Decode backslash escaping within the input string.
*/
Expand Down
1 change: 1 addition & 0 deletions lang/en/errors.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
'import_zip_cant_read' => 'Could not read ZIP file.',
'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',
'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',
'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',
'import_validation_failed' => 'Import ZIP failed to validate with errors:',
'import_zip_failed_notification' => 'Failed to import ZIP file.',
'import_perms_books' => 'You are lacking the required permissions to create books.',
Expand Down
1 change: 1 addition & 0 deletions lang/en/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.',

'zip_file' => 'The :attribute needs to reference a file within the ZIP.',
'zip_file_size' => 'The file :attribute must not exceed :size MB.',
'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',
'zip_model_expected' => 'Data object expected but ":type" found.',
'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',
Expand Down
53 changes: 53 additions & 0 deletions tests/Exports/ZipImportRunnerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Exceptions\ZipImportException;
use BookStack\Exports\ZipExports\ZipImportRunner;
use BookStack\Uploads\Image;
use Tests\TestCase;
Expand Down Expand Up @@ -431,4 +432,56 @@ public function test_drawing_references_are_updated_within_content()

ZipTestHelper::deleteZipForImport($import);
}

public function test_error_thrown_if_zip_item_exceeds_app_file_upload_limit()
{
$tempFile = tempnam(sys_get_temp_dir(), 'bs-zip-test');
file_put_contents($tempFile, str_repeat('a', 2500000));
$parent = $this->entities->chapter();
config()->set('app.upload_limit', 1);

$import = ZipTestHelper::importFromData([], [
'page' => [
'name' => 'Page A',
'html' => '<p>Hello</p>',
'attachments' => [
[
'name' => 'Text attachment',
'file' => 'file_attachment'
]
],
],
], [
'file_attachment' => $tempFile,
]);

$this->asAdmin();

$this->expectException(ZipImportException::class);
$this->expectExceptionMessage('The file file_attachment must not exceed 1 MB.');

$this->runner->run($import, $parent);
ZipTestHelper::deleteZipForImport($import);
}

public function test_error_thrown_if_zip_data_exceeds_app_file_upload_limit()
{
$parent = $this->entities->chapter();
config()->set('app.upload_limit', 1);

$import = ZipTestHelper::importFromData([], [
'page' => [
'name' => 'Page A',
'html' => '<p>' . str_repeat('a', 2500000) . '</p>',
],
]);

$this->asAdmin();

$this->expectException(ZipImportException::class);
$this->expectExceptionMessage('ZIP data.json content exceeds the configured application maximum upload size.');

$this->runner->run($import, $parent);
ZipTestHelper::deleteZipForImport($import);
}
}
49 changes: 49 additions & 0 deletions tests/Search/SearchOptionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,53 @@ public function test_from_request_properly_parses_out_extras_as_string()
$this->assertEquals('dino', $options->exacts->all()[0]->value);
$this->assertTrue($options->exacts->all()[0]->negated);
}

public function test_from_string_results_are_count_limited_and_larger_for_logged_in_users()
{
$terms = [
...array_fill(0, 40, 'cat'),
...array_fill(0, 50, '"bees"'),
...array_fill(0, 50, '{is_template}'),
...array_fill(0, 50, '[a=b]'),
];

$options = SearchOptions::fromString(implode(' ', $terms));

$this->assertCount(5, $options->searches->all());
$this->assertCount(2, $options->exacts->all());
$this->assertCount(4, $options->tags->all());
$this->assertCount(5, $options->filters->all());

$this->asEditor();
$options = SearchOptions::fromString(implode(' ', $terms));

$this->assertCount(10, $options->searches->all());
$this->assertCount(4, $options->exacts->all());
$this->assertCount(8, $options->tags->all());
$this->assertCount(10, $options->filters->all());
}

public function test_from_request_results_are_count_limited_and_larger_for_logged_in_users()
{
$request = new Request([
'search' => str_repeat('hello ', 20),
'tags' => array_fill(0, 20, 'a=b'),
'extras' => str_repeat('-[b=c] -{viewed_by_me} -"dino"', 20),
]);

$options = SearchOptions::fromRequest($request);

$this->assertCount(5, $options->searches->all());
$this->assertCount(2, $options->exacts->all());
$this->assertCount(4, $options->tags->all());
$this->assertCount(5, $options->filters->all());

$this->asEditor();
$options = SearchOptions::fromRequest($request);

$this->assertCount(10, $options->searches->all());
$this->assertCount(4, $options->exacts->all());
$this->assertCount(8, $options->tags->all());
$this->assertCount(10, $options->filters->all());
}
}
Loading