diff --git a/app/Uploads/Attachment.php b/app/Uploads/Attachment.php
index d1060477d..474d68998 100644
--- a/app/Uploads/Attachment.php
+++ b/app/Uploads/Attachment.php
@@ -41,12 +41,12 @@ class Attachment extends Model
/**
* Get the url of this file.
*/
- public function getUrl(): string
+ public function getUrl($openInline = false): string
{
if ($this->external && strpos($this->path, 'http') !== 0) {
return $this->path;
}
- return url('/attachments/' . $this->id);
+ return url('/attachments/' . $this->id . ($openInline ? '?open=true' : ''));
}
/**
diff --git a/resources/js/components/attachments-list.js b/resources/js/components/attachments-list.js
new file mode 100644
index 000000000..34979c2e7
--- /dev/null
+++ b/resources/js/components/attachments-list.js
@@ -0,0 +1,47 @@
+/**
+ * Attachments List
+ * Adds '?open=true' query to file attachment links
+ * when ctrl/cmd is pressed down.
+ * @extends {Component}
+ */
+class AttachmentsList {
+
+ setup() {
+ this.container = this.$el;
+ this.setupListeners();
+ }
+
+ setupListeners() {
+ const isExpectedKey = (event) => event.key === 'Control' || event.key === 'Meta';
+ window.addEventListener('keydown', event => {
+ if (isExpectedKey(event)) {
+ this.addOpenQueryToLinks();
+ }
+ }, {passive: true});
+ window.addEventListener('keyup', event => {
+ if (isExpectedKey(event)) {
+ this.removeOpenQueryFromLinks();
+ }
+ }, {passive: true});
+ }
+
+ addOpenQueryToLinks() {
+ const links = this.container.querySelectorAll('a.attachment-file');
+ for (const link of links) {
+ if (link.href.split('?')[1] !== 'open=true') {
+ link.href = link.href + '?open=true';
+ link.setAttribute('target', '_blank');
+ }
+ }
+ }
+
+ removeOpenQueryFromLinks() {
+ const links = this.container.querySelectorAll('a.attachment-file');
+ for (const link of links) {
+ link.href = link.href.split('?')[0];
+ link.removeAttribute('target');
+ }
+ }
+}
+
+export default AttachmentsList;
\ No newline at end of file
diff --git a/resources/js/components/index.js b/resources/js/components/index.js
index 91ccdaf3a..010ee04ba 100644
--- a/resources/js/components/index.js
+++ b/resources/js/components/index.js
@@ -2,6 +2,7 @@ import addRemoveRows from "./add-remove-rows.js"
import ajaxDeleteRow from "./ajax-delete-row.js"
import ajaxForm from "./ajax-form.js"
import attachments from "./attachments.js"
+import attachmentsList from "./attachments-list.js"
import autoSuggest from "./auto-suggest.js"
import backToTop from "./back-to-top.js"
import bookSort from "./book-sort.js"
@@ -56,6 +57,7 @@ const componentMapping = {
"ajax-delete-row": ajaxDeleteRow,
"ajax-form": ajaxForm,
"attachments": attachments,
+ "attachments-list": attachmentsList,
"auto-suggest": autoSuggest,
"back-to-top": backToTop,
"book-sort": bookSort,
diff --git a/resources/views/attachments/list.blade.php b/resources/views/attachments/list.blade.php
index 8c9be8290..f0a1354ea 100644
--- a/resources/views/attachments/list.blade.php
+++ b/resources/views/attachments/list.blade.php
@@ -1,8 +1,10 @@
-@foreach($attachments as $attachment)
-
-@endforeach
\ No newline at end of file
+
+ @foreach($attachments as $attachment)
+
+ @endforeach
+
\ No newline at end of file
diff --git a/tests/Uploads/AttachmentTest.php b/tests/Uploads/AttachmentTest.php
index 1ca9ea23b..55a5aa84f 100644
--- a/tests/Uploads/AttachmentTest.php
+++ b/tests/Uploads/AttachmentTest.php
@@ -4,11 +4,9 @@ use BookStack\Entities\Tools\TrashCan;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Uploads\Attachment;
use BookStack\Entities\Models\Page;
-use BookStack\Auth\Permissions\PermissionService;
use BookStack\Uploads\AttachmentService;
use Illuminate\Http\UploadedFile;
use Tests\TestCase;
-use Tests\TestResponse;
class AttachmentTest extends TestCase
{
@@ -57,7 +55,7 @@ class AttachmentTest extends TestCase
public function test_file_upload()
{
- $page = Page::first();
+ $page = Page::query()->first();
$this->asAdmin();
$admin = $this->getAdmin();
$fileName = 'upload_test_file.txt';
@@ -85,7 +83,7 @@ class AttachmentTest extends TestCase
public function test_file_upload_does_not_use_filename()
{
- $page = Page::first();
+ $page = Page::query()->first();
$fileName = 'upload_test_file.txt';
@@ -99,7 +97,7 @@ class AttachmentTest extends TestCase
public function test_file_display_and_access()
{
- $page = Page::first();
+ $page = Page::query()->first();
$this->asAdmin();
$fileName = 'upload_test_file.txt';
@@ -119,7 +117,7 @@ class AttachmentTest extends TestCase
public function test_attaching_link_to_page()
{
- $page = Page::first();
+ $page = Page::query()->first();
$admin = $this->getAdmin();
$this->asAdmin();
@@ -156,7 +154,7 @@ class AttachmentTest extends TestCase
public function test_attachment_updating()
{
- $page = Page::first();
+ $page = Page::query()->first();
$this->asAdmin();
$attachment = $this->createAttachment($page);
@@ -180,7 +178,7 @@ class AttachmentTest extends TestCase
public function test_file_deletion()
{
- $page = Page::first();
+ $page = Page::query()->first();
$this->asAdmin();
$fileName = 'deletion_test.txt';
$this->uploadFile($fileName, $page->id);
@@ -202,7 +200,7 @@ class AttachmentTest extends TestCase
public function test_attachment_deletion_on_page_deletion()
{
- $page = Page::first();
+ $page = Page::query()->first();
$this->asAdmin();
$fileName = 'deletion_test.txt';
$this->uploadFile($fileName, $page->id);
@@ -230,7 +228,7 @@ class AttachmentTest extends TestCase
{
$admin = $this->getAdmin();
$viewer = $this->getViewer();
- $page = Page::first(); /** @var Page $page */
+ $page = Page::query()->first(); /** @var Page $page */
$this->actingAs($admin);
$fileName = 'permission_test.txt';
@@ -253,7 +251,7 @@ class AttachmentTest extends TestCase
public function test_data_and_js_links_cannot_be_attached_to_a_page()
{
- $page = Page::first();
+ $page = Page::query()->first();
$this->asAdmin();
$badLinks = [
@@ -291,4 +289,22 @@ class AttachmentTest extends TestCase
]);
}
}
+
+ public function test_file_access_with_open_query_param_provides_inline_response_with_correct_content_type()
+ {
+ $page = Page::query()->first();
+ $this->asAdmin();
+ $fileName = 'upload_test_file.txt';
+
+ $upload = $this->uploadFile($fileName, $page->id);
+ $upload->assertStatus(200);
+ $attachment = Attachment::query()->orderBy('id', 'desc')->take(1)->first();
+
+ $attachmentGet = $this->get($attachment->getUrl(true));
+ // http-foundation/Response does some 'fixing' of responses to add charsets to text responses.
+ $attachmentGet->assertHeader('Content-Type', 'text/plain; charset=UTF-8');
+ $attachmentGet->assertHeader('Content-Disposition', "inline; filename=\"upload_test_file.txt\"");
+
+ $this->deleteUploads();
+ }
}