commit f882d1ea0ed1dccd870351df6ae52137b3513d57 Author: Mantao Huang Date: Sat Mar 7 17:11:15 2026 -0600 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..024cef7 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Zotero JS Bridge + +A Zotero plugin that exposes Zotero's internal JavaScript API as an HTTP API. This allows an external program to easily call and automate tasks like "Find Full Text", "Export file", reading items, and modifying collections directly from outside Zotero. + +> [!CAUTION] +> **SECURITY WARNING:** The `/execute` endpoint provided by this plugin allows for the execution of **arbitrary JavaScript code** within Zotero's internal environment. +> +> Because Zotero's HTTP server does not currently provide authentication mechanisms, **any application or script running locally on your machine can execute code and access, modify, or delete your Zotero data**. This plugin is intended strictly for local development, automation, and specific integrations in trusted environments. Do not expose your local Zotero HTTP port to untrusted networks. + +## Endpoints + +By default, Zotero's built-in HTTP server runs on port `23119`. You can make POST requests to the following endpoints: + +### `/zotero-js-bridge/execute` + +Executes arbitrary JavaScript within Zotero's context. + +**Request:** `POST http://127.0.0.1:23119/zotero-js-bridge/execute` +**Body:** +```json +{ + "code": "return await Zotero.Items.getAll();" +} +``` + +### `/zotero-js-bridge/findPDFsForItems` + +A specialized endpoint to trigger Zotero's "Find Available PDFs" feature for a specific list of item IDs. + +**Request:** `POST http://127.0.0.1:23119/zotero-js-bridge/findPDFsForItems` +**Body:** +```json +{ + "itemIds": [123, 124, 125] +} +``` + +## Development + +- `manifest.json`: Zotero plugin manifest. +- `bootstrap.js`: Handles plugin lifecycle and endpoint registration. diff --git a/bootstrap.js b/bootstrap.js new file mode 100644 index 0000000..ba28d6c --- /dev/null +++ b/bootstrap.js @@ -0,0 +1,97 @@ +if (typeof Zotero === 'undefined') { + var Zotero; +} + +function log(msg) { + Zotero.debug("Zotero JS Bridge: " + msg); +} + +function install() { } + +function uninstall() { } + +function startup({ id, version, resourceURI, rootURI }) { + log("Starting up..."); + + // Register generic execution endpoint + Zotero.Server.Endpoints["/zotero-js-bridge/execute"] = function () { }; + Zotero.Server.Endpoints["/zotero-js-bridge/execute"].prototype = { + supportedMethods: ["POST"], + supportedDataTypes: ["application/json", "application/x-www-form-urlencoded"], + init: async function (options, sendResponseCallback) { + try { + let requestData = typeof options === 'string' ? options : (options.data || options); + if (typeof requestData === 'string') { + try { + requestData = JSON.parse(requestData); + } catch (e) { + // Handled later or ignored if not JSON + } + } + + let code = requestData.code; + if (!code) { + sendResponseCallback(400, "application/json", JSON.stringify({ success: false, error: "Missing 'code' in request body" })); + return; + } + + // Execute code in Zotero's context, providing async support + const asyncFunc = new Function('Zotero', `return (async () => { ${code} })();`); + const result = await asyncFunc(Zotero); + + sendResponseCallback(200, "application/json", JSON.stringify({ success: true, result: result })); + } catch (e) { + log("Error in /execute: " + e); + sendResponseCallback(500, "application/json", JSON.stringify({ success: false, error: e.message || e.toString() })); + } + } + }; + + // Register endpoint to find full text / PDFs for items + Zotero.Server.Endpoints["/zotero-js-bridge/findPDFsForItems"] = function () { }; + Zotero.Server.Endpoints["/zotero-js-bridge/findPDFsForItems"].prototype = { + supportedMethods: ["POST"], + supportedDataTypes: ["application/json", "application/x-www-form-urlencoded"], + init: async function (options, sendResponseCallback) { + try { + let requestData = typeof options === 'string' ? options : (options.data || options); + if (typeof requestData === 'string') { + try { + requestData = JSON.parse(requestData); + } catch (e) { + // Handled later + } + } + + let itemIds = requestData.itemIds; + if (!Array.isArray(itemIds)) { + sendResponseCallback(400, "application/json", JSON.stringify({ success: false, error: "Missing or invalid 'itemIds' in request body" })); + return; + } + + // Retrieve items and filter out nulls/falsy values + let items = await Zotero.Items.getAsync(itemIds); + if (!items) items = []; + items = items.filter(item => item != null); + + if (items.length > 0) { + Zotero.Attachments.addAvailablePDFs(items); + } + + sendResponseCallback(200, "application/json", JSON.stringify({ + success: true, + message: `Started finding PDFs for ${items.length} items.` + })); + } catch (e) { + log("Error in /findPDFsForItems: " + e); + sendResponseCallback(500, "application/json", JSON.stringify({ success: false, error: e.message || e.toString() })); + } + } + }; +} + +function shutdown() { + log("Shutting down..."); + delete Zotero.Server.Endpoints["/zotero-js-bridge/execute"]; + delete Zotero.Server.Endpoints["/zotero-js-bridge/findPDFsForItems"]; +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..daee210 --- /dev/null +++ b/manifest.json @@ -0,0 +1,14 @@ +{ + "manifest_version": 2, + "name": "Zotero JS Bridge", + "version": "1.0.0", + "description": "Exposes Zotero's internal JavaScript API as an HTTP API.", + "applications": { + "zotero": { + "id": "zotero-js-bridge@example.com", + "update_url": "https://github.com/zotero-folder/releases/download/release/updates.json", + "strict_min_version": "7.0.0", + "strict_max_version": "8.*" + } + } +} \ No newline at end of file