From 9a41dc69fe8cf4170f83c36fc02396d7da77f8ad Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Tue, 1 Dec 2020 01:58:55 -0500 Subject: [PATCH] Add PDF attachment properties to Zotero.Item - .attachmentLastProcessedModificationTime - .attachmentPageIndex --- chrome/content/zotero/xpcom/data/item.js | 108 +++++++++++++++++++--- chrome/content/zotero/xpcom/data/items.js | 4 +- chrome/content/zotero/xpcom/schema.js | 4 + resource/schema/userdata.sql | 3 + test/tests/itemTest.js | 79 +++++++++++++++- 5 files changed, 181 insertions(+), 17 deletions(-) diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js index 5ce3b80b08..6cef4b2824 100644 --- a/chrome/content/zotero/xpcom/data/item.js +++ b/chrome/content/zotero/xpcom/data/item.js @@ -49,6 +49,8 @@ Zotero.Item = function(itemTypeOrID) { this._attachmentSyncState = 0; this._attachmentSyncedModificationTime = null; this._attachmentSyncedHash = null; + this._attachmentLastProcessedModificationTime = null; + this._attachmentPageIndex = null; // loadCreators this._creators = []; @@ -336,8 +338,10 @@ Zotero.Item.prototype._parseRowData = function(row) { case 'libraryID': case 'itemTypeID': case 'attachmentSyncState': - case 'attachmentSyncedHash': case 'attachmentSyncedModificationTime': + case 'attachmentSyncedHash': + case 'attachmentLastProcessedModificationTime': + case 'attachmentPageIndex': case 'createdByUserID': case 'lastModifiedByUserID': break; @@ -1722,8 +1726,9 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) { if (this._changed.attachmentData) { let sql = "REPLACE INTO itemAttachments " + "(itemID, parentItemID, linkMode, contentType, charsetID, path, " - + "syncState, storageModTime, storageHash) " - + "VALUES (?,?,?,?,?,?,?,?,?)"; + + "syncState, storageModTime, storageHash, " + + "lastProcessedModificationTime, pageIndex) " + + "VALUES (?,?,?,?,?,?,?,?,?,?,?)"; let linkMode = this.attachmentLinkMode; let contentType = this.attachmentContentType; let charsetID = this.attachmentCharset @@ -1733,6 +1738,8 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) { let syncState = this.attachmentSyncState; let storageModTime = this.attachmentSyncedModificationTime; let storageHash = this.attachmentSyncedHash; + let lastProcessedModificationTime = this.attachmentLastProcessedModificationTime; + let pageIndex = this.attachmentPageIndex; if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE && libraryType != 'user') { throw new Error("Linked files can only be added to user library"); @@ -1747,7 +1754,9 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) { path ? { string: path } : null, syncState !== undefined ? syncState : 0, storageModTime !== undefined ? storageModTime : null, - storageHash || null + storageHash || null, + lastProcessedModificationTime || null, + typeof pageIndex === 'number' ? pageIndex : null ]; yield Zotero.DB.queryAsync(sql, params); @@ -3181,6 +3190,66 @@ Zotero.defineProperty(Zotero.Item.prototype, 'attachmentSyncedHash', { }); +// +// PDF attachment properties +// +for (let name of ['lastProcessedModificationTime', 'pageIndex']) { + let prop = 'attachment' + Zotero.Utilities.capitalize(name); + + Zotero.defineProperty(Zotero.Item.prototype, prop, { + get: function () { + if (!this.isFileAttachment()) { + return undefined; + } + return this['_' + prop]; + }, + set: function (val) { + if (!this.isFileAttachment()) { + throw new Error(`${prop} can only be set for file attachments`); + } + + if (this.isEmbeddedImageAttachment()) { + throw new Error(`${prop} cannot be set for embedded-image attachments`); + } + + switch (name) { + case 'lastProcessedModificationTime': + if (typeof val != 'number') { + Zotero.debug(val, 2); + throw new Error(`${prop} must be a number`); + } + if (parseInt(val) != val || val < 0) { + Zotero.debug(val, 2); + throw new Error(`${prop} must be a timestamp in milliseconds`); + } + if (val < 10000000000) { + Zotero.logError("attachmentlastProcesedModificationTime should be a timestamp in milliseconds " + + "-- " + val + " given"); + } + break; + + case 'pageIndex': + if (typeof val != 'number') { + Zotero.debug(val, 2); + throw new Error(`${prop} must be a number`); + } + break; + } + + if (val == this['_' + prop]) { + return; + } + + if (!this._changed.attachmentData) { + this._changed.attachmentData = {}; + } + this._changed.attachmentData[name] = true; + this['_' + prop] = val; + } + }); +} + + /** * Modification time of an attachment file * @@ -3245,7 +3314,6 @@ Zotero.defineProperty(Zotero.Item.prototype, 'attachmentHash', { }); - /** * Return plain text of attachment content * @@ -3563,7 +3631,7 @@ for (let name of ['position']) { Zotero.defineProperty(Zotero.Item.prototype, 'annotationImageAttachment', { get: function () { if (!this.isImageAnnotation()) { - throw new Error("'annotationImageAttachment' is only valid for image annotations"); + return undefined; } var attachments = this.getAttachments(); if (!attachments.length) { @@ -4625,14 +4693,6 @@ Zotero.Item.prototype.fromJSON = function (json, options = {}) { this.attachmentLinkMode = linkMode; break; - case 'contentType': - this.attachmentContentType = val; - break; - - case 'charset': - this.attachmentCharset = val; - break; - case 'filename': if (val === "") { Zotero.logError("Ignoring empty attachment filename in JSON for item " + this.libraryKey); @@ -4642,8 +4702,14 @@ Zotero.Item.prototype.fromJSON = function (json, options = {}) { } break; + case 'contentType': + case 'charset': case 'path': - this.attachmentPath = val; + this['attachment' + field[0].toUpperCase() + field.substr(1)] = val; + break; + + case 'attachmentPageIndex': + this[field] = val; break; // @@ -4914,6 +4980,18 @@ Zotero.Item.prototype.toJSON = function (options = {}) { //obj.md5 = (yield this.attachmentHash) || null; } } + + // PDF attachment properties + if (this.isFileAttachment()) { + let props = ['pageIndex']; + for (let prop of props) { + let fullProp = 'attachment' + Zotero.Utilities.capitalize(prop); + let val = this[fullProp]; + if (val !== null && val !== undefined) { + obj[fullProp] = val; + } + } + } } // Notes and embedded attachment notes diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js index b512e3a7b2..99aabbc023 100644 --- a/chrome/content/zotero/xpcom/data/items.js +++ b/chrome/content/zotero/xpcom/data/items.js @@ -76,7 +76,9 @@ Zotero.Items = function() { attachmentPath: "IA.path AS attachmentPath", attachmentSyncState: "IA.syncState AS attachmentSyncState", attachmentSyncedModificationTime: "IA.storageModTime AS attachmentSyncedModificationTime", - attachmentSyncedHash: "IA.storageHash AS attachmentSyncedHash" + attachmentSyncedHash: "IA.storageHash AS attachmentSyncedHash", + attachmentLastProcessedModificationTime: "IA.lastProcessedModificationTime AS attachmentLastProcessedModificationTime", + attachmentPageIndex: "IA.pageIndex AS attachmentPageIndex" }; } }, {lazy: true}); diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js index 6811a6e080..2c49a87e82 100644 --- a/chrome/content/zotero/xpcom/schema.js +++ b/chrome/content/zotero/xpcom/schema.js @@ -3238,6 +3238,10 @@ Zotero.Schema = new function(){ yield Zotero.DB.queryAsync("CREATE TABLE itemAnnotations (\n itemID INTEGER PRIMARY KEY,\n parentItemID INT NOT NULL,\n type INTEGER NOT NULL,\n text TEXT,\n comment TEXT,\n color TEXT,\n pageLabel TEXT,\n sortIndex TEXT NOT NULL,\n position TEXT NOT NULL,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n FOREIGN KEY (parentItemID) REFERENCES itemAttachments(itemID) ON DELETE CASCADE\n)"); yield Zotero.DB.queryAsync("CREATE INDEX itemAnnotations_parentItemID ON itemAnnotations(parentItemID)"); + + yield Zotero.DB.queryAsync("ALTER TABLE itemAttachments ADD COLUMN lastProcessedModificationTime INT"); + yield Zotero.DB.queryAsync("CREATE INDEX itemAttachments_lastProcessedModificationTime ON itemAttachments(lastProcessedModificationTime)"); + yield Zotero.DB.queryAsync("ALTER TABLE itemAttachments ADD COLUMN pageIndex INT"); } // If breaking compatibility or doing anything dangerous, clear minorUpdateFrom diff --git a/resource/schema/userdata.sql b/resource/schema/userdata.sql index 703e1ec1a2..c13ac1e616 100644 --- a/resource/schema/userdata.sql +++ b/resource/schema/userdata.sql @@ -104,6 +104,8 @@ CREATE TABLE itemAttachments ( syncState INT DEFAULT 0, storageModTime INT, storageHash TEXT, + lastProcessedModificationTime INT, + pageIndex INT, FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE, FOREIGN KEY (parentItemID) REFERENCES items(itemID) ON DELETE CASCADE, FOREIGN KEY (charsetID) REFERENCES charsets(charsetID) ON DELETE SET NULL @@ -112,6 +114,7 @@ CREATE INDEX itemAttachments_parentItemID ON itemAttachments(parentItemID); CREATE INDEX itemAttachments_charsetID ON itemAttachments(charsetID); CREATE INDEX itemAttachments_contentType ON itemAttachments(contentType); CREATE INDEX itemAttachments_syncState ON itemAttachments(syncState); +CREATE INDEX itemAttachments_lastProcessedModificationTime ON itemAttachments(lastProcessedModificationTime); CREATE TABLE itemAnnotations ( itemID INTEGER PRIMARY KEY, diff --git a/test/tests/itemTest.js b/test/tests/itemTest.js index 8ef711cbfb..50bae3a3c5 100644 --- a/test/tests/itemTest.js +++ b/test/tests/itemTest.js @@ -1159,6 +1159,25 @@ describe("Zotero.Item", function () { }); + describe("#attachmentLastProcessedModificationTime", function () { + it("should save time in milliseconds", async function () { + var item = await createDataObject('item'); + var attachment = await importFileAttachment('test.pdf', { parentID: item.id }); + + var mtime = Date.now(); + attachment.attachmentLastProcessedModificationTime = mtime; + await attachment.saveTx(); + + assert.equal(attachment.attachmentLastProcessedModificationTime, mtime); + + var sql = "SELECT lastProcessedModificationTime FROM itemAttachments WHERE itemID=?"; + var dbmtime = await Zotero.DB.valueQueryAsync(sql, attachment.id); + + assert.equal(mtime, dbmtime); + }); + }); + + describe("Annotations", function () { var item; var attachment; @@ -1709,13 +1728,21 @@ describe("Zotero.Item", function () { assert.isNull(json.md5); }) - it("shouldn't include filename or path for linked_url attachments", function* () { + it("should include PDF properties", async function () { + var item = await importPDFAttachment(); + item.attachmentPageIndex = 4; + var json = item.toJSON(); + assert.propertyVal(json, 'attachmentPageIndex', 4); + }); + + it("shouldn't include filename, path, or PDF properties for linked_url attachments", function* () { var item = new Zotero.Item('attachment'); item.attachmentLinkMode = 'linked_url'; item.url = "https://www.zotero.org/"; var json = item.toJSON(); assert.notProperty(json, "filename"); assert.notProperty(json, "path"); + assert.notProperty(json, 'attachmentPageIndex'); }); it("shouldn't include various properties on embedded-image attachments", async function () { @@ -1731,6 +1758,7 @@ describe("Zotero.Item", function () { assert.notProperty(json, 'note'); assert.notProperty(json, 'charset'); assert.notProperty(json, 'path'); + assert.notProperty(json, 'attachmentPageIndex'); }); }); @@ -2343,6 +2371,55 @@ describe("Zotero.Item", function () { assert.equal(item.getField("bookTitle"), "Publication Title"); }); + it("should import attachment content type and path", async function () { + var contentType = 'application/pdf'; + var path = OS.Path.join(getTestDataDirectory().path, 'test.pdf'); + var json = { + itemType: 'attachment', + linkMode: 'linked_file', + contentType, + path + }; + var item = new Zotero.Item(); + item.libraryID = Zotero.Libraries.userLibraryID; + item.fromJSON(json, { strict: true }); + assert.propertyVal(item, 'attachmentContentType', contentType); + assert.propertyVal(item, 'attachmentPath', path); + }); + + it("should import other attachment fields", async function () { + var contentType = 'application/pdf'; + var json = { + itemType: 'attachment', + linkMode: 'linked_file', + contentType: 'text/plain', + charset: 'utf-8', + path: 'attachments:test.txt' + }; + var item = new Zotero.Item(); + item.libraryID = Zotero.Libraries.userLibraryID; + item.fromJSON(json, { strict: true }); + assert.propertyVal(item, 'attachmentCharset', 'utf-8'); + }); + + it("should import PDF fields", async function () { + var attachment = await importPDFAttachment(); + var json = attachment.toJSON(); + + var item = new Zotero.Item(); + item.libraryID = attachment.libraryID; + item.fromJSON(json, { strict: true }); + assert.propertyVal(item, 'attachmentPageIndex', null); + + json.attachmentPageIndex = 4; + + item = new Zotero.Item(); + item.libraryID = attachment.libraryID; + item.fromJSON(json, { strict: true }); + + assert.propertyVal(item, 'attachmentPageIndex', json.attachmentPageIndex); + }); + it("should import annotation fields", async function () { var attachment = await importPDFAttachment();