mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-07-15 21:31:36 +03:00
Merge branch 'development' into Zhey-on/feature/csp-image-css-controls-6033
This commit is contained in:
@@ -117,7 +117,7 @@ class CommentMentionTest extends TestCase
|
||||
$page = $this->entities->page();
|
||||
|
||||
$this->permissions->disableEntityInheritedPermissions($page);
|
||||
$this->permissions->addEntityPermission($page, ['view'], $userA->roles()->first());
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view'], $userA->roles()->first());
|
||||
|
||||
$this->asAdmin()->post("/comment/{$page->id}", [
|
||||
'html' => '<p>Hello <a data-mention-user-id="' . $userA->id . '"></a> and <a data-mention-user-id="' . $userB->id . '"></a></p>'
|
||||
|
||||
@@ -39,7 +39,7 @@ class ContentPermissionsApiTest extends TestCase
|
||||
$page = $this->entities->page();
|
||||
$owner = $this->users->newUser();
|
||||
$role = $this->users->createRole();
|
||||
$this->permissions->addEntityPermission($page, ['view', 'delete'], $role);
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view', 'delete'], $role);
|
||||
$this->permissions->changeEntityOwner($page, $owner);
|
||||
$this->permissions->setFallbackPermissions($page, ['update', 'create']);
|
||||
|
||||
@@ -209,7 +209,7 @@ class ContentPermissionsApiTest extends TestCase
|
||||
public function test_update_can_clear_roles_permissions()
|
||||
{
|
||||
$page = $this->entities->page();
|
||||
$this->permissions->addEntityPermission($page, ['view'], $this->users->createRole());
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view'], $this->users->createRole());
|
||||
$page->owned_by = null;
|
||||
$page->save();
|
||||
|
||||
|
||||
@@ -154,6 +154,34 @@ class ImageGalleryApiTest extends TestCase
|
||||
$resp->assertStatus(404);
|
||||
}
|
||||
|
||||
public function test_create_requires_update_permission_for_the_target_page()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$this->actingAsForApi($editor);
|
||||
|
||||
$makeRequest = function (int $uploadedTo) {
|
||||
return $this->call('POST', $this->baseEndpoint, [
|
||||
'type' => 'gallery',
|
||||
'uploaded_to' => $uploadedTo,
|
||||
'name' => 'My awesome image!',
|
||||
], [], [
|
||||
'image' => $this->files->uploadedImage('my-cool-image.png'),
|
||||
]);
|
||||
};
|
||||
|
||||
$page = $this->entities->page();
|
||||
$this->permissions->disableEntityInheritedPermissions($page);
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view'], $editor->roles()->first());
|
||||
|
||||
$resp = $makeRequest($page->id);
|
||||
$resp->assertStatus(403);
|
||||
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view', 'update'], $editor->roles()->first());
|
||||
|
||||
$resp = $makeRequest($page->id);
|
||||
$resp->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_create_has_restricted_types()
|
||||
{
|
||||
$this->actingAsApiEditor();
|
||||
|
||||
@@ -106,6 +106,7 @@ class SearchApiTest extends TestCase
|
||||
$this->permissions->setEntityPermissions($page, ['view'], [$editor->roles()->first()]);
|
||||
|
||||
$resp = $this->getJson($this->baseEndpoint . '?query=superextrauniquevalue');
|
||||
$resp->assertOk();
|
||||
$resp->assertJsonPath('data.0.id', $page->id);
|
||||
$resp->assertJsonPath('data.0.book.name', $book->name);
|
||||
$resp->assertJsonMissingPath('data.0.chapter');
|
||||
|
||||
108
tests/Api/TagsApiTest.php
Normal file
108
tests/Api/TagsApiTest.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Api;
|
||||
|
||||
use BookStack\Activity\Models\Tag;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TagsApiTest extends TestCase
|
||||
{
|
||||
use TestsApi;
|
||||
|
||||
public function test_list_names_provides_rolled_up_tag_info(): void
|
||||
{
|
||||
$tagInfo = ['name' => 'MyGreatApiTag', 'value' => 'cat'];
|
||||
$pagesToTag = Page::query()->take(10)->get();
|
||||
$booksToTag = Book::query()->take(3)->get();
|
||||
$chaptersToTag = Chapter::query()->take(5)->get();
|
||||
$pagesToTag->each(fn (Page $page) => $page->tags()->save(new Tag($tagInfo)));
|
||||
$booksToTag->each(fn (Book $book) => $book->tags()->save(new Tag($tagInfo)));
|
||||
$chaptersToTag->each(fn (Chapter $chapter) => $chapter->tags()->save(new Tag($tagInfo)));
|
||||
|
||||
$resp = $this->actingAsApiEditor()->getJson('api/tags/names?filter[name]=MyGreatApiTag');
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertJson([
|
||||
'data' => [
|
||||
[
|
||||
'name' => 'MyGreatApiTag',
|
||||
'values' => 1,
|
||||
'usages' => 18,
|
||||
'page_count' => 10,
|
||||
'book_count' => 3,
|
||||
'chapter_count' => 5,
|
||||
'shelf_count' => 0,
|
||||
]
|
||||
],
|
||||
'total' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_list_names_is_limited_by_permission_visibility(): void
|
||||
{
|
||||
$pagesToTag = Page::query()->take(10)->get();
|
||||
$pagesToTag->each(fn (Page $page) => $page->tags()->save(new Tag(['name' => 'MyGreatApiTag', 'value' => 'cat' . $page->id])));
|
||||
|
||||
$this->permissions->disableEntityInheritedPermissions($pagesToTag[3]);
|
||||
$this->permissions->disableEntityInheritedPermissions($pagesToTag[6]);
|
||||
|
||||
$resp = $this->actingAsApiEditor()->getJson('api/tags/names?filter[name]=MyGreatApiTag');
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertJson([
|
||||
'data' => [
|
||||
[
|
||||
'name' => 'MyGreatApiTag',
|
||||
'values' => 8,
|
||||
'usages' => 8,
|
||||
'page_count' => 8,
|
||||
'book_count' => 0,
|
||||
'chapter_count' => 0,
|
||||
'shelf_count' => 0,
|
||||
]
|
||||
],
|
||||
'total' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_list_values_returns_values_for_set_tag()
|
||||
{
|
||||
$pagesToTag = Page::query()->take(10)->get();
|
||||
$booksToTag = Book::query()->take(3)->get();
|
||||
$chaptersToTag = Chapter::query()->take(5)->get();
|
||||
$pagesToTag->each(fn (Page $page) => $page->tags()->save(new Tag(['name' => 'MyValueApiTag', 'value' => 'tag-page' . $page->id])));
|
||||
$booksToTag->each(fn (Book $book) => $book->tags()->save(new Tag(['name' => 'MyValueApiTag', 'value' => 'tag-book' . $book->id])));
|
||||
$chaptersToTag->each(fn (Chapter $chapter) => $chapter->tags()->save(new Tag(['name' => 'MyValueApiTag', 'value' => 'tag-chapter' . $chapter->id])));
|
||||
|
||||
$resp = $this->actingAsApiEditor()->getJson('api/tags/values-for-name?name=MyValueApiTag');
|
||||
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertJson(['total' => 18]);
|
||||
$resp->assertJsonFragment([
|
||||
[
|
||||
'name' => 'MyValueApiTag',
|
||||
'value' => 'tag-page' . $pagesToTag[0]->id,
|
||||
'usages' => 1,
|
||||
'page_count' => 1,
|
||||
'book_count' => 0,
|
||||
'chapter_count' => 0,
|
||||
'shelf_count' => 0,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_list_values_is_limited_by_permission_visibility(): void
|
||||
{
|
||||
$pagesToTag = Page::query()->take(10)->get();
|
||||
$pagesToTag->each(fn (Page $page) => $page->tags()->save(new Tag(['name' => 'MyGreatApiTag', 'value' => 'cat' . $page->id])));
|
||||
|
||||
$this->permissions->disableEntityInheritedPermissions($pagesToTag[3]);
|
||||
$this->permissions->disableEntityInheritedPermissions($pagesToTag[6]);
|
||||
|
||||
$resp = $this->actingAsApiEditor()->getJson('api/tags/values-for-name?name=MyGreatApiTag');
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertJson(['total' => 8]);
|
||||
$resp->assertJsonMissing(['value' => 'cat' . $pagesToTag[3]->id]);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Tests\Commands;
|
||||
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -61,4 +62,21 @@ class CopyShelfPermissionsCommandTest extends TestCase
|
||||
'view' => true, 'update' => true, 'create' => false, 'delete' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_copy_shelf_permissions_command_using_slug_without_interaction()
|
||||
{
|
||||
$shelf = $this->entities->shelfHasBooks();
|
||||
$editorRole = $this->users->editor()->roles()->first();
|
||||
/** @var Book $child */
|
||||
$child = $shelf->books()->first();
|
||||
$child->shelves()->where('id', '!=', $shelf->id)->delete();
|
||||
|
||||
$this->assertFalse($child->hasPermissions());
|
||||
|
||||
$this->permissions->setEntityPermissions($shelf, ['view', 'update'], [$editorRole]);
|
||||
$this->artisan('bookstack:copy-shelf-permissions --all --no-interaction');
|
||||
|
||||
$child->refresh();
|
||||
$this->assertTrue($child->hasPermissions(), 'Child book should now be restricted');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,18 +96,44 @@ class InstallModuleCommandTest extends TestCase
|
||||
});
|
||||
}
|
||||
|
||||
public function test_remote_module_install_does_not_follow_redirects_to_different_origin()
|
||||
public function test_remote_module_install_prompts_on_following_redirects_to_different_origin()
|
||||
{
|
||||
$this->usingThemeFolder(function () {
|
||||
$zip = $this->getModuleZipPath();
|
||||
|
||||
$http = $this->mockHttpClient([
|
||||
new Response(302, ['Location' => 'http://example.com/a-test-module.zip']),
|
||||
new Response(301, ['Location' => 'https://a.example.com:8080/a-test-module.zip']),
|
||||
new Response(200, ['Content-Length' => filesize($zip)], file_get_contents($zip))
|
||||
]);
|
||||
|
||||
$this->artisan('bookstack:install-module', ['location' => 'https://example.com/test-module.zip'])
|
||||
->expectsConfirmation('Are you sure you trust this source?', 'yes')
|
||||
->expectsOutput('The download URL is redirecting to a different site: http://example.com')
|
||||
->expectsConfirmation('Do you trust downloading the module from this site?', 'yes')
|
||||
->expectsOutput('The download URL is redirecting to a different site: https://a.example.com:8080')
|
||||
->expectsConfirmation('Do you trust downloading the module from this site?', 'yes')
|
||||
->assertExitCode(0);
|
||||
|
||||
$this->assertEquals(3, $http->requestCount());
|
||||
$this->assertEquals('https', $http->requestAt(0)->getUri()->getScheme());
|
||||
$this->assertEquals('http', $http->requestAt(1)->getUri()->getScheme());
|
||||
$this->assertEquals('a.example.com', $http->requestAt(2)->getUri()->getHost());
|
||||
});
|
||||
}
|
||||
|
||||
public function test_remote_module_install_redirect_origin_prompt_rejection()
|
||||
{
|
||||
$this->usingThemeFolder(function () {
|
||||
$http = $this->mockHttpClient([
|
||||
new Response(302, ['Location' => 'http://example.com/a-test-module.zip']),
|
||||
new Response(301, ['Location' => 'https://a.example.com:8080/a-test-module.zip']),
|
||||
]);
|
||||
|
||||
$this->artisan('bookstack:install-module', ['location' => 'https://example.com/test-module.zip'])
|
||||
->expectsConfirmation('Are you sure you trust this source?', 'yes')
|
||||
->expectsOutput('The download URL is redirecting to a different site: http://example.com')
|
||||
->expectsConfirmation('Do you trust downloading the module from this site?', 'no')
|
||||
->assertExitCode(1);
|
||||
|
||||
$this->assertEquals(1, $http->requestCount());
|
||||
@@ -115,6 +141,26 @@ class InstallModuleCommandTest extends TestCase
|
||||
});
|
||||
}
|
||||
|
||||
public function test_remote_module_install_has_redirect_limit()
|
||||
{
|
||||
$this->usingThemeFolder(function () {
|
||||
$http = $this->mockHttpClient([
|
||||
new Response(302, ['Location' => 'https://example.com/a-test-module.zip']),
|
||||
new Response(302, ['Location' => 'https://example.com/b-test-module.zip']),
|
||||
new Response(302, ['Location' => 'https://example.com/c-test-module.zip']),
|
||||
new Response(302, ['Location' => 'https://example.com/d-test-module.zip']),
|
||||
]);
|
||||
|
||||
$this->artisan('bookstack:install-module', ['location' => 'https://example.com/test-module.zip'])
|
||||
->expectsConfirmation('Are you sure you trust this source?', 'yes')
|
||||
->expectsOutput('ERROR: Failed to download module from https://example.com/test-module.zip')
|
||||
->assertExitCode(1);
|
||||
|
||||
$this->assertEquals(4, $http->requestCount());
|
||||
$this->assertEquals('/c-test-module.zip', $http->requestAt(3)->getUri()->getPath());
|
||||
});
|
||||
}
|
||||
|
||||
public function test_remote_module_install_download_failures_are_announced_to_user()
|
||||
{
|
||||
$this->usingThemeFolder(function () {
|
||||
@@ -175,6 +221,52 @@ class InstallModuleCommandTest extends TestCase
|
||||
->assertExitCode(1);
|
||||
}
|
||||
|
||||
public function test_module_zip_when_files_in_nested_directory()
|
||||
{
|
||||
$this->usingThemeFolder(function ($themeFolder) {
|
||||
$zip = new ZipArchive();
|
||||
$zipFile = tempnam(sys_get_temp_dir(), 'bs-test-module');
|
||||
$zip->open($zipFile, ZipArchive::CREATE);
|
||||
|
||||
$zip->addEmptyDir('mod');
|
||||
$zip->addFromString('mod/bookstack-module.json', json_encode($metadata ?? [
|
||||
'name' => 'Test Module',
|
||||
'description' => 'A test module for BookStack',
|
||||
'version' => '1.0.0',
|
||||
]));
|
||||
$zip->addFromString('mod/functions.php', '<?php $a = "cat";');
|
||||
$zip->addEmptyDir('mod/a');
|
||||
$zip->addFromString('mod/a/cat.txt', 'Meow');
|
||||
$zip->close();
|
||||
|
||||
$this->artisan('bookstack:install-module', ['location' => $zipFile])
|
||||
->expectsConfirmation('Are you sure you want to install this module?', 'yes')
|
||||
->assertExitCode(0);
|
||||
|
||||
$modulePath = glob(theme_path('modules/*'), GLOB_ONLYDIR)[0];
|
||||
$this->assertFileExists($modulePath . '/a/cat.txt');
|
||||
$contents = file_get_contents($modulePath . '/a/cat.txt');
|
||||
$this->assertEquals('Meow', $contents);
|
||||
});
|
||||
}
|
||||
|
||||
public function test_module_install_negates_zip_slip()
|
||||
{
|
||||
$this->usingThemeFolder(function () {
|
||||
$zip = $this->getModuleZipPath(null, [
|
||||
'../parent.txt' => str_repeat('dog', 10)
|
||||
]);
|
||||
|
||||
$expectedInstallPath = theme_path('modules/test-module');
|
||||
$this->artisan('bookstack:install-module', ['location' => $zip])
|
||||
->expectsConfirmation('Are you sure you want to install this module?', 'yes')
|
||||
->expectsOutput("ERROR: Failed to install module with error: Failed to load extract files from module ZIP with error: Bad file path found in module ZIP file: ../parent.txt")
|
||||
->assertExitCode(1);
|
||||
|
||||
$this->assertDirectoryDoesNotExist($expectedInstallPath);
|
||||
});
|
||||
}
|
||||
|
||||
public function test_local_module_install_without_active_theme_can_setup_theme_folder()
|
||||
{
|
||||
$zip = $this->getModuleZipPath();
|
||||
|
||||
@@ -16,7 +16,7 @@ class RegeneratePermissionsCommandTest extends TestCase
|
||||
$page = $this->entities->page();
|
||||
$editor = $this->users->editor();
|
||||
$role = $editor->roles()->first();
|
||||
$this->permissions->addEntityPermission($page, ['view'], $role);
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view'], $role);
|
||||
JointPermission::query()->truncate();
|
||||
|
||||
$this->assertDatabaseMissing('joint_permissions', ['entity_id' => $page->id]);
|
||||
|
||||
@@ -27,7 +27,7 @@ class DebugViewTest extends TestCase
|
||||
$resp->assertSeeText('BookStack Version: ' . trim(file_get_contents(base_path('version'))));
|
||||
// Dynamic help links
|
||||
$this->withHtml($resp)->assertElementExists('a[href*="q=' . urlencode('BookStack An error occurred during testing') . '"]');
|
||||
$this->withHtml($resp)->assertElementExists('a[href*="?q=is%3Aissue+' . urlencode('An error occurred during testing') . '"]');
|
||||
$this->withHtml($resp)->assertElementExists('a[href*="?q=' . urlencode('An error occurred during testing') . '"]');
|
||||
}
|
||||
|
||||
public function test_debug_view_only_shows_when_debug_mode_is_enabled()
|
||||
|
||||
@@ -256,8 +256,8 @@ class BookTest extends TestCase
|
||||
{
|
||||
$book = $this->entities->book();
|
||||
|
||||
$input = '<h1>Test</h1><p id="abc" href="beans">Content<a href="#cat" target="_blank" data-a="b">a</a><section>Hello</section></p>';
|
||||
$expected = '<p>Content<a href="#cat" target="_blank">a</a></p>';
|
||||
$input = '<h1>Test</h1><p id="abc" href="beans">Content<a href="#cat" target="_blank" data-a="b">a</a><section>Hello</section><code id="abc">code</code></p>';
|
||||
$expected = '<p>Content<a href="#cat" target="_blank">a</a><code>code</code></p>';
|
||||
|
||||
$this->asEditor()->put($book->getUrl(), [
|
||||
'name' => $book->name,
|
||||
|
||||
@@ -370,7 +370,7 @@ class PageContentTest extends TestCase
|
||||
|
||||
public function test_base64_images_within_html_blanked_if_not_supported_extension_for_extract()
|
||||
{
|
||||
// Relevant to https://github.com/BookStackApp/BookStack/issues/3010 and other cases
|
||||
// Relevant to https://codeberg.org/bookstack/bookstack/issues/3010 and other cases
|
||||
$extensions = [
|
||||
'jiff', 'pngr', 'png ', ' png', '.png', 'png.', 'p.ng', ',png',
|
||||
'data:image/png', ',data:image/png',
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace Tests\Entity;
|
||||
|
||||
use BookStack\Activity\ActivityType;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Models\PageRevision;
|
||||
use BookStack\Permissions\Permission;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PageRevisionTest extends TestCase
|
||||
@@ -257,6 +259,33 @@ class PageRevisionTest extends TestCase
|
||||
$revisionView->assertDontSee('dontwantthishere');
|
||||
}
|
||||
|
||||
public function test_access_to_revision_operation_requires_revision_view_all_permission()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$this->actingAs($editor);
|
||||
|
||||
$page = $this->entities->page();
|
||||
$this->createRevisions($page, 3);
|
||||
/** @var PageRevision $revision */
|
||||
$revision = $page->revisions()->orderBy('id', 'desc')->first();
|
||||
|
||||
$this->get($page->getUrl())->assertSee($page->getUrl('/revisions'), false);
|
||||
$this->get($page->getUrl('/revisions'))->assertOk();
|
||||
$this->get($revision->getUrl())->assertOk();
|
||||
$this->get($revision->getUrl('/changes'))->assertOk();
|
||||
$this->put($revision->getUrl('/restore'))->assertRedirect($page->getUrl());
|
||||
$this->delete($revision->getUrl('/delete'))->assertRedirect($page->getUrl('/revisions'));
|
||||
|
||||
$this->permissions->removeUserRolePermissions($editor, [Permission::RevisionViewAll]);
|
||||
|
||||
$this->get($page->getUrl())->assertDontSee($page->getUrl('/revisions'), false);
|
||||
$this->assertPermissionError($this->get($page->getUrl('/revisions')));
|
||||
$this->assertPermissionError($this->get($revision->getUrl()));
|
||||
$this->assertPermissionError($this->get($revision->getUrl('/changes')));
|
||||
$this->assertPermissionError($this->put($revision->getUrl('/restore')));
|
||||
$this->assertPermissionError($this->delete($revision->getUrl('/delete')));
|
||||
}
|
||||
|
||||
public function test_revision_restore_action_only_visible_with_permission()
|
||||
{
|
||||
$page = $this->entities->page();
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ErrorTest extends TestCase
|
||||
@@ -44,7 +43,7 @@ class ErrorTest extends TestCase
|
||||
$this->actingAs($editor)->get($page->getUrl())->assertOk();
|
||||
|
||||
$this->permissions->disableEntityInheritedPermissions($book);
|
||||
$this->permissions->addEntityPermission($page, ['view'], $editor->roles()->first());
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view'], $editor->roles()->first());
|
||||
|
||||
$resp = $this->actingAs($editor)->get($book->getUrl());
|
||||
$resp->assertNotFound();
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Tests\Exports;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Permissions\Permission;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -229,6 +230,20 @@ class HtmlExportTest extends TestCase
|
||||
$resp->assertDontSee('ExportWizardTheFifth');
|
||||
}
|
||||
|
||||
public function test_page_export_only_includes_revision_count_if_user_has_revision_view_permissions()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$page = $this->entities->page();
|
||||
|
||||
$resp = $this->actingAs($editor)->get($page->getUrl('/export/html'));
|
||||
$resp->assertSee('Revision #');
|
||||
|
||||
$this->permissions->removeUserRolePermissions($editor, [Permission::RevisionViewAll]);
|
||||
|
||||
$resp = $this->actingAs($editor)->get($page->getUrl('/export/html'));
|
||||
$resp->assertDontSee('Revision #');
|
||||
}
|
||||
|
||||
public function test_html_exports_contain_csp_meta_tag()
|
||||
{
|
||||
$entities = [
|
||||
|
||||
@@ -79,6 +79,39 @@ class PdfExportTest extends TestCase
|
||||
$this->assertStringContainsString('<details open="open"', $pdfHtml);
|
||||
}
|
||||
|
||||
public function test_custom_fonts_loaded_for_dom_pdf_when_used()
|
||||
{
|
||||
// Set up custom font usage
|
||||
$page = $this->entities->page()->forceFill([
|
||||
'html' => '<p><strong>Bold</strong>text</p>',
|
||||
]);
|
||||
$page->save();
|
||||
$this->setSettings([
|
||||
'app-custom-head' => '<style>* { font-family: "meow words"}</style>'
|
||||
]);
|
||||
$normalFont = $this->files->testFilePath('fonts/Cardiff.ttf');
|
||||
$normalFontTarget = storage_path('fonts/dompdf/MeowWords.ttf');
|
||||
$boldFont = $this->files->testFilePath('fonts/Cardiff-Bold.ttf');
|
||||
$boldFontTarget = storage_path('fonts/dompdf/MeowWords-Bold.ttf');
|
||||
copy($normalFont, $normalFontTarget);
|
||||
copy($boldFont, $boldFontTarget);
|
||||
|
||||
$resp = $this->asEditor()->get($page->getUrl('/export/pdf'));
|
||||
$resp->assertStatus(200);
|
||||
|
||||
// Existance of UFM files indicates the metrics have been generated
|
||||
$this->assertFileExists(storage_path('fonts/dompdf/MeowWords.ufm'));
|
||||
$this->assertFileExists(storage_path('fonts/dompdf/MeowWords-Bold.ufm'));
|
||||
// Existence of cache json files indicates the fonts have been used
|
||||
$this->assertFileExists(storage_path('fonts/dompdf/cache/MeowWords.ufm.json'));
|
||||
$this->assertFileExists(storage_path('fonts/dompdf/cache/MeowWords-Bold.ufm.json'));
|
||||
|
||||
$filesToCleanUp = [...glob(storage_path('fonts/dompdf/Meow*')), ...glob(storage_path('fonts/dompdf/cache/Meow*'))];
|
||||
foreach ($filesToCleanUp as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_wkhtmltopdf_only_used_when_allow_untrusted_is_true()
|
||||
{
|
||||
$page = $this->entities->page();
|
||||
|
||||
@@ -52,7 +52,7 @@ class TextExportTest extends TestCase
|
||||
$resp = $this->asEditor()->get($entities['book']->getUrl('/export/plaintext'));
|
||||
|
||||
$expected = "Export Book\nThis is a book with stuff to export\n\nExport chapter\nA test chapter to be exported\nIt has loads of info within\n\n";
|
||||
$expected .= "My wonderful page!\nMy great page Full of great stuff";
|
||||
$expected .= "My wonderful page!\nMy great page\nFull of great stuff";
|
||||
$resp->assertSee($expected);
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ class TextExportTest extends TestCase
|
||||
$resp = $this->asEditor()->get($entities['book']->getUrl('/export/plaintext'));
|
||||
|
||||
$expected = "Export chapter\nA test chapter to be exported\nIt has loads of info within\n\n";
|
||||
$expected .= "My wonderful page!\nMy great page Full of great stuff";
|
||||
$expected .= "My wonderful page!\nMy great page\nFull of great stuff";
|
||||
$resp->assertSee($expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,4 +90,29 @@ class ZipExportValidatorTest extends TestCase
|
||||
|
||||
$this->assertEquals('The file needs to reference a file of type image/png,image/jpeg,image/gif,image/webp, found text/plain.', $results['page.images.0.file']);
|
||||
}
|
||||
|
||||
public function test_page_link_attachments_cant_be_data_or_js()
|
||||
{
|
||||
$validateResultCountByLink = [
|
||||
'data:text/html,<p>hi</p>' => 1,
|
||||
'javascript:alert(\'hi\')' => 1,
|
||||
'mailto:email@example.com' => 0,
|
||||
];
|
||||
|
||||
foreach ($validateResultCountByLink as $link => $count) {
|
||||
$validator = $this->getValidatorForData([
|
||||
'page' => [
|
||||
'id' => 4,
|
||||
'name' => 'My page',
|
||||
'markdown' => 'hello',
|
||||
'attachments' => [
|
||||
['id' => 4, 'name' => 'Attachment A', 'link' => $link],
|
||||
],
|
||||
]
|
||||
]);
|
||||
|
||||
$results = $validator->validate();
|
||||
$this->assertCount($count, $results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,8 +101,9 @@ class PermissionsProvider
|
||||
$this->addEntityPermissionEntries($entity, $permissions);
|
||||
}
|
||||
|
||||
public function addEntityPermission(Entity $entity, array $actionList, Role $role)
|
||||
public function setEntityPermissionsForRole(Entity $entity, array $actionList, Role $role)
|
||||
{
|
||||
$entity->permissions()->where('role_id', '=', $role->id)->delete();
|
||||
$permissionData = $this->actionListToEntityPermissionData($actionList, $role->id);
|
||||
$this->addEntityPermissionEntries($entity, [$permissionData]);
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$page = $this->entities->page();
|
||||
|
||||
$this->permissions->disableEntityInheritedPermissions($page);
|
||||
$this->permissions->addEntityPermission($page, [], $roleA);
|
||||
$this->permissions->addEntityPermission($page, ['view'], $roleB);
|
||||
$this->permissions->setEntityPermissionsForRole($page, [], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view'], $roleB);
|
||||
|
||||
$this->assertVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -42,7 +42,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$chapter = $page->chapter;
|
||||
|
||||
$this->permissions->disableEntityInheritedPermissions($chapter);
|
||||
$this->permissions->addEntityPermission($chapter, ['view'], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, ['view'], $roleA);
|
||||
|
||||
$this->assertVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -54,7 +54,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$chapter = $page->chapter;
|
||||
|
||||
$this->permissions->disableEntityInheritedPermissions($chapter);
|
||||
$this->permissions->addEntityPermission($chapter, [], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, [], $roleA);
|
||||
|
||||
$this->assertNotVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -67,8 +67,8 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$chapter = $page->chapter;
|
||||
|
||||
$this->permissions->disableEntityInheritedPermissions($chapter);
|
||||
$this->permissions->addEntityPermission($chapter, [], $roleA);
|
||||
$this->permissions->addEntityPermission($chapter, ['view'], $roleB);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, [], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, ['view'], $roleB);
|
||||
|
||||
$this->assertVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -80,8 +80,8 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$chapter = $page->chapter;
|
||||
|
||||
$this->permissions->disableEntityInheritedPermissions($chapter);
|
||||
$this->permissions->addEntityPermission($chapter, [], $roleA);
|
||||
$this->permissions->addEntityPermission($page, ['view'], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, [], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view'], $roleA);
|
||||
|
||||
$this->assertVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -93,8 +93,8 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$chapter = $page->chapter;
|
||||
|
||||
$this->permissions->disableEntityInheritedPermissions($chapter);
|
||||
$this->permissions->addEntityPermission($chapter, ['view'], $roleA);
|
||||
$this->permissions->addEntityPermission($page, [], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, ['view'], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($page, [], $roleA);
|
||||
|
||||
$this->assertNotVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -107,8 +107,8 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$chapter = $page->chapter;
|
||||
|
||||
$this->permissions->disableEntityInheritedPermissions($chapter);
|
||||
$this->permissions->addEntityPermission($page, [], $roleA);
|
||||
$this->permissions->addEntityPermission($chapter, ['view'], $roleB);
|
||||
$this->permissions->setEntityPermissionsForRole($page, [], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, ['view'], $roleB);
|
||||
|
||||
$this->assertVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -121,8 +121,8 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$chapter = $page->chapter;
|
||||
|
||||
$this->permissions->disableEntityInheritedPermissions($chapter);
|
||||
$this->permissions->addEntityPermission($page, ['view'], $roleA);
|
||||
$this->permissions->addEntityPermission($chapter, [], $roleB);
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view'], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, [], $roleB);
|
||||
|
||||
$this->assertVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -131,7 +131,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
{
|
||||
[$user, $roleA] = $this->users->newUserWithRole();
|
||||
$page = $this->entities->page();
|
||||
$this->permissions->addEntityPermission($page, ['view'], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view'], $roleA);
|
||||
|
||||
$this->assertVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -140,7 +140,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
{
|
||||
[$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']);
|
||||
$page = $this->entities->page();
|
||||
$this->permissions->addEntityPermission($page, [], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($page, [], $roleA);
|
||||
|
||||
$this->assertNotVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -150,7 +150,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
[$user, $roleA] = $this->users->newUserWithRole([], []);
|
||||
$page = $this->entities->pageWithinChapter();
|
||||
$chapter = $page->chapter;
|
||||
$this->permissions->addEntityPermission($chapter, ['view'], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, ['view'], $roleA);
|
||||
|
||||
$this->assertVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -160,7 +160,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
[$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']);
|
||||
$page = $this->entities->pageWithinChapter();
|
||||
$chapter = $page->chapter;
|
||||
$this->permissions->addEntityPermission($chapter, [], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, [], $roleA);
|
||||
|
||||
$this->assertNotVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -170,7 +170,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
[$user, $roleA] = $this->users->newUserWithRole([], ['page-view-own']);
|
||||
$page = $this->entities->pageWithinChapter();
|
||||
$chapter = $page->chapter;
|
||||
$this->permissions->addEntityPermission($chapter, [], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, [], $roleA);
|
||||
$this->permissions->changeEntityOwner($page, $user);
|
||||
|
||||
$this->assertNotVisibleToUser($page, $user);
|
||||
@@ -182,7 +182,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$roleB = $this->users->attachNewRole($user);
|
||||
$page = $this->entities->page();
|
||||
|
||||
$this->permissions->addEntityPermission($page, [], $roleB);
|
||||
$this->permissions->setEntityPermissionsForRole($page, [], $roleB);
|
||||
|
||||
$this->assertNotVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -194,7 +194,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$page = $this->entities->page();
|
||||
$this->permissions->changeEntityOwner($page, $user);
|
||||
|
||||
$this->permissions->addEntityPermission($page, [], $roleB);
|
||||
$this->permissions->setEntityPermissionsForRole($page, [], $roleB);
|
||||
|
||||
$this->assertNotVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -207,7 +207,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$page = $this->entities->pageWithinChapter();
|
||||
$chapter = $page->chapter;
|
||||
|
||||
$this->permissions->addEntityPermission($chapter, [], $roleB);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, [], $roleB);
|
||||
|
||||
$this->assertNotVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -220,7 +220,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$chapter = $page->chapter;
|
||||
$this->permissions->changeEntityOwner($page, $user);
|
||||
|
||||
$this->permissions->addEntityPermission($chapter, [], $roleB);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, [], $roleB);
|
||||
|
||||
$this->assertNotVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -231,7 +231,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$page = $this->entities->page();
|
||||
|
||||
$this->permissions->setFallbackPermissions($page, []);
|
||||
$this->permissions->addEntityPermission($page, ['view'], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view'], $roleA);
|
||||
|
||||
$this->assertVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -241,7 +241,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$page = $this->entities->page();
|
||||
|
||||
$this->permissions->setFallbackPermissions($page, ['view']);
|
||||
$this->permissions->addEntityPermission($page, [], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($page, [], $roleA);
|
||||
|
||||
$this->assertNotVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -253,7 +253,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$page = $this->entities->page();
|
||||
|
||||
$this->permissions->setFallbackPermissions($page, []);
|
||||
$this->permissions->addEntityPermission($page, ['view'], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view'], $roleA);
|
||||
|
||||
$this->assertVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -265,7 +265,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$page = $this->entities->page();
|
||||
|
||||
$this->permissions->setFallbackPermissions($page, ['view']);
|
||||
$this->permissions->addEntityPermission($page, [], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($page, [], $roleA);
|
||||
|
||||
$this->assertNotVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -277,7 +277,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$chapter = $page->chapter;
|
||||
|
||||
$this->permissions->setFallbackPermissions($chapter, []);
|
||||
$this->permissions->addEntityPermission($chapter, ['view'], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, ['view'], $roleA);
|
||||
|
||||
$this->assertVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -289,7 +289,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$chapter = $page->chapter;
|
||||
|
||||
$this->permissions->setFallbackPermissions($chapter, ['view']);
|
||||
$this->permissions->addEntityPermission($chapter, [], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, [], $roleA);
|
||||
|
||||
$this->assertNotVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -302,7 +302,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$chapter = $page->chapter;
|
||||
|
||||
$this->permissions->setFallbackPermissions($chapter, []);
|
||||
$this->permissions->addEntityPermission($chapter, ['view'], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, ['view'], $roleA);
|
||||
|
||||
$this->assertVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -315,7 +315,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
$chapter = $page->chapter;
|
||||
|
||||
$this->permissions->setFallbackPermissions($chapter, ['view']);
|
||||
$this->permissions->addEntityPermission($chapter, [], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, [], $roleA);
|
||||
|
||||
$this->assertNotVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -328,7 +328,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
|
||||
$this->permissions->setFallbackPermissions($chapter, []);
|
||||
$this->permissions->setFallbackPermissions($page, []);
|
||||
$this->permissions->addEntityPermission($chapter, ['view'], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($chapter, ['view'], $roleA);
|
||||
|
||||
$this->assertNotVisibleToUser($page, $user);
|
||||
}
|
||||
@@ -342,7 +342,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase
|
||||
|
||||
$this->permissions->setFallbackPermissions($book, []);
|
||||
$this->permissions->setFallbackPermissions($chapter, []);
|
||||
$this->permissions->addEntityPermission($book, ['view'], $roleA);
|
||||
$this->permissions->setEntityPermissionsForRole($book, ['view'], $roleA);
|
||||
|
||||
$this->assertNotVisibleToUser($page, $user);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Permissions\Models\RolePermission;
|
||||
use BookStack\Users\Models\Role;
|
||||
use BookStack\Users\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\View;
|
||||
|
||||
@@ -173,7 +172,7 @@ class PublicActionTest extends TestCase
|
||||
$newRole = $this->users->attachNewRole($this->users->guest(), []);
|
||||
$page = $this->entities->page();
|
||||
$this->permissions->disableEntityInheritedPermissions($page);
|
||||
$this->permissions->addEntityPermission($page, ['view', 'update'], $newRole);
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view', 'update'], $newRole);
|
||||
|
||||
$resp = $this->get($page->getUrl());
|
||||
$resp->assertOk();
|
||||
|
||||
@@ -136,17 +136,21 @@ class EntitySearchTest extends TestCase
|
||||
$page->tags()->saveMany([new Tag(['name' => 'DonkCount', 'value' => '500'])]);
|
||||
$page->created_by = $this->users->admin()->id;
|
||||
$page->save();
|
||||
$otherPage = $this->entities->newPage(['name' => 'A different page in negation tests', 'html' => '<p>A different page in negation tests</p>']);
|
||||
|
||||
$editor = $this->users->editor();
|
||||
$this->actingAs($editor);
|
||||
|
||||
$exactSearch = $this->get('/search?term=' . urlencode('negation -"tortoise"'));
|
||||
$exactSearch->assertStatus(200)->assertDontSeeText($page->name);
|
||||
$exactSearch->assertSeeText($otherPage->name);
|
||||
|
||||
$tagSearchA = $this->get('/search?term=' . urlencode('negation [DonkCount=500]'));
|
||||
$tagSearchA->assertStatus(200)->assertSeeText($page->name);
|
||||
$tagSearchA->assertDontSeeText($otherPage->name);
|
||||
$tagSearchB = $this->get('/search?term=' . urlencode('negation -[DonkCount=500]'));
|
||||
$tagSearchB->assertStatus(200)->assertDontSeeText($page->name);
|
||||
$tagSearchB->assertSeeText($otherPage->name);
|
||||
|
||||
$filterSearchA = $this->get('/search?term=' . urlencode('negation -{created_by:me}'));
|
||||
$filterSearchA->assertStatus(200)->assertSeeText($page->name);
|
||||
|
||||
@@ -271,6 +271,21 @@ class BookSortTest extends TestCase
|
||||
$this->withHtml($resp)->assertElementExists('select[name="auto-sort"] option[value="' . $sort->id . '"]');
|
||||
}
|
||||
|
||||
public function test_auto_sort_rule_create_hint_shown_on_sort_page()
|
||||
{
|
||||
$book = $this->entities->book();
|
||||
$hintText = 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.';
|
||||
|
||||
// Admin users see link for creating new rule
|
||||
$resp = $this->asAdmin()->get($book->getUrl('/sort'));
|
||||
$this->withHtml($resp)->assertLinkExists(url('/settings/sorting/rules/new'), 'Create Sort Rule');
|
||||
$resp->assertDontSee($hintText);
|
||||
|
||||
// Non-admin users see help text
|
||||
$resp = $this->asEditor()->get($book->getUrl('/sort'));
|
||||
$resp->assertSee($hintText);
|
||||
}
|
||||
|
||||
public function test_auto_sort_option_submit_saves_to_book()
|
||||
{
|
||||
$sort = SortRule::factory()->create();
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use BookStack\Exceptions\HttpFetchException;
|
||||
use BookStack\Util\SsrUrlValidator;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SsrUrlValidatorTest extends TestCase
|
||||
{
|
||||
public function test_allowed()
|
||||
{
|
||||
$testMap = [
|
||||
// Single values
|
||||
['config' => '', 'url' => '', 'result' => false],
|
||||
['config' => '', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => ' ', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => '*', 'url' => '', 'result' => false],
|
||||
['config' => '*', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => 'https://*', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => 'http://*', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => 'https://*example.com', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => 'https://*ample.com', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => 'https://*.example.com', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => 'https://*.example.com', 'url' => 'https://test.example.com', 'result' => true],
|
||||
['config' => '*//example.com', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => '*//example.com', 'url' => 'http://example.com', 'result' => true],
|
||||
['config' => '*//example.co', 'url' => 'http://example.co.uk', 'result' => false],
|
||||
['config' => '*//example.co/bookstack', 'url' => 'https://example.co/bookstack/a/path', 'result' => true],
|
||||
['config' => '*//example.co*', 'url' => 'https://example.co.uk/bookstack/a/path', 'result' => true],
|
||||
['config' => 'https://example.com', 'url' => 'https://example.com/a/b/c?test=cat', 'result' => true],
|
||||
['config' => 'https://example.com', 'url' => 'https://example.co.uk', 'result' => false],
|
||||
|
||||
// Escapes
|
||||
['config' => 'https://(.*?).com', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => 'https://example.com', 'url' => 'https://example.co.uk#https://example.com', 'result' => false],
|
||||
|
||||
// Multi values
|
||||
['config' => '*//example.org *//example.com', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => '*//example.org *//example.com', 'url' => 'https://example.com/a/b/c?test=cat#hello', 'result' => true],
|
||||
['config' => '*.example.org *.example.com', 'url' => 'https://example.co.uk', 'result' => false],
|
||||
['config' => ' *.example.org *.example.com ', 'url' => 'https://example.co.uk', 'result' => false],
|
||||
['config' => '* *.example.com', 'url' => 'https://example.co.uk', 'result' => true],
|
||||
['config' => '*//example.org *//example.com *//example.co.uk', 'url' => 'https://example.co.uk', 'result' => true],
|
||||
['config' => '*//example.org *//example.com *//example.co.uk', 'url' => 'https://example.net', 'result' => false],
|
||||
];
|
||||
|
||||
foreach ($testMap as $test) {
|
||||
$result = (new SsrUrlValidator($test['config']))->allowed($test['url']);
|
||||
$this->assertEquals($test['result'], $result, "Failed asserting url '{$test['url']}' with config '{$test['config']}' results " . ($test['result'] ? 'true' : 'false'));
|
||||
}
|
||||
}
|
||||
|
||||
public function test_enssure_allowed()
|
||||
{
|
||||
$result = (new SsrUrlValidator('https://example.com'))->ensureAllowed('https://example.com');
|
||||
$this->assertNull($result);
|
||||
|
||||
$this->expectException(HttpFetchException::class);
|
||||
(new SsrUrlValidator('https://example.com'))->ensureAllowed('https://test.example.com');
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace Tests\Uploads;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Repos\PageRepo;
|
||||
use BookStack\Entities\Tools\TrashCan;
|
||||
use BookStack\Permissions\Permission;
|
||||
use BookStack\Uploads\Attachment;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -206,6 +207,21 @@ class AttachmentTest extends TestCase
|
||||
$this->files->deleteAllAttachmentFiles();
|
||||
}
|
||||
|
||||
public function test_attachment_deletion_requires_page_access()
|
||||
{
|
||||
$page = $this->entities->page();
|
||||
$attachment = Attachment::factory()->create(['uploaded_to' => $page->id]);
|
||||
$editor = $this->users->editor();
|
||||
|
||||
$this->permissions->disableEntityInheritedPermissions($page);
|
||||
$this->permissions->grantUserRolePermissions($editor, [Permission::AttachmentDeleteAll]);
|
||||
|
||||
$resp = $this->actingAs($editor)->delete($attachment->getUrl());
|
||||
$resp->assertNotFound();
|
||||
|
||||
$this->assertDatabaseHas('attachments', ['id' => $attachment->id]);
|
||||
}
|
||||
|
||||
public function test_attachment_access_without_permission_shows_404()
|
||||
{
|
||||
$admin = $this->users->admin();
|
||||
|
||||
@@ -72,6 +72,30 @@ class DrawioTest extends TestCase
|
||||
$this->assertTrue($testImageData === $uploadedImageData, 'Uploaded image file data does not match our test image as expected');
|
||||
}
|
||||
|
||||
public function test_base64_upload_requires_edit_permission_to_page()
|
||||
{
|
||||
$page = $this->entities->page();
|
||||
$editor = $this->users->editor();
|
||||
$this->actingAs($editor);
|
||||
|
||||
$this->permissions->disableEntityInheritedPermissions($page);
|
||||
|
||||
$upload = function () use ($page) {
|
||||
return $this->postJson('images/drawio', [
|
||||
'uploaded_to' => $page->id,
|
||||
'image' => 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=',
|
||||
]);
|
||||
};
|
||||
|
||||
$upload()->assertStatus(404);
|
||||
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view'], $editor->roles()->first());
|
||||
$upload()->assertStatus(403);
|
||||
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view', 'update'], $editor->roles()->first());
|
||||
$upload()->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_drawio_url_can_be_configured()
|
||||
{
|
||||
config()->set('services.drawio', 'http://cats.com?dog=tree');
|
||||
|
||||
@@ -36,6 +36,45 @@ class ImageTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_image_upload_with_page_reference_requires_visibility_and_update_permissions_of_target_page()
|
||||
{
|
||||
$page = $this->entities->page();
|
||||
$editor = $this->users->editor();
|
||||
|
||||
$this->permissions->disableEntityInheritedPermissions($page);
|
||||
$this->actingAs($editor);
|
||||
|
||||
$resp = $this->files->uploadGalleryImage($this, 'test-image.png', $page->id);
|
||||
$resp->assertStatus(404);
|
||||
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view'], $editor->roles()->first());
|
||||
|
||||
$resp = $this->files->uploadGalleryImage($this, 'test-image.png', $page->id);
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$this->permissions->setEntityPermissionsForRole($page, ['view', 'update'], $editor->roles()->first());
|
||||
|
||||
$resp = $this->files->uploadGalleryImage($this, 'test-image.png', $page->id);
|
||||
$resp->assertStatus(200);
|
||||
|
||||
$this->files->deleteAtRelativePath($resp->json('path'));
|
||||
}
|
||||
|
||||
public function test_image_upload_with_page_reference_requires_page_to_exist()
|
||||
{
|
||||
$page = $this->entities->page();
|
||||
$this->entities->destroy($page);
|
||||
|
||||
$editor = $this->users->editor();
|
||||
$this->actingAs($editor);
|
||||
|
||||
$resp = $this->files->uploadGalleryImage($this, 'test-image.png', $page->id);
|
||||
$resp->assertStatus(404);
|
||||
|
||||
$resp = $this->files->uploadGalleryImage($this, 'test-image.png', 0);
|
||||
$resp->assertStatus(404);
|
||||
}
|
||||
|
||||
public function test_image_display_thumbnail_generation_does_not_increase_image_size()
|
||||
{
|
||||
$page = $this->entities->page();
|
||||
@@ -75,7 +114,7 @@ class ImageTest extends TestCase
|
||||
|
||||
public function test_image_display_thumbnail_generation_for_animated_avif_images_uses_original_file()
|
||||
{
|
||||
if (! function_exists('imageavif')) {
|
||||
if ((gd_info()['AVIF Support'] ?? false) !== true) {
|
||||
$this->markTestSkipped('imageavif() is not available');
|
||||
}
|
||||
|
||||
|
||||
59
tests/User/UserManagementMfaTest.php
Normal file
59
tests/User/UserManagementMfaTest.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\User;
|
||||
|
||||
use BookStack\Access\Mfa\MfaValue;
|
||||
use BookStack\Activity\ActivityType;
|
||||
use BookStack\Permissions\Permission;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UserManagementMfaTest extends TestCase
|
||||
{
|
||||
public function test_configured_mfa_options_visible_on_user_edit()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
|
||||
$resp = $this->asAdmin()->get($editor->getEditUrl());
|
||||
$resp->assertSeeText('0 methods configured');
|
||||
|
||||
MfaValue::factory()->create(['user_id' => $editor->id, 'method' => MfaValue::METHOD_BACKUP_CODES]);
|
||||
|
||||
$resp = $this->get($editor->getEditUrl());
|
||||
$resp->assertSeeText('1 method configured');
|
||||
$resp->assertDontSeeText('0 methods configured');
|
||||
}
|
||||
|
||||
public function test_reset_mfa_flow()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
MfaValue::factory()->create(['user_id' => $editor->id, 'method' => MfaValue::METHOD_BACKUP_CODES]);
|
||||
MfaValue::factory()->create(['user_id' => $editor->id, 'method' => MfaValue::METHOD_TOTP]);
|
||||
|
||||
$this->assertEquals(2, $editor->mfaValues()->count());
|
||||
|
||||
$resp = $this->asAdmin()->get($editor->getEditUrl());
|
||||
$this->withHtml($resp)->assertElementContains('form[action$="/mfa"] button[type="submit"]', 'Reset');
|
||||
|
||||
$resp = $this->delete($editor->getEditUrl('/mfa'));
|
||||
$resp->assertRedirect($editor->getEditUrl());
|
||||
$this->assertActivityExists(ActivityType::USER_MFA_RESET);
|
||||
|
||||
$resp = $this->followRedirects($resp);
|
||||
$resp->assertSee('Multi-factor authentication methods reset');
|
||||
|
||||
$this->assertEquals(0, $editor->mfaValues()->count());
|
||||
}
|
||||
|
||||
public function test_users_manage_permission_required_for_mfa_reset()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$resp = $this->actingAs($editor)->delete($editor->getEditUrl('/mfa'));
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$this->permissions->grantUserRolePermissions($editor, [Permission::UsersManage]);
|
||||
|
||||
$resp = $this->delete($editor->getEditUrl('/mfa'));
|
||||
$this->assertNotPermissionError($resp);
|
||||
$resp->assertRedirect($editor->getEditUrl());
|
||||
}
|
||||
}
|
||||
63
tests/Util/HtmlToPlainTextTest.php
Normal file
63
tests/Util/HtmlToPlainTextTest.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Util;
|
||||
|
||||
use BookStack\Util\HtmlToPlainText;
|
||||
use Tests\TestCase;
|
||||
|
||||
class HtmlToPlainTextTest extends TestCase
|
||||
{
|
||||
public function test_it_converts_html_to_plain_text()
|
||||
{
|
||||
$html = <<<HTML
|
||||
<p>This is a test</p>
|
||||
<ul>
|
||||
<li>Item 1</li>
|
||||
<li>Item 2</li>
|
||||
</ul>
|
||||
<h2>A Header</h2>
|
||||
<p>more <©> text <strong>with bold</strong></p>
|
||||
HTML;
|
||||
$expected = <<<TEXT
|
||||
This is a test
|
||||
Item 1
|
||||
Item 2
|
||||
A Header
|
||||
more <©> text with bold
|
||||
TEXT;
|
||||
|
||||
$this->runTest($html, $expected);
|
||||
}
|
||||
|
||||
public function test_adjacent_list_items_are_separated_by_newline()
|
||||
{
|
||||
$html = <<<HTML
|
||||
<ul><li>Item A</li><li>Item B</li></ul>
|
||||
HTML;
|
||||
$expected = <<<TEXT
|
||||
Item A
|
||||
Item B
|
||||
TEXT;
|
||||
|
||||
$this->runTest($html, $expected);
|
||||
}
|
||||
|
||||
public function test_inline_formats_dont_cause_newlines()
|
||||
{
|
||||
$html = <<<HTML
|
||||
<p><strong>H</strong><a>e</a><sup>l</sup><span>l</span><em>o</em></p>
|
||||
HTML;
|
||||
$expected = <<<TEXT
|
||||
Hello
|
||||
TEXT;
|
||||
|
||||
$this->runTest($html, $expected);
|
||||
}
|
||||
|
||||
protected function runTest(string $html, string $expected): void
|
||||
{
|
||||
$converter = new HtmlToPlainText();
|
||||
$result = $converter->convert(trim($html));
|
||||
$this->assertEquals(trim($expected), $result);
|
||||
}
|
||||
}
|
||||
142
tests/Util/SsrUrlValidatorTest.php
Normal file
142
tests/Util/SsrUrlValidatorTest.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Util;
|
||||
|
||||
use BookStack\Exceptions\HttpFetchException;
|
||||
use BookStack\Util\SsrUrlValidator;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SsrUrlValidatorTest extends TestCase
|
||||
{
|
||||
public function test_is_uses_app_config_by_default()
|
||||
{
|
||||
config()->set([
|
||||
'app.ssr_hosts' => 'https://donkey.example.com',
|
||||
]);
|
||||
|
||||
$validator = new SsrUrlValidator();
|
||||
|
||||
$this->assertTrue($validator->allowed('https://donkey.example.com'));
|
||||
$this->assertFalse($validator->allowed('https://monkey.example.com'));
|
||||
}
|
||||
|
||||
public function test_config_string_can_be_passed_in_constructor()
|
||||
{
|
||||
config()->set([
|
||||
'app.ssr_hosts' => 'https://donkey.example.com',
|
||||
]);
|
||||
|
||||
$validator = new SsrUrlValidator('https://monkey.example.com');
|
||||
|
||||
$this->assertFalse($validator->allowed('https://donkey.example.com'));
|
||||
$this->assertTrue($validator->allowed('https://monkey.example.com'));
|
||||
}
|
||||
|
||||
public function test_config_string_can_include_multiple_space_seperated_values()
|
||||
{
|
||||
$validator = new SsrUrlValidator('https://monkey.example.com https://cat.example.com');
|
||||
|
||||
$this->assertFalse($validator->allowed('https://donkey.example.com'));
|
||||
$this->assertTrue($validator->allowed('https://monkey.example.com'));
|
||||
$this->assertTrue($validator->allowed('https://cat.example.com'));
|
||||
}
|
||||
|
||||
public function test_ensure_allowed_throws_if_not_allowed()
|
||||
{
|
||||
$validator = new SsrUrlValidator('https://monkey.example.com');
|
||||
|
||||
$this->assertNull($validator->ensureAllowed('https://monkey.example.com'));
|
||||
|
||||
$this->assertThrows(function () use ($validator) {
|
||||
$validator->ensureAllowed('https://donkey.example.com');
|
||||
}, HttpFetchException::class, 'The URL does not match the configured allowed SSR hosts');
|
||||
}
|
||||
|
||||
public function test_basic_url_matching()
|
||||
{
|
||||
$tests = [
|
||||
// Single values
|
||||
['config' => '', 'url' => '', 'result' => false],
|
||||
['config' => '', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => ' ', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => '*', 'url' => '', 'result' => false],
|
||||
['config' => '*', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => 'https://*', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => 'http://*', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => 'https://*example.com', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => 'https://*ample.com', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => 'https://*.example.com', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => 'https://*.example.com', 'url' => 'https://test.example.com', 'result' => true],
|
||||
['config' => '*//example.com', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => '*//example.com', 'url' => 'http://example.com', 'result' => true],
|
||||
['config' => '*//example.co', 'url' => 'http://example.co.uk', 'result' => false],
|
||||
['config' => '*//example.co/bookstack', 'url' => 'https://example.co/bookstack/a/path', 'result' => true],
|
||||
['config' => '*//example.co*', 'url' => 'https://example.co.uk/bookstack/a/path', 'result' => true],
|
||||
['config' => 'https://example.com', 'url' => 'https://example.com/a/b/c?test=cat', 'result' => true],
|
||||
['config' => 'https://example.com', 'url' => 'https://example.co.uk', 'result' => false],
|
||||
|
||||
// Escapes
|
||||
['config' => 'https://(.*?).com', 'url' => 'https://example.com', 'result' => false],
|
||||
['config' => 'https://example.com', 'url' => 'https://example.co.uk#https://example.com', 'result' => false],
|
||||
|
||||
// Multi values
|
||||
['config' => '*//example.org *//example.com', 'url' => 'https://example.com', 'result' => true],
|
||||
['config' => '*//example.org *//example.com', 'url' => 'https://example.com/a/b/c?test=cat#hello', 'result' => true],
|
||||
['config' => '*.example.org *.example.com', 'url' => 'https://example.co.uk', 'result' => false],
|
||||
['config' => ' *.example.org *.example.com ', 'url' => 'https://example.co.uk', 'result' => false],
|
||||
['config' => '* *.example.com', 'url' => 'https://example.co.uk', 'result' => true],
|
||||
['config' => '*//example.org *//example.com *//example.co.uk', 'url' => 'https://example.co.uk', 'result' => true],
|
||||
['config' => '*//example.org *//example.com *//example.co.uk', 'url' => 'https://example.net', 'result' => false],
|
||||
|
||||
// Further tests
|
||||
['config' => 'https://monkey.example.com', 'url' => 'https://monkey.example.com/a/b', 'result' => true,],
|
||||
['config' => 'https://monkey.example.com', 'url' => 'https://monkey.example.com/a/b?a=b#ab', 'result' => true,],
|
||||
['config' => 'https://monkey.example.com', 'url' => 'https://monkey.example.com:8080/a', 'result' => false,],
|
||||
['config' => '*', 'url' => 'https://a.example.com', 'result' => true,],
|
||||
['config' => 'https://monkey.example.com', 'url' => 'http://monkey.example.com/a/b?a=b#ab', 'result' => false,],
|
||||
['config' => 'https://monkey.example.com', 'url' => 'https://beans.monkey.example.com/a/b?a=b#ab', 'result' => false,],
|
||||
['config' => 'https://*monkey.example.com', 'url' => 'https://amonkey.example.com/a/b?a=b#ab', 'result' => true,],
|
||||
['config' => 'https://*monkey.example.com', 'url' => 'https://donkey.example.com/a/b/monkey.example.com/b?a=b#ab', 'result' => false,],
|
||||
['config' => 'https://monkey.example.com', 'url' => 'https://example.com/monkey.example.com/b?a=monkey.example.com#monkey.example.com', 'result' => false,],
|
||||
['config' => 'https://*.example.com', 'url' => 'https://a.b.example.com/a/b', 'result' => true,],
|
||||
['config' => 'https://*.example.com', 'url' => 'https://a.b.example.a.com/a/b', 'result' => false,],
|
||||
['config' => 'https://*.example.com', 'url' => 'https://a.com/a/b?val=a.example.com', 'result' => false,],
|
||||
['config' => 'https://*.example.com', 'url' => 'https://a.com/a/b#example.com', 'result' => false,],
|
||||
['config' => 'https://a.*.example.com', 'url' => 'https://a.b.c.example.com/c/d', 'result' => true,],
|
||||
['config' => 'https://example.com/webhooks/', 'url' => 'https://example.com/webhooks/beans', 'result' => true,],
|
||||
['config' => 'https://example.com/webhooks/', 'url' => 'https://example.com/a/webhooks/', 'result' => false,],
|
||||
['config' => 'https://example.com:8080', 'url' => 'https://example.com/a/b', 'result' => false,],
|
||||
['config' => 'https://example.com:8080', 'url' => 'https://example.com:8080/a/b', 'result' => true,],
|
||||
['config' => 'https://example.com/*', 'url' => 'https://example.com:8080/a/b', 'result' => false,],
|
||||
];
|
||||
|
||||
foreach ($tests as $testCase) {
|
||||
$validator = new SsrUrlValidator($testCase['config']);
|
||||
$result = $validator->allowed($testCase['url']);
|
||||
$this->assertEquals($testCase['result'], $result, "Failed asserting expected result for config {$testCase['config']} and test value {$testCase['url']}");
|
||||
}
|
||||
}
|
||||
|
||||
public function test_wildcard_does_not_match_userinfo_data_but_still_allows_it()
|
||||
{
|
||||
$validator = new SsrUrlValidator('https://*monkey.example.com');
|
||||
$this->assertFalse($validator->allowed('https://monkey.example.com@a.example.com'));
|
||||
|
||||
$validator = new SsrUrlValidator('https://monkey.example.com*');
|
||||
$this->assertFalse($validator->allowed('https://monkey.example.com@a.example.com'));
|
||||
$this->assertFalse($validator->allowed('https://monkey.example.com:monkey.example.com@a.example.com'));
|
||||
|
||||
$validator = new SsrUrlValidator('https://monkey.example.com');
|
||||
$this->assertTrue($validator->allowed('https://a:b@monkey.example.com'));
|
||||
}
|
||||
|
||||
public function test_percent_encoded_slashes_in_host_are_rejected()
|
||||
{
|
||||
$validator = new SsrUrlValidator('*');
|
||||
|
||||
$this->assertFalse($validator->allowed('https://cat.example.com%2Fa/b'));
|
||||
$this->assertFalse($validator->allowed('https://cat.example.com%2fa/b'));
|
||||
$this->assertFalse($validator->allowed('https://cat%2f.example.com/a/b'));
|
||||
$this->assertFalse($validator->allowed('https://cat.exa%2Fmple.com'));
|
||||
}
|
||||
}
|
||||
BIN
tests/test-data/fonts/Cardiff-Bold.ttf
Normal file
BIN
tests/test-data/fonts/Cardiff-Bold.ttf
Normal file
Binary file not shown.
BIN
tests/test-data/fonts/Cardiff.ttf
Normal file
BIN
tests/test-data/fonts/Cardiff.ttf
Normal file
Binary file not shown.
2
tests/test-data/fonts/attribution.txt
Normal file
2
tests/test-data/fonts/attribution.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Font files by Roger White, in public domain.
|
||||
https://web.archive.org/web/20110609213636/http://www.rogersfonts.org.uk/
|
||||
Reference in New Issue
Block a user