Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/zotero/zotero-connectors/llms.txt

Use this file to discover all available pages before exploring further.

When a translator finishes extracting bibliographic metadata from a web page, that data begins a second journey: it must be packaged, enriched with browser cookies for authentication-gated attachments, and delivered either to the Zotero desktop client over its local HTTP server or — when the client is unavailable — to zotero.org via the Web API. This pipeline is split across two files: src/common/itemSaver.js runs in the inject (content) script context where it has direct DOM access, and src/common/itemSaver_background.js runs in the background script context where it can make arbitrary network requests and issue connector API calls.

Overview: Two-Phase Save

1

Inject-side (itemSaver.js)

The ItemSaver class is constructed in the inject script. Its saveItems() method first attempts _saveToZotero(). If that fails with status 0 (desktop client offline), it falls back to _saveToServer().
2

Package items and add cookies

The payload is assembled: translated items, a session ID, the current page URI, and the active proxy. callMethodWithCookies() retrieves all browser cookies for the tab’s URL and attaches them as detailedCookies in the request body so the desktop client can authenticate attachment downloads.
3

Send to connector server

Zotero.Connector.callMethodWithCookies("saveItems", payload) POSTs the items to the Zotero desktop client at http://localhost:23119/connector/saveItems.
4

Save attachments

Depending on which version of Zotero is running, attachments are saved via Zotero directly or uploaded by the connector background page. Snapshots are captured via SingleFile.

ItemSaver Constructor Options

let saver = new ItemSaver({
  sessionID: "abc12345",   // Unique ID for this save session
  proxy:     proxyObject,  // Zotero.Proxy instance (or null)
  baseURI:   "https://example.com/article",  // Page URI
  itemType:  "multiple"    // "multiple" for item-selector saves
});
The sessionID ties together item creation, attachment uploads, and progress polling so that the desktop client can associate all parts of a save into one session.

Save Flow to Zotero Desktop

saveItems(items, attachmentCallback, itemsDoneCallback)

Entry point. Calls _saveToZotero() and catches status === 0 to trigger the server fallback:
saveItems: async function(items, attachmentCallback, itemsDoneCallback=()=>0) {
  try {
    return await this._saveToZotero(items, attachmentCallback, itemsDoneCallback);
  }
  catch (e) {
    if (e.status == 0) {
      return this._saveToServer(items, attachmentCallback, itemsDoneCallback);
    }
    throw e;
  }
}

Attachment Filtering

Before sending, _saveToZotero() filters item attachments based on preferences retrieved from the connector:
  • automaticSnapshots — if false, text/html attachments are dropped.
  • downloadAssociatedFiles — if false, non-HTML attachments are dropped.
  • PDF pages (document.contentType === 'application/pdf') are automatically appended as a Full Text PDF attachment.

Saving Attachments

The connector supports two attachment workflows depending on the connected Zotero version:

Zotero handles downloads (legacy)

When supportsAttachmentUpload is false, HTML snapshots are sent to the desktop client via _executeSingleFile() (which posts to the saveSingleFile endpoint). Zotero handles other attachment downloads itself. The connector calls _pollForProgress() to relay status updates back to the progress window.

Connector uploads directly (modern)

When supportsAttachmentUpload is true, the background page fetches each PDF or EPUB as an ArrayBuffer and posts it to saveAttachment with binary data in the request body and metadata in the X-Metadata header.

Attachment Delivery Methods

Zotero.ItemSaver.saveAttachmentToZotero(attachment, sessionID, tab)

Defined in itemSaver_background.js. Downloads the attachment as an ArrayBuffer and POSTs it directly to the Zotero desktop client:
// Metadata travels in the X-Metadata request header
let metadata = JSON.stringify({
  id: attachment.id,
  url: attachment.url,
  contentType: attachment.mimeType,
  parentItemID: attachment.parentItem,
  title: this._rfc2047Encode(attachment.title),  // RFC 2047-encodes non-ASCII
});

return Zotero.Connector.callMethod({
  method: "saveAttachment",
  headers: {
    "Content-Type": `${attachment.mimeType}`,
    "X-Metadata": metadata
  },
  queryString: `sessionID=${sessionID}`
}, arrayBuffer);
Non-ASCII characters in attachment titles are encoded using RFC 2047 quoted-printable encoding (=?UTF-8?Q?...?=) so they can be safely transmitted in HTTP headers.

Zotero.ItemSaver.saveStandaloneAttachmentToZotero(attachment, sessionID, tab)

Same pattern as saveAttachmentToZotero, but calls the saveStandaloneAttachment endpoint and does not require a parentItemID. Used when intercepting direct PDF/file downloads via contentTypeHandler.js. Default timeout is 60 seconds.

Zotero.ItemSaver.saveAttachmentToServer(attachment, tab)

Uploads an attachment to zotero.org when the desktop client is offline. The workflow is:
  1. _createServerAttachmentItem(attachment) — creates an attachment item via Zotero.API.createItem(), receives an item key.
  2. Binary data for HTML snapshots is encoded as UTF-8; Safari fetches binary attachments in the content script (where the user’s cookies are present) and passes them as base64.
  3. Zotero.API.uploadAttachment(attachment) — posts to the Web API file upload endpoint.

Snapshot Saving via SingleFile

HTML snapshots are captured using the SingleFile library (src/common/singlefile.js). The inject-side method _executeSingleFile() calls:
let data = { items: this._items, sessionID: this._sessionID };
data.snapshotContent = await Zotero.SingleFile.retrievePageData();
data.url = this._items[0].url || document.location.href;
data.title = this._snapshotAttachment.title;
await Zotero.Connector.saveSingleFile(
  { method: "saveSingleFile", headers: {"Content-Type": "application/json"} },
  data
);
retrievePageData() injects the single-file-hooks-frames.js script into the page (for deferred image loading support) and then calls singlefile.getPageData(). Fetch requests within SingleFile are handled by Zotero.SingleFile.singleFileFetch(), which throttles concurrent requests (capped at 10 via throttleAsync) to avoid crashing the extension on pages with many resources.
Browser cookies are critical for downloading authentication-gated attachments (journal PDFs, institutional repository files, etc.). callMethodWithCookies() collects all cookies for the tab URL using browser.cookies.getAll() and serialises them into a detailedCookies string:
cookieHeader += '\n' + cookies[i].name + '=' + cookies[i].value
  + ';Domain=' + cookies[i].domain
  + (cookies[i].path ? ';Path=' + cookies[i].path : '')
  + (cookies[i].hostOnly ? ';hostOnly' : '')
  + (cookies[i].secure ? ';secure' : '');
Firefox with First-Party Isolation enabled may return no cookies if firstPartyDomain is not specified. The connector sets firstPartyDomain: null for Firefox ≥ 59, which may result in attachment downloads failing when FPI is active.

Bot-Detection Bypass

For whitelisted domains (sciencedirect.com, pdf.sciencedirectassets.com, ncbi.nlm.nih.gov), the background page can attempt to bypass JavaScript-based bot protection via two escalating strategies:
  1. Hidden iframe — loads a monitor page in a hidden <iframe> within the current tab and waits up to 5 seconds for the PDF URL to appear in a webRequest event.
  2. Popup window — opens a centred browser popup where the user can manually solve a CAPTCHA, then monitors for the PDF download URL.

Progress Polling: _pollForProgress()

When using the legacy attachment workflow, the inject script polls the sessionProgress endpoint every second (up to 60 polls) to relay attachment download status back to the progress window:
var response = await Zotero.Connector.callMethod(
  "sessionProgress", { sessionID: this._sessionID }
)
// response.items[].attachments[].progress → forwarded to attachmentCallback

Save Flow Diagram

Translated items


ItemSaver.saveItems()

       ├─ Zotero online? ──yes──► callMethodWithCookies("saveItems", payload)
       │                               │
       │                               ├── zoteroSupportsAttachmentUpload = true
       │                               │       └── saveAttachmentsToZotero()
       │                               │             ├── saveAttachmentToZotero() [background, ArrayBuffer]
       │                               │             └── _executeSingleFile() [snapshot via SingleFile]
       │                               │
       │                               └── zoteroSupportsAttachmentUpload = false
       │                                       └── saveAttachmentsViaZotero()
       │                                             ├── _executeSingleFile()
       │                                             └── _pollForProgress()

       └─ Zotero offline? ─yes─► _saveToServer()

                                       └── Zotero.API.createItem()
                                             └── saveAttachmentToServer()
                                                   └── Zotero.API.uploadAttachment()

Build docs developers (and LLMs) love