From 9fb88bf0877ff0ee0d602d6f3d7e6b48dbb34922 Mon Sep 17 00:00:00 2001 From: pi-dal Date: Sat, 9 May 2026 19:21:11 +0800 Subject: [PATCH] fix(attachments): add PMCID fallback for PubMed resolver lookup PubMed saves can include a PMCID even when DOI-based full-text lookup fails to produce a PDF. In that case the connector asks the desktop attachment resolver flow for fallback sources, but getFileResolvers() only considered DOI and URL data, so Zotero never tried the matching PMC article URL. Treat PMCID as a valid signal for file lookup eligibility and add a PubMed Central OA resolver before DOI-based OA lookup. Cover the new fallback path in attachment resolver tests and the connector hasAttachmentResolvers endpoint. --- chrome/content/zotero/xpcom/attachments.js | 15 ++++++- test/tests/attachmentsTest.js | 42 ++++++++++++++++++++ test/tests/server_connectorTest.js | 46 ++++++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/chrome/content/zotero/xpcom/attachments.js b/chrome/content/zotero/xpcom/attachments.js index 54e3939706..9ae7aef4b0 100644 --- a/chrome/content/zotero/xpcom/attachments.js +++ b/chrome/content/zotero/xpcom/attachments.js @@ -1239,9 +1239,10 @@ Zotero.Attachments = new function () { this.canFindFileForItem = function (item) { + let pmcid = item.getField('PMCID') || item.getExtraField('PMCID'); return item.isRegularItem() && !item.isFeedItem - && (!!item.getField('DOI') || !!item.getField('url') || !!item.getExtraField('DOI')) + && (!!item.getField('DOI') || !!item.getField('url') || !!item.getExtraField('DOI') || !!pmcid) && this.FIND_AVAILABLE_FILE_TYPES.every(type => item.numFileAttachmentsWithContentType(type) == 0); }; @@ -1275,7 +1276,12 @@ Zotero.Attachments = new function () { var resolvers = []; var doi = item.getField('DOI') || item.getExtraField('DOI'); + var pmcid = item.getField('PMCID') || item.getExtraField('PMCID'); doi = Zotero.Utilities.cleanDOI(doi); + if (pmcid) { + let matches = pmcid.match(/\bPMC\d+\b/i); + pmcid = matches && matches[0].toUpperCase(); + } if (useDOI && doi) { doi = Zotero.Utilities.cleanDOI(doi); @@ -1300,6 +1306,13 @@ Zotero.Attachments = new function () { } } + if (useOA && pmcid) { + resolvers.push({ + pageURL: `https://pmc.ncbi.nlm.nih.gov/articles/${pmcid}/`, + accessMethod: 'oa' + }); + } + if (useOA && doi) { resolvers.push(async function () { let urls = await Zotero.Utilities.Internal.getOpenAccessPDFURLs(doi); diff --git a/test/tests/attachmentsTest.js b/test/tests/attachmentsTest.js index b4237d178f..ebc124fc40 100644 --- a/test/tests/attachmentsTest.js +++ b/test/tests/attachmentsTest.js @@ -1031,6 +1031,48 @@ describe("Zotero.Attachments", function () { assert.equal(await OS.File.stat(attachment.getFilePath()).size, pdfSize); }); + it("should include a PubMed Central resolver from a PMCID in Extra", async function () { + var item = createUnsavedDataObject('item', { itemType: 'journalArticle' }); + item.setField('title', 'Test'); + item.setField('extra', 'PMCID: PMC9262588'); + await item.saveTx(); + + var resolvers = Zotero.Attachments.getFileResolvers(item, ['oa']); + + assert.deepEqual(resolvers, [ + { + pageURL: 'https://pmc.ncbi.nlm.nih.gov/articles/PMC9262588/', + accessMethod: 'oa' + } + ]); + }); + + it("should include a PubMed Central resolver alongside DOI OA resolvers", async function () { + var item = createUnsavedDataObject('item', { itemType: 'journalArticle' }); + item.setField('title', 'Test'); + item.setField('DOI', '10.1093/nar/gkac173'); + item.setField('extra', 'PMCID: PMC9262588'); + await item.saveTx(); + + var resolvers = Zotero.Attachments.getFileResolvers(item, ['oa']); + + assert.lengthOf(resolvers, 2); + assert.deepEqual(resolvers[0], { + pageURL: 'https://pmc.ncbi.nlm.nih.gov/articles/PMC9262588/', + accessMethod: 'oa' + }); + assert.isFunction(resolvers[1]); + }); + + it("should allow finding files for an item with a PMCID in Extra", async function () { + var item = createUnsavedDataObject('item', { itemType: 'journalArticle' }); + item.setField('title', 'Test'); + item.setField('extra', 'PMCID: PMC9262588'); + await item.saveTx(); + + assert.isTrue(Zotero.Attachments.canFindFileForItem(item)); + }); + it("should add a PDF from a URL", async function () { var url = pageURL1; var item = createUnsavedDataObject('item', { itemType: 'journalArticle' }); diff --git a/test/tests/server_connectorTest.js b/test/tests/server_connectorTest.js index e5223f7051..c9bcfbd0a0 100644 --- a/test/tests/server_connectorTest.js +++ b/test/tests/server_connectorTest.js @@ -701,6 +701,52 @@ describe("Connector Server", function () { assert.isTrue(JSON.parse(response.responseText)); }); + it("should respond with 'true' if the item has a PMCID fallback", async function () { + const sessionID = Zotero.Utilities.randomString(); + const itemID = Zotero.Utilities.randomString(); + const body = { + sessionID, + items: [ + { + id: itemID, + itemType: "journalArticle", + title: "Test Article with PMCID", + extra: "PMCID: PMC9262588", + } + ] + }; + + let response = await httpRequest( + "POST", + connectorServerPath + "/connector/saveItems", + { + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(body) + } + ); + + assert.equal(response.status, 201); + + response = await httpRequest( + "POST", + connectorServerPath + "/connector/hasAttachmentResolvers", + { + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + sessionID, + itemID + }), + } + ); + + assert.equal(response.status, 200); + assert.isTrue(JSON.parse(response.responseText)); + }); + it("should respond with 'false' if the item has no OA attachments", async function () { const sessionID = Zotero.Utilities.randomString(); const itemID = Zotero.Utilities.randomString();