Skip to content

Attach From Files Developer Guide

Ed Mozley edited this page Nov 13, 2026 · 4 revisions

Attach from Files β€” Developer Guide

5.2.0 Β· #303 (from pappas81) Β· User guides: Attached documents Β§ From Files, Files Β§ Attaching to tasks and tickets Β· Related: Attached documents - Developer Guide, Files - Developer Guide

πŸ”‘ The one-line version

The Documents panel's "attach something already in FreeITSM" box now finds files in Files too, and attaches one as a link (a document of kind files that is opened through Files) or a copy (an ordinary document). Like iManage, which of the two is offered is a setting: Files β†’ Settings β†’ Sharing, files_attach_mode = link (default) | copy | both.

The hard part is not attaching. It is that two permission models meet:

  • Documents: a document is visible to everyone who can see its record.
  • Files: pessimistic. A file is reached only through a grant on its folder, View only blocks download, and in a folder you cannot open, even the name is hidden.

A link has to keep Files' rules everywhere a document can surface.

πŸ“ Files

File What
includes/services/files_attach.php FilesAttach::mode(), saveMode(), search(), attach(), viewFor()
includes/documents.php DOCUMENT_KIND_FILES = 'files'
api/documents/find.php Adds files (and files_mode) to its answer; leaves files documents out of the existing-documents search
api/documents/attach.php {files_item_id, how: link|copy, parent_type, parent_id} β†’ FilesAttach::attach()
api/documents/list.php, DocumentsService::listFor() A files row gets files_access, files_gone, open_url, and no title for a viewer without access
api/documents/download.php A files document β†’ 302 to files/?item=ID for a viewer with access, else the usual 404
includes/search/documents_index.php A files document is never indexed, and has no text of its own
api/self-service/get_ticket_detail.php, get_document.php, includes/knowledge/help_centre_documents.php AND d.kind <> 'files' - the portal never sees one
api/v1/resources/project_documents.php Content of a files document β†’ 409 in_files
api/system/debug-tools/D012_ticket_attachments.php Treats it like a web link (no bytes to check)
assets/js/documents.js (v=9), assets/css/documents.css (v=6) The "In Files" results with Link / Copy, and the row
assets/js/files-desktop.js (v=15) files/?item=ID opens that file in the viewer
tickets/index.php, assets/js/inbox.js (v=202), api/tickets/get_notes.php, assets/css/inbox.css (v=88) From Files (beside From Computer) in the Add note window; a note's Files links shown by viewFor(); window.TICKET_FILES_ATTACH_MODE
assets/js/files-picker.js, assets/css/files-picker.css (v=1) FilesPicker.open() - the file-dialog window: folder tree, file list, search, Link / Copy
Files β†’ Settings β†’ Sharing Cap::FILES_SHARING (files.sharing, sensitive), api/files/settings.php?tab=sharing

πŸ—„οΈ Data

-- documents: one new column, and a third kind
files_item_id  INT NULL   -- kind 'files': the Files item this links to; no storage_key, no bytes here

A copy is an ordinary kind = 'file' document. Its description says "Copied from Files: " and its content_hash is the version's SHA-256.

πŸ—‚οΈ In a Documents panel: From Files and Open in place

Every Documents panel (tasks, assets, projects, changes, CMDB, contracts ... - documentsPanelAssets() / FreeITSMDocuments.mount()) has a From Files button beside the attach a document already in FreeITSM box, opening the same file-dialog window as the ticket note (FilesPicker.open({mode, parentType, parentId, onPick}), mode from files_attach_mode in list.php's answer). It is shown only when can_save_to_files is true and the person can edit the panel.

Open in place. A click on a document's title or Open opens it in the Files viewer on the page:

// documents.js - the link carries what the viewer needs; Ctrl / middle-click still follow it to a new tab
<a href="download.php?id=…" data-open-files="<files item id>">   // a Files link -> FilesViewer.openItem(id)
<a href="download.php?id=…" data-open-doc="<id>" data-name="…" data-size="…">   // an upload -> FilesViewer.openUrl(...), null = download

list.php (and get_notes.php for tickets) adds files_item_id_open only for someone who can open the item - the id is never handed to someone who cannot.

🎫 On a ticket: the note window

A ticket has no Documents panel - its files are attached to notes (parent_type = 'ticket_note'). So on a ticket, Attach from Files is From Files in the Add note window:

// assets/js/files-picker.js (v=1) - a FloatWin.page() window like a file dialog
FilesPicker.open({ mode, parentType: 'ticket', parentId, onPick: function (itemId, how, name) {} });
//   left:  api/files/list.php?folder=<id>&tree=1   (folders, expanding like Explorer)
//   right: api/files/list.php?folder=<id>          (the folder's files; the folder's level decides Copy)
//   search: api/documents/find.php -> files         (across all of Files)
//   Link / Copy per window.TICKET_FILES_ATTACH_MODE; double-click or Enter = the first offered

// assets/js/inbox.js (v=202)
toggleNoteFromFiles()             // opens the picker (the From Files button)
addNoteFromFiles(itemId, how, name)  // pendingNoteFromFiles - shown with the computer files, removable
// saveNote(), once the note has an id:
fetch('../api/documents/attach.php', { body: JSON.stringify({ files_item_id, how, parent_type: 'ticket_note', parent_id: data.note_id }) })

api/tickets/get_notes.php applies the same FilesAttach::viewFor() rule to a note's files documents (no name for someone who cannot open the folder), and the portal's shared-note queries already leave kind files out - a shared note's copy reaches the requester, its link never does.

TRAP: I first wrote this as "Attach from Files on a ticket" and tested it with parent_type = 'ticket' over HTTP - which works, but nothing in the ticket screen shows a ticket-level Documents panel, so no one could reach it. Test the screen, not just the endpoint.

πŸ”— Link

// FilesAttach::attach(..., 'link')
INSERT INTO documents (kind, title, files_item_id, tenant_id, uploaded_by_id) VALUES ('files', <item name>, <item id>, ...);
documentInsertLink($conn, $docId, $parentType, $parentId, $analystId);
filesAudit($conn, $analystId, 'attach', $target, ['attached_as' => 'link', 'to' => "$parentType:$parentId"]);

Opening it never serves bytes from Documents:

// api/documents/download.php - after documentCanView()
if ($doc['kind'] === DOCUMENT_KIND_FILES) {
    $view = FilesAttach::viewFor($conn, $analystId, (int)$doc['files_item_id']);
    if (!$view['access']) documentDenied();          // the same 404 as "no such document"
    header('Location: ' . $view['open_url'], true, 302);   // BASE_URL . 'files/?item=' . id - never anything the row holds
    exit;
}

files/?item=ID calls the Files viewer, which asks the server, so the folder's permission, View only and the watermark apply exactly as when the file is opened from its folder. A link is always the latest version.

What a viewer may know

FilesAttach::viewFor($conn, $analystId, $itemId)
// -> ['access' => bool, 'title' => ?string, 'open_url' => ?string, 'gone' => bool]
// access: the Files module AND at least View on the item's folder

list.php and listFor() blank the title and description of a files row the viewer cannot open, so the panel shows "A file in Files you do not have access to". A file since deleted from Files reads "A file that has been deleted from Files". That is Files' own rule, hidden means not found, carried onto every record the file is attached to.

πŸ“„ Copy

// FilesAttach::attach(..., 'copy')
if ($lvl < FilesAcl::DOWNLOAD) throw ... 'attach it as a link instead';            // View only cannot copy
if ((int)$ver['size_bytes'] > attachmentMaxBytes()) throw ... 'attach it as a link instead';
filesStoreFor($conn, $key)->readTo($key, $tmp);                                    // local folder or S3
$s = uploadStoreBytes(file_get_contents($tmp), $it['name'], <documents dir>, ATTACHMENT_POLICY_DROP, attachmentAllowedTypes($conn));
// ... an ordinary 'file' document; filesAudit(..., 'download', ..., ['attached_as' => 'copy', ...])
  • The same gates as any upload: extension, content against extension, and size.
  • Over the attachment limit: refused before anything is read into memory.
  • Audit: recorded in Files as a download, because that is what it is.

🧭 The setting decides; the server enforces

$mode = self::mode($conn);   // link | copy | both
if (!in_array($how, ['link', 'copy'], true) || ($mode !== 'both' && $mode !== $how)) {
    throw new ServiceError('validation', 'invalid_field', 'That way of attaching is not offered - see Files -> Settings -> Sharing.');
}

search() only returns files in folders the person can open (FilesAcl::levels()), and marks each can_link / can_copy from the mode and their level. The browser only draws buttons; attach() checks everything again.

⚠️ Traps

  • TRAP: a files document has no bytes. Anything that serves, sends or indexes a document must treat kind files as "go to Files", or leave it out:
    • download.php;
    • the portal queries;
    • REST content;
    • the search index;
    • D012.
  • TRAP: never show a files document's title without FilesAttach::viewFor(). The name is Files' to reveal, and a Documents row is visible to everyone on the record.
  • TRAP: no apostrophes in SQL comments inside a prepared statement. PDO can read one as the start of a string literal and lose track of the ? placeholders after it.
  • TRAP: attach() joins a caller's transaction ($own = !$conn->inTransaction()), as FilesVersionsService::restore() does. The test found both.

πŸ§ͺ Tests

  • tests/files-attach.php (16, test database, temporary Files store, rolled back):
    • the link-only default;
    • search: a person with a grant finds the file, a person without one does not;
    • a copy refused under link-only;
    • attaching refused for a person who cannot open the folder;
    • a link stored as files with no bytes;
    • the list showing the name and files/?item= to a person with access, and nothing to a person without;
    • "both" offering a copy, and the copy having the same bytes and saying where it came from;
    • View only refused a copy;
    • a deleted item reading as deleted;
    • the three portal queries excluding files.
  • A live run on the test install (9):
    • the box finding it under "In Files";
    • the server refusing a copy too;
    • attach, then the list;
    • download.php giving 302 to files/?item=;
    • an analyst without access getting the same 404 and a list with no name;
    • "Either" attaching a copy that downloads.
  • Existing tests still green: document-permissions (22), document-search-permissions (9), knowledge-help-centre-documents (19), project-documents-api (45).
  • Chrome: documents.js loads, and files/?item= opens the viewer showing the file's contents. In the ticket screen: Add note -> From Files shows, the search finds the file, Link adds it to the pending list, and after Save the note shows it.
  • A live run for notes (6): the note search; a shared note with a link and a copy; the note listing the link by name for someone with access and without it for someone without; the portal's query for that shared note returning only the copy.

See also: Attached documents - Developer Guide Β· Files versions - Developer Guide Β· Files storage - Developer Guide

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Responses to feedback

Blue sky thinking

Bugs resolved

Pull requests

Links

Clone this wiki locally