diff --git a/chrome/content/zotero/xpcom/data/collection.js b/chrome/content/zotero/xpcom/data/collection.js index db205fb4b5..1ae5aa4b1e 100644 --- a/chrome/content/zotero/xpcom/data/collection.js +++ b/chrome/content/zotero/xpcom/data/collection.js @@ -279,168 +279,136 @@ Zotero.Collection.prototype.getChildItems = function (asIDs, includeDeleted) { return objs; } +Zotero.Collection.prototype._initSave = Zotero.Promise.coroutine(function* (env) { + if (!this.name) { + throw new Error('Collection name is empty'); + } + + return Zotero.Collection._super.prototype._initSave.apply(this, arguments); +}); -Zotero.Collection.prototype.save = Zotero.Promise.coroutine(function* () { - try { - Zotero.Collections.editCheck(this); +Zotero.Collection.prototype._saveData = Zotero.Promise.coroutine(function* (env) { + var isNew = env.isNew; + + var collectionID = env.id = this._id = this.id ? this.id : yield Zotero.ID.get('collections'); + var libraryID = env.libraryID = this.libraryID; + var key = env.key = this._key = this.key ? this.key : this._generateKey(); + + Zotero.debug("Saving collection " + this.id); + + // Verify parent + if (this._parentKey) { + let newParent = Zotero.Collections.getByLibraryAndKey( + this.libraryID, this._parentKey + ); - if (!this.name) { - throw new Error('Collection name is empty'); + if (!newParent) { + throw new Error("Cannot set parent to invalid collection " + this._parentKey); } - if (Zotero.Utilities.isEmpty(this._changed)) { - Zotero.debug("Collection " + this.id + " has not changed"); - return false; + if (newParent.id == this.id) { + throw new Error('Cannot move collection into itself!'); } - var isNew = !this.id; - - // Register this item's identifiers in Zotero.DataObjects on transaction commit, - // before other callbacks run - var collectionID, libraryID, key; - if (isNew) { - var transactionOptions = { - onCommit: function () { - Zotero.Collections.registerIdentifiers(collectionID, libraryID, key); - } - }; - } - else { - var transactionOptions = null; + if (this.id && (yield this.hasDescendent('collection', newParent.id))) { + throw ('Cannot move collection "' + this.name + '" into one of its own descendents'); } - return Zotero.DB.executeTransaction(function* () { - // how to know if date modified changed (in server code too?) - - collectionID = this._id = this.id ? this.id : yield Zotero.ID.get('collections'); - libraryID = this.libraryID; - key = this._key = this.key ? this.key : this._generateKey(); - - Zotero.debug("Saving collection " + this.id); - - // Verify parent - if (this._parentKey) { - let newParent = Zotero.Collections.getByLibraryAndKey( - this.libraryID, this._parentKey - ); - - if (!newParent) { - throw new Error("Cannot set parent to invalid collection " + this._parentKey); - } - - if (newParent.id == this.id) { - throw new Error('Cannot move collection into itself!'); - } - - if (this.id && (yield this.hasDescendent('collection', newParent.id))) { - throw ('Cannot move collection "' + this.name + '" into one of its own descendents'); - } - - var parent = newParent.id; - } - else { - var parent = null; - } - - var columns = [ - 'collectionID', - 'collectionName', - 'parentCollectionID', - 'clientDateModified', - 'libraryID', - 'key', - 'version', - 'synced' - ]; - var sqlValues = [ - collectionID ? { int: collectionID } : null, - { string: this.name }, - parent ? parent : null, - Zotero.DB.transactionDateTime, - this.libraryID ? this.libraryID : 0, - key, - this.version ? this.version : 0, - this.synced ? 1 : 0 - ]; - if (isNew) { - var placeholders = columns.map(function () '?').join(); - - var sql = "REPLACE INTO collections (" + columns.join(', ') + ") " - + "VALUES (" + placeholders + ")"; - var insertID = yield Zotero.DB.queryAsync(sql, sqlValues); - if (!collectionID) { - collectionID = insertID; - } - } - else { - columns.shift(); - sqlValues.push(sqlValues.shift()); - let sql = 'UPDATE collections SET ' - + columns.map(function (x) x + '=?').join(', ') - + ' WHERE collectionID=?'; - yield Zotero.DB.queryAsync(sql, sqlValues); - } - - if (this._changed.parentKey) { - var parentIDs = []; - if (this.id && this._previousData.parentKey) { - parentIDs.push(Zotero.Collections.getIDFromLibraryAndKey( - this.libraryID, this._previousData.parentKey - )); - } - if (this.parentKey) { - parentIDs.push(Zotero.Collections.getIDFromLibraryAndKey( - this.libraryID, this.parentKey - )); - } - if (this.id) { - Zotero.Notifier.trigger('move', 'collection', this.id); - } - } - - if (isNew && this.libraryID) { - var groupID = Zotero.Groups.getGroupIDFromLibraryID(this.libraryID); - var group = Zotero.Groups.get(groupID); - group.clearCollectionCache(); - } - - if (isNew) { - Zotero.Notifier.trigger('add', 'collection', this.id); - } - else { - Zotero.Notifier.trigger('modify', 'collection', this.id, this._previousData); - } - - // Invalidate cached child collections - if (parentIDs) { - Zotero.Collections.refreshChildCollections(parentIDs); - } - - // New collections have to be reloaded via Zotero.Collections.get(), so mark them as disabled - if (isNew) { - var id = this.id; - this._disabled = true; - return id; - } - - yield this.reload(); - this._clearChanged(); - - return true; - }.bind(this), transactionOptions); + var parent = newParent.id; } - catch (e) { - try { - yield this.reload(); - this._clearChanged(); - } - catch (e2) { - Zotero.debug(e2, 1); - } - - Zotero.debug(e, 1); - throw e; + else { + var parent = null; } + + var columns = [ + 'collectionID', + 'collectionName', + 'parentCollectionID', + 'clientDateModified', + 'libraryID', + 'key', + 'version', + 'synced' + ]; + var sqlValues = [ + collectionID ? { int: collectionID } : null, + { string: this.name }, + parent ? parent : null, + Zotero.DB.transactionDateTime, + this.libraryID ? this.libraryID : 0, + key, + this.version ? this.version : 0, + this.synced ? 1 : 0 + ]; + if (isNew) { + var placeholders = columns.map(function () '?').join(); + + var sql = "REPLACE INTO collections (" + columns.join(', ') + ") " + + "VALUES (" + placeholders + ")"; + var insertID = yield Zotero.DB.queryAsync(sql, sqlValues); + if (!collectionID) { + collectionID = env.id = insertID; + } + } + else { + columns.shift(); + sqlValues.push(sqlValues.shift()); + let sql = 'UPDATE collections SET ' + + columns.map(function (x) x + '=?').join(', ') + + ' WHERE collectionID=?'; + yield Zotero.DB.queryAsync(sql, sqlValues); + } + + if (this._changed.parentKey) { + var parentIDs = []; + if (this.id && this._previousData.parentKey) { + parentIDs.push(Zotero.Collections.getIDFromLibraryAndKey( + this.libraryID, this._previousData.parentKey + )); + } + if (this.parentKey) { + parentIDs.push(Zotero.Collections.getIDFromLibraryAndKey( + this.libraryID, this.parentKey + )); + } + if (this.id) { + Zotero.Notifier.trigger('move', 'collection', this.id); + } + env.parentIDs = parentIDs; + } +}); + +Zotero.Collection.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) { + var isNew = env.isNew; + if (isNew && this.libraryID) { + var groupID = Zotero.Groups.getGroupIDFromLibraryID(this.libraryID); + var group = Zotero.Groups.get(groupID); + group.clearCollectionCache(); + } + + if (isNew) { + Zotero.Notifier.trigger('add', 'collection', this.id); + } + else { + Zotero.Notifier.trigger('modify', 'collection', this.id, this._previousData); + } + + // Invalidate cached child collections + if (env.parentIDs) { + Zotero.Collections.refreshChildCollections(env.parentIDs); + } + + // New collections have to be reloaded via Zotero.Collections.get(), so mark them as disabled + if (isNew) { + var id = this.id; + this._disabled = true; + return id; + } + + yield this.reload(); + this._clearChanged(); + + return true; }); diff --git a/chrome/content/zotero/xpcom/data/dataObject.js b/chrome/content/zotero/xpcom/data/dataObject.js index d7d928548a..a563f7e679 100644 --- a/chrome/content/zotero/xpcom/data/dataObject.js +++ b/chrome/content/zotero/xpcom/data/dataObject.js @@ -425,6 +425,98 @@ Zotero.DataObject.prototype._clearFieldChange = function (field) { delete this._previousData[field]; } + +Zotero.DataObject.prototype.isEditable = function () { + return Zotero.Libraries.isEditable(this.libraryID); +} + + +Zotero.DataObject.prototype.editCheck = function () { + if (!Zotero.Sync.Server.updatesInProgress && !Zotero.Sync.Storage.updatesInProgress && !this.isEditable()) { + throw ("Cannot edit " + this._objectType + " in read-only Zotero library"); + } +} + +/** + * Save changes to database + * + * @return {Promise} Promise for itemID of new item, + * TRUE on item update, or FALSE if item was unchanged + */ +Zotero.DataObject.prototype.save = Zotero.Promise.coroutine(function* (options) { + var env = { + arguments: arguments, + transactionOptions: null, + options: options || {} + }; + + var proceed = yield this._initSave(env); + if (!proceed) return false; + + if (env.isNew) { + Zotero.debug('Saving data for new ' + this._objectType + ' to database', 4); + } + else { + Zotero.debug('Updating database with new ' + this._objectType + ' data', 4); + } + + return Zotero.DB.executeTransaction(function* () { + yield this._saveData(env); + return yield this._finalizeSave(env); + }.bind(this), env.transactionOptions) + .catch(e => { + return this._recoverFromSaveError(env, e) + .catch(function(e2) { + Zotero.debug(e2, 1); + }) + .then(function() { + Zotero.debug(e, 1); + throw e; + }) + }); +}); + +Zotero.DataObject.prototype.hasChanged = function() { + Zotero.debug(this._changed); + return !!Object.keys(this._changed).filter(dataType => this._changed[dataType]).length +} + +Zotero.DataObject.prototype._saveData = function() { + throw new Error("Zotero.DataObject.prototype._saveData is an abstract method"); +} + +Zotero.DataObject.prototype._finalizeSave = function() { + throw new Error("Zotero.DataObject.prototype._finalizeSave is an abstract method"); +} + +Zotero.DataObject.prototype._recoverFromSaveError = Zotero.Promise.coroutine(function* () { + yield this.reload(null, true); + this._clearChanged(); +}); + +Zotero.DataObject.prototype._initSave = Zotero.Promise.coroutine(function* (env) { + env.isNew = !this.id; + + this.editCheck(); + + if (!this.hasChanged()) { + Zotero.debug(this._ObjectType + ' ' + this.id + ' has not changed', 4); + return false; + } + + // Register this object's identifiers in Zotero.DataObjects on transaction commit, + // before other callbacks run + if (env.isNew) { + env.transactionOptions = { + onCommit: () => { + this.ObjectsClass.registerIdentifiers(env.id, env.libraryID, env.key); + } + }; + } + + return true; +}); + /** * Generates data object key * @return {String} key diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js index fecc926fbd..f3c57bbe0e 100644 --- a/chrome/content/zotero/xpcom/data/item.js +++ b/chrome/content/zotero/xpcom/data/item.js @@ -503,15 +503,6 @@ Zotero.Item.prototype.loadFromRow = function(row, reload) { } -/* - * Check if any data fields have changed since last save - */ -Zotero.Item.prototype.hasChanged = function() { - Zotero.debug(this._changed); - return !!Object.keys(this._changed).filter((dataType) => this._changed[dataType]).length -} - - /* * Set or change the item's type */ @@ -1173,609 +1164,572 @@ Zotero.Item.prototype.removeRelatedItem = Zotero.Promise.coroutine(function* (it }); -/** - * Save changes to database - * - * @return {Promise} Promise for itemID of new item, - * TRUE on item update, or FALSE if item was unchanged - */ -Zotero.Item.prototype.save = Zotero.Promise.coroutine(function* (options) { - try { - if (!options) { - options = {}; - } +Zotero.Item.prototype.isEditable = function() { + var editable = Zotero.Item._super.prototype.isEditable.apply(this); + if (!editable) return false; + + // Check if we're allowed to save attachments + if (this.isAttachment() + && (this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL || + this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE) + && !Zotero.Libraries.isFilesEditable(this.libraryID) + ) { + return false; + } + + return true; +} + +Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) { + var isNew = env.isNew; + var options = env.options; + + var itemTypeID = this.itemTypeID; - var isNew = !this.id; - - Zotero.Items.editCheck(this); - - if (!this.hasChanged()) { - Zotero.debug('Item ' + this.id + ' has not changed', 4); - return false; - } - - // Register this item's identifiers in Zotero.DataObjects on transaction commit, - // before other callbacks run - var itemID, libraryID, key; - if (isNew) { - var transactionOptions = { - onCommit: function () { - Zotero.Items.registerIdentifiers(itemID, libraryID, key); + var sqlColumns = []; + var sqlValues = []; + var reloadParentChildItems = {}; + + // + // Primary fields + // + // If available id value, use it -- otherwise we'll use autoincrement + var itemID = env.id = this._id = this.id ? this.id : yield Zotero.ID.get('items'); + Zotero.debug('='); + var libraryID = env.libraryID = this.libraryID; + var key = env.key = this._key = this.key ? this.key : this._generateKey(); + + sqlColumns.push( + 'itemTypeID', + 'dateAdded', + 'libraryID', + 'key', + 'version', + 'synced' + ); + + sqlValues.push( + { int: itemTypeID }, + this.dateAdded ? this.dateAdded : Zotero.DB.transactionDateTime, + this.libraryID ? this.libraryID : 0, + key, + this.version ? this.version : 0, + this.synced ? 1 : 0 + ); + + if (isNew) { + sqlColumns.push('dateModified', 'clientDateModified'); + sqlValues.push(Zotero.DB.transactionDateTime, Zotero.DB.transactionDateTime); + } + else { + for each (let field in ['dateModified', 'clientDateModified']) { + switch (field) { + case 'dateModified': + case 'clientDateModified': + let skipFlag = "skip" + field[0].toUpperCase() + field.substr(1) + "Update"; + if (!options[skipFlag]) { + sqlColumns.push(field); + sqlValues.push(Zotero.DB.transactionDateTime); } - }; + break; + } + } + } + + if (isNew) { + sqlColumns.unshift('itemID'); + sqlValues.unshift(parseInt(itemID)); + + var sql = "INSERT INTO items (" + sqlColumns.join(", ") + ") " + + "VALUES (" + sqlValues.map(function () "?").join() + ")"; + var insertID = yield Zotero.DB.queryAsync(sql, sqlValues); + if (!itemID) { + itemID = env.id = insertID; + } + + Zotero.Notifier.trigger('add', 'item', itemID); + } + else { + var sql = "UPDATE items SET " + sqlColumns.join("=?, ") + "=? WHERE itemID=?"; + sqlValues.push(parseInt(itemID)); + yield Zotero.DB.queryAsync(sql, sqlValues); + + var notifierData = {}; + notifierData[itemID] = { changed: this._previousData }; + Zotero.Notifier.trigger('modify', 'item', itemID, notifierData); + } + + // + // ItemData + // + if (this._changed.itemData) { + let del = []; + + let valueSQL = "SELECT valueID FROM itemDataValues WHERE value=?"; + let insertValueSQL = "INSERT INTO itemDataValues VALUES (?,?)"; + let replaceSQL = "REPLACE INTO itemData VALUES (?,?,?)"; + + for (let fieldID in this._changed.itemData) { + fieldID = parseInt(fieldID); + let value = this.getField(fieldID, true); + + // If field changed and is empty, mark row for deletion + if (!value) { + del.push(fieldID); + continue; + } + + if (Zotero.ItemFields.getID('accessDate') == fieldID + && (this.getField(fieldID)) == 'CURRENT_TIMESTAMP') { + value = Zotero.DB.transactionDateTime; + } + + let valueID = yield Zotero.DB.valueQueryAsync(valueSQL, [value], { debug: true }) + if (!valueID) { + valueID = yield Zotero.ID.get('itemDataValues'); + yield Zotero.DB.queryAsync(insertValueSQL, [valueID, value], { debug: false }); + } + + yield Zotero.DB.queryAsync(replaceSQL, [itemID, fieldID, valueID], { debug: false }); + } + + // Delete blank fields + if (del.length) { + sql = 'DELETE from itemData WHERE itemID=? AND ' + + 'fieldID IN (' + del.map(function () '?').join() + ')'; + yield Zotero.DB.queryAsync(sql, [itemID].concat(del)); + } + } + + // + // Creators + // + if (this._changed.creators) { + for (let orderIndex in this._changed.creators) { + orderIndex = parseInt(orderIndex); + + if (isNew) { + Zotero.debug('Adding creator in position ' + orderIndex, 4); + } + else { + Zotero.debug('Creator ' + orderIndex + ' has changed', 4); + } + + let creatorData = this.getCreator(orderIndex); + // If no creator in this position, just remove the item-creator association + if (!creatorData) { + let sql = "DELETE FROM itemCreators WHERE itemID=? AND orderIndex=?"; + yield Zotero.DB.queryAsync(sql, [itemID, orderIndex]); + Zotero.Prefs.set('purge.creators', true); + continue; + } + + let previousCreatorID = this._previousData.creators[orderIndex] + ? this._previousData.creators[orderIndex].id + : false; + let newCreatorID = yield Zotero.Creators.getIDFromData(creatorData, true); + + // If there was previously a creator at this position and it's different from + // the new one, the old one might need to be purged. + if (previousCreatorID && previousCreatorID != newCreatorID) { + Zotero.Prefs.set('purge.creators', true); + } + + let sql = "INSERT OR REPLACE INTO itemCreators " + + "(itemID, creatorID, creatorTypeID, orderIndex) VALUES (?, ?, ?, ?)"; + yield Zotero.DB.queryAsync( + sql, + [ + itemID, + newCreatorID, + creatorData.creatorTypeID, + orderIndex + ] + ); + } + } + + // Parent item + let parentItem = this.parentKey; + parentItem = parentItem ? Zotero.Items.getByLibraryAndKey(this.libraryID, parentItem) : null; + if (this._changed.parentKey) { + if (isNew) { + if (!parentItem) { + // TODO: clear caches? + let msg = this._parentKey + " is not a valid item key"; + throw new Zotero.Error(msg, "MISSING_OBJECT"); + } + + let newParentItemNotifierData = {}; + //newParentItemNotifierData[newParentItem.id] = {}; + Zotero.Notifier.trigger('modify', 'item', parentItem.id, newParentItemNotifierData); + + switch (Zotero.ItemTypes.getName(itemTypeID)) { + case 'note': + case 'attachment': + reloadParentChildItems[parentItem.id] = true; + break; + } } else { - var transactionOptions = null; + let type = Zotero.ItemTypes.getName(itemTypeID); + let Type = type[0].toUpperCase() + type.substr(1); + + if (this._parentKey) { + if (!parentItem) { + // TODO: clear caches + let msg = "Cannot set source to invalid item " + this._parentKey; + throw new Zotero.Error(msg, "MISSING_OBJECT"); + } + + let newParentItemNotifierData = {}; + //newParentItemNotifierData[newParentItem.id] = {}; + Zotero.Notifier.trigger('modify', 'item', parentItem.id, newParentItemNotifierData); + } + + var oldParentKey = this._previousData.parentKey; + if (oldParentKey) { + var oldParentItem = Zotero.Items.getByLibraryAndKey(this.libraryID, oldParentKey); + if (oldParentItem) { + let oldParentItemNotifierData = {}; + //oldParentItemNotifierData[oldParentItem.id] = {}; + Zotero.Notifier.trigger('modify', 'item', oldParentItem.id, oldParentItemNotifierData); + } + else { + Zotero.debug("Old source item " + oldParentKey + + " didn't exist in Zotero.Item.save()", 2); + } + } + + // If this was an independent item, remove from any collections + // where it existed previously and add parent instead + if (!oldParentKey) { + let sql = "SELECT collectionID FROM collectionItems WHERE itemID=?"; + let changedCollections = yield Zotero.DB.columnQueryAsync(sql, this.id); + if (changedCollections) { + for (let i=0; i wrapper if not present - if (!noteText.match(/^
[\s\S]*<\/div>$/)) { - // Keep consistent with getNote() - noteText = '
' + noteText + '
'; - } - - let params = [ - parent ? parent : null, - noteText, - this._noteTitle ? this._noteTitle : '' - ]; - let sql = "SELECT COUNT(*) FROM itemNotes WHERE itemID=?"; - if (yield Zotero.DB.valueQueryAsync(sql, itemID)) { - sql = "UPDATE itemNotes SET parentItemID=?, note=?, title=? WHERE itemID=?"; - params.push(itemID); - } - else { - sql = "INSERT INTO itemNotes " - + "(itemID, parentItemID, note, title) VALUES (?,?,?,?)"; - params.unshift(itemID); - } - yield Zotero.DB.queryAsync(sql, params); - - if (parentItem) { - reloadParentChildItems[parentItem.id] = true; - } - } - - // - // Attachment - // - if (!isNew) { - // If attachment title changes, update parent attachments - if (this._changed.itemData && this._changed.itemData[110] && this.isAttachment() && parentItem) { - reloadParentChildItems[parentItem.id] = true; - } - } - - if (this.isAttachment() || this._changed.attachmentData) { - let sql = "REPLACE INTO itemAttachments (itemID, parentItemID, linkMode, " - + "contentType, charsetID, path, syncState) VALUES (?,?,?,?,?,?,?)"; - let parent = this.parentID; - let linkMode = this.attachmentLinkMode; - let contentType = this.attachmentContentType; - let charsetID = Zotero.CharacterSets.getID(this.attachmentCharset); - let path = this.attachmentPath; - let syncState = this.attachmentSyncState; - - if (this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) { - // Save attachment within attachment base directory as relative path - if (Zotero.Prefs.get('saveRelativeAttachmentPath')) { - path = Zotero.Attachments.getBaseDirectoryRelativePath(path); - } - // If possible, convert relative path to absolute - else { - let file = Zotero.Attachments.resolveRelativePath(path); - if (file) { - path = file.persistentDescriptor; - } - } - } - - let params = [ - itemID, - parent ? parent : null, - { int: linkMode }, - contentType ? { string: contentType } : null, - charsetID ? { int: charsetID } : null, - path ? { string: path } : null, - syncState ? { int: syncState } : 0 - ]; - yield Zotero.DB.queryAsync(sql, params); - - // Clear cached child attachments of the parent - if (!isNew && parentItem) { - reloadParentChildItems[parentItem.id] = true; - } - } - - // Tags - if (this._changed.tags) { - let oldTags = this._previousData.tags; - let newTags = this._tags; - - // Convert to individual JSON objects, diff, and convert back - let oldTagsJSON = oldTags.map(function (x) JSON.stringify(x)); - let newTagsJSON = newTags.map(function (x) JSON.stringify(x)); - let toAdd = Zotero.Utilities.arrayDiff(newTagsJSON, oldTagsJSON) - .map(function (x) JSON.parse(x)); - let toRemove = Zotero.Utilities.arrayDiff(oldTagsJSON, newTagsJSON) - .map(function (x) JSON.parse(x));; - - for (let i=0; i wrapper if not present + if (!noteText.match(/^
[\s\S]*<\/div>$/)) { + // Keep consistent with getNote() + noteText = '
' + noteText + '
'; + } + + let params = [ + parent ? parent : null, + noteText, + this._noteTitle ? this._noteTitle : '' + ]; + let sql = "SELECT COUNT(*) FROM itemNotes WHERE itemID=?"; + if (yield Zotero.DB.valueQueryAsync(sql, itemID)) { + sql = "UPDATE itemNotes SET parentItemID=?, note=?, title=? WHERE itemID=?"; + params.push(itemID); + } + else { + sql = "INSERT INTO itemNotes " + + "(itemID, parentItemID, note, title) VALUES (?,?,?,?)"; + params.unshift(itemID); + } + yield Zotero.DB.queryAsync(sql, params); + + if (parentItem) { + reloadParentChildItems[parentItem.id] = true; + } + } + + // + // Attachment + // + if (!isNew) { + // If attachment title changes, update parent attachments + if (this._changed.itemData && this._changed.itemData[110] && this.isAttachment() && parentItem) { + reloadParentChildItems[parentItem.id] = true; + } + } + + if (this.isAttachment() || this._changed.attachmentData) { + let sql = "REPLACE INTO itemAttachments (itemID, parentItemID, linkMode, " + + "contentType, charsetID, path, syncState) VALUES (?,?,?,?,?,?,?)"; + let parent = this.parentID; + let linkMode = this.attachmentLinkMode; + let contentType = this.attachmentContentType; + let charsetID = Zotero.CharacterSets.getID(this.attachmentCharset); + let path = this.attachmentPath; + let syncState = this.attachmentSyncState; + + if (this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) { + // Save attachment within attachment base directory as relative path + if (Zotero.Prefs.get('saveRelativeAttachmentPath')) { + path = Zotero.Attachments.getBaseDirectoryRelativePath(path); + } + // If possible, convert relative path to absolute + else { + let file = Zotero.Attachments.resolveRelativePath(path); + if (file) { + path = file.persistentDescriptor; + } + } + } + + let params = [ + itemID, + parent ? parent : null, + { int: linkMode }, + contentType ? { string: contentType } : null, + charsetID ? { int: charsetID } : null, + path ? { string: path } : null, + syncState ? { int: syncState } : 0 + ]; + yield Zotero.DB.queryAsync(sql, params); + + // Clear cached child attachments of the parent + if (!isNew && parentItem) { + reloadParentChildItems[parentItem.id] = true; + } + } + + // Tags + if (this._changed.tags) { + let oldTags = this._previousData.tags; + let newTags = this._tags; + + // Convert to individual JSON objects, diff, and convert back + let oldTagsJSON = oldTags.map(function (x) JSON.stringify(x)); + let newTagsJSON = newTags.map(function (x) JSON.stringify(x)); + let toAdd = Zotero.Utilities.arrayDiff(newTagsJSON, oldTagsJSON) + .map(function (x) JSON.parse(x)); + let toRemove = Zotero.Utilities.arrayDiff(oldTagsJSON, newTagsJSON) + .map(function (x) JSON.parse(x));; + + for (let i=0; i