Skip to content

[Core]: Upload all document types (not just images) #371

Open

Description

@kiuX89

Hello, thank you for your work on this software :)

I would like to make a recommendation following a specific need, for some of my customers I send them documents and it would be nice if there was the possibility of having an insert with the documents for my customers to download. The little plus would also be to have a user folder where I can put large files directly inside without going through the web interface.

Sincerely

Activity

  1. nielsdrost7 commented on Dec 3, 2024

    @nielsdrost7
    Collaborator

    @kiuX89 thank you for your feature request.

    Can you give a bit more explanation on what you would like with the documents for your customers?

    Is it exactly the same document per customer?
    Or is there personal information per customer in each document?

    Is it Word Documents or PDF Documents?
    And when the customers are logged in they need to see their own documents?

    Are you uploading the documents per customer?

  2. kiuX89 commented on Dec 3, 2024

    @kiuX89
    Author

    Basically, I'd like to upload documents of all types from the web interface, I have PDF, Word (docx), ZIP, Tar.gz. For PDF and Word, putting them in the interface would be simple, but for ZIP or Tar.gz files, it would be more practical to put them in a local folder bearing the user's name. Not all documents are identical, and yes, there is some personal/secret information.

    When customers log on, they should see a document insert, so that when they click on it, they find each document and can click on the download button.

  3. kiuX89 commented on Dec 11, 2024

    @kiuX89
    Author

    Hello, do you have any idea how long it would take to code this feature?

  4. nielsdrost7 commented on Dec 11, 2024

    @nielsdrost7
    Collaborator

    So... a user has committed lots of code, amongst other things he committed a user documents feature.

    I've created a branch and a pull-request from it:
    https://github.com/InvoicePlane/InvoicePlane/pull/1137/files

    Personally with what you like (with the files stored per user) i really would have liked to have made it in V2 of InvoicePlane

    This PR doesn't have files stored per user (I think)

    These are the allowed types for now:
    odt|ods|pdf|doc|docx|xls|xlsx|jpeg|jpg|png|gif|tiff

    I think you wanted some more types, they can be easily added to the code.

    So if you want to try out that pull-request (that branch) and let us know what you think?

  5. linked a pull request that will close this issueChrissie: Client_Documents #1137on Dec 11, 2024
  6. changed the title [-]Documents[/-] [+][Feature-Request]: Make uploading of all kinds of Documents possible[/+] on Aug 7, 2025
  7. nielsdrost7 commented on Jun 13, 2026

    @nielsdrost7
    Collaborator

    Planning Note — v2 Implementation

    Story: As a company admin, I want to upload any type of document (PDFs, contracts, receipts, images) and attach them to invoices, quotes, clients, or expenses, so that all relevant files are stored alongside the corresponding record without needing an external document manager.

    v2 context: v2 has an Upload model in the Core module (Modules/Core/Models/Upload.php). It handles file storage via Laravel's Storage facade. The model needs to support more MIME types and be attachable to all major document models via a polymorphic relationship.

    Acceptance Criteria:

    • The uploads table stores files with a polymorphic uploadable_type / uploadable_id.
    • Accepted MIME types: PDFs, common image types (JPEG, PNG, GIF, WebP), MS Office (docx, xlsx), plain text, ZIP — configurable via a MIME type allow-list in settings.
    • File size limit configurable in Settings (default 10 MB).
    • Uploading is available on: Invoice edit, Quote edit, Client (Relation) edit, Expense edit.
    • A "Documents" section appears at the bottom of each supported edit page.
    • Files can be downloaded and deleted by the company admin.
    • Files are stored in the tenant-scoped disk path (storage/company/{company_id}/uploads/).

    Implementation:

    • Extend the Upload model with uploadable() morphTo relationship.
    • Add FileUpload::make('documents') section to Invoice/Quote/Client/Expense Filament schemas.
    • Add an UploadService::attachToModel(UploadedFile, Model) method that validates MIME type and stores the file.
    • MIME validation: use $file->getMimeType() against a configured allow-list.

    Test Scenarios (PHPUnit + Livewire):

    // PDF attaches to invoice successfully
    /* Arrange */ $invoice = Invoice::factory()->for($this->company)->create();
                  $file = UploadedFile::fake()->create('contract.pdf', 200, 'application/pdf');
    /* Act */     app(UploadService::class)->attachToModel($file, $invoice);
    /* Assert */  $this->assertDatabaseHas('uploads',
                      ['uploadable_type' => Invoice::class, 'uploadable_id' => $invoice->id]);
    // Disallowed MIME type is rejected
    /* Arrange */ $invoice = Invoice::factory()->for($this->company)->create();
                  $file = UploadedFile::fake()->create('malware.exe', 100, 'application/x-msdownload');
    /* Act & Assert */ $this->expectException(\InvalidArgumentException::class);
                  app(UploadService::class)->attachToModel($file, $invoice);

    Generated by Claude Code

  8. changed the title [-][Feature-Request]: Make uploading of all kinds of Documents possible[/-] [+][Core]: Upload all document types (not just images)[/+] on Jun 13, 2026
  9. added this to the v2.0.0-alpha milestone on Jun 13, 2026
  10. nielsdrost7 commented on Jul 16, 2026

    @nielsdrost7
    Collaborator

    Test Suggestions

    Added to support future implementation of this request. The issue body is unchanged.

    This may already be partly solved: the only file-upload field found in the invoice form, FileUpload::make('attachments')->multiple() in Modules/Invoices/Filament/Company/Resources/Invoices/Schemas/InvoiceForm.php, has no ->image() or ->acceptedFileTypes() restriction, so it already accepts any file type today (only the company-branding fields settings.login_logo and settings.invoice_logo in Modules/Core/Filament/Admin/Pages/Settings.php are restricted with ->image()). What's missing: Modules\Core\Models\Upload / UploadDetail exist as a generic polymorphic attachment model but a repo-wide search shows nothing ever calls Upload::create() — there is no UploadService, so the models are dormant and the invoice attachments field just writes to disk via Filament's raw file storage, not through the Upload model. There is also no per-user "drop-in" storage folder feature (the second part of this request) anywhere in the codebase.

    • PHPUnit: it_accepts_a_non_image_document_on_the_invoice_attachments_field()
    #[Test]
    public function it_accepts_a_non_image_document_on_the_invoice_attachments_field(): void
    {
        /* Arrange */
        Storage::fake('public');
        $invoice = Invoice::factory()->for($this->company)->create();
        $file    = UploadedFile::fake()->create('contract.pdf', 100, 'application/pdf');
    
        /* Act */
        Livewire::actingAs($this->user)
            ->test(EditInvoice::class, ['record' => $invoice->getRouteKey(), 'tenant' => Str::lower($this->company->search_code)])
            ->fillForm(['attachments' => [$file]])
            ->call('save')
            ->assertHasNoFormErrors();
    }
    • PHPUnit: it_creates_an_upload_model_record_for_a_client_document() (aspirational — wires the dormant Upload model into the flow)
    #[Test]
    public function it_creates_an_upload_model_record_for_a_client_document(): void
    {
        /* Arrange */
        $client = Relation::factory()->for($this->company)->create();
        $file   = UploadedFile::fake()->create('spec.docx', 200);
    
        /* Act */
        app(UploadService::class)->store($client, $file);
    
        /* Assert */
        $this->assertDatabaseHas('uploads', ['uploadable_id' => $client->id, 'uploadable_type' => Relation::class]);
    }
    • PHPUnit: it_lists_documents_uploaded_directly_to_a_users_storage_folder() (aspirational)
    #[Test]
    public function it_lists_documents_uploaded_directly_to_a_users_storage_folder(): void
    {
        /* Arrange */
        Storage::fake('local');
        Storage::disk('local')->put("users/{$this->user->id}/inbox/large-file.zip", 'contents');
    
        /* Act */
        $files = app(UserStorageFolderService::class)->list($this->user);
    
        /* Assert */
        $this->assertContains('large-file.zip', $files->pluck('name'));
    }

    Edge cases worth covering: a filename with no extension or a disguised MIME type (server-side MIME sniffing, not just the client-visible file input accept attribute, since that's trivially bypassed); and disk quota per company/user if a large-file drop-in folder is added.

  11. nielsdrost7 commented on Jul 18, 2026

    @nielsdrost7
    Collaborator

    Original request: "for some of my customers I send them documents and it would be nice if there was the possibility of having an insert with the documents for my customers to download. The little plus would also be to have a user folder where I can put large files directly inside without going through the web interface."

    Client-facing document downloads: documents attached to an invoice or quote (any allowed type, not just images) are listed on the customer-facing (guest) document view and downloadable there — without an account, gated by the document's guest URL key. This is the v2 port of v1's guest attachment surface, which #510 (admin-side Documents module) does not cover.

    v1 behavioral spec:

    • application/modules/guest/controllers/Get.php — show_files($url_key) returns the attachment list ONLY when the url_key belongs to a guest-visible invoice/quote (is_url_key_guest_visible(), 32-char url_key check); get_file()/attachment() stream the download with path-traversal and filename validation.
    • application/modules/guest/views/invoices_view.php / quotes_view.php — read-only Dropzone listing the files.
    • Admin upload side: application/modules/upload/controllers/Upload.php (extension allowlist jpg/jpeg/png/pdf/gif/webp, MIME validation, EXIF stripping).

    v2 groundwork already present: Modules/Core/Models/Attachment.php has client_visibility (boolean) and url_key columns — unused; Modules/Core/Observers/AttachmentObserver.php is empty stubs.

    Specific

    • Extend the allowed upload types beyond images: at minimum pdf, jpg, jpeg, png, gif, webp (v1 allowlist), plus common documents (doc/docx, xls/xlsx, odt/ods, txt, zip) behind a configurable allowlist.
    • Each attachment on an invoice/quote has a "visible to client" toggle (persisted to attachments.client_visibility).
    • The guest/public view of an invoice or quote (public-link feature) lists client-visible attachments with download links.
    • Download endpoint requires the document's unguessable url_key; returns 404/empty for non-guest-visible documents or attachments with client_visibility = false.
    • Downloads stream from private storage (no direct public disk URLs), with correct Content-Type and Content-Disposition.
    • Optional (original request's "little plus", may split out): a per-company server-side drop folder whose files can be linked to a client without going through the web UI.

    Measurable

    • A PDF and a DOCX can be attached to an invoice and marked client-visible.
    • The guest view lists exactly the client-visible attachments; hidden ones never appear in the listing or via direct URL.
    • Wrong/forged url_key yields no file listing and no download.
    • PHPUnit covers: visible download OK, hidden attachment 404, wrong url_key 404, disallowed extension rejected at upload.

    Achievable

    1. Depends on the guest/public document view (client portal / public links work); depends on [Documents]: Epic: Global Document Management #510 for the admin upload UI.
    2. Add client_visibility toggle to the attachment management UI; implement AttachmentObserver file cleanup on delete.
    3. Signed or url_key-gated download controller/route streaming from the private disk.

    Relevant

    Restores a v1 capability (customers self-serve supporting documents from the invoice/quote link) that reduces email back-and-forth; the client_visibility schema was built for exactly this.

    Time-Bound

    Implement in the sprint following #510's admin-side upload UI.

    Arrange

    • An invoice with a valid guest url_key; attachment A (pdf, client_visibility=true) and attachment B (png, client_visibility=false).

    Act

    • Open the guest invoice URL as an unauthenticated visitor; request the listing and both files (B via forged direct URL).

    Assert

    • Listing shows attachment A only.
    • A downloads with correct filename/Content-Type from private storage.
    • B and any forged/wrong url_key request return 404 (no file disclosure, no path traversal).
    • Upload of a disallowed extension (e.g. .exe) is rejected with a validation error.
    • Attachment file is removed from storage when the Attachment record is deleted (observer).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    refinedRefined with SMART plan / test suggestions

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions