4373 lines
117 KiB
JavaScript
4373 lines
117 KiB
JavaScript
/*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
Copyright © 2009 Center for History and New Media
|
|
George Mason University, Fairfax, Virginia, USA
|
|
http://zotero.org
|
|
|
|
This file is part of Zotero.
|
|
|
|
Zotero is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Zotero is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***** END LICENSE BLOCK *****
|
|
*/
|
|
|
|
|
|
/*
|
|
* Constructor for Item object
|
|
*/
|
|
Zotero.Item = function(itemTypeOrID) {
|
|
if (arguments[1] || arguments[2]) {
|
|
throw ("Zotero.Item constructor only takes one parameter");
|
|
}
|
|
|
|
Zotero.Item._super.apply(this);
|
|
|
|
this._disabled = false;
|
|
|
|
// loadPrimaryData (additional properties in dataObject.js)
|
|
this._itemTypeID = null;
|
|
this._firstCreator = null;
|
|
this._sortCreator = null;
|
|
this._numNotes = null;
|
|
this._numNotesTrashed = null;
|
|
this._numNotesEmbedded = null;
|
|
this._numNotesEmbeddedTrashed = null;
|
|
this._numAttachments = null;
|
|
this._numAttachmentsTrashed = null;
|
|
this._attachmentCharset = null;
|
|
this._attachmentLinkMode = null;
|
|
this._attachmentContentType = null;
|
|
this._attachmentPath = null;
|
|
this._attachmentSyncState = 0;
|
|
this._attachmentSyncedModificationTime = null;
|
|
this._attachmentSyncedHash = null;
|
|
|
|
// loadCreators
|
|
this._creators = [];
|
|
this._creatorIDs = [];
|
|
|
|
// loadItemData
|
|
this._itemData = null;
|
|
this._noteTitle = null;
|
|
this._noteText = null;
|
|
this._displayTitle = null;
|
|
|
|
// loadChildItems
|
|
this._attachments = null;
|
|
this._notes = null;
|
|
|
|
this._tags = [];
|
|
this._collections = [];
|
|
|
|
this._bestAttachmentState = null;
|
|
this._fileExists = null;
|
|
|
|
this._deleted = null;
|
|
this._hasNote = null;
|
|
|
|
this._noteAccessTime = null;
|
|
|
|
if (itemTypeOrID) {
|
|
// setType initializes type-specific properties in this._itemData
|
|
this.setType(Zotero.ItemTypes.getID(itemTypeOrID));
|
|
}
|
|
}
|
|
|
|
Zotero.extendClass(Zotero.DataObject, Zotero.Item);
|
|
|
|
Zotero.Item.prototype._objectType = 'item';
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'ContainerObjectsClass', {
|
|
get: function() Zotero.Collections
|
|
});
|
|
|
|
Zotero.Item.prototype._dataTypes = Zotero.Item._super.prototype._dataTypes.concat([
|
|
'creators',
|
|
'itemData',
|
|
'note',
|
|
'childItems',
|
|
// 'relatedItems', // TODO: remove
|
|
'tags',
|
|
'collections',
|
|
'relations'
|
|
]);
|
|
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'id', {
|
|
get: function() this._id,
|
|
set: function(val) this.setField('id', val)
|
|
});
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'itemID', {
|
|
get: function() {
|
|
Zotero.debug("Item.itemID is deprecated -- use Item.id");
|
|
return this._id;
|
|
}
|
|
});
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'libraryID', {
|
|
get: function() this._libraryID,
|
|
set: function(val) this.setField('libraryID', val)
|
|
});
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'key', {
|
|
get: function() this._key,
|
|
set: function(val) this.setField('key', val)
|
|
});
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'itemTypeID', {
|
|
get: function() this._itemTypeID
|
|
});
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'dateAdded', {
|
|
get: function() this._dateAdded,
|
|
set: function(val) this.setField('dateAdded', val)
|
|
});
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'dateModified', {
|
|
get: function() this._dateModified,
|
|
set: function(val) this.setField('dateModified', val)
|
|
});
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'version', {
|
|
get: function() this._version,
|
|
set: function(val) this.setField('version', val)
|
|
});
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'synced', {
|
|
get: function() this._synced,
|
|
set: function(val) this.setField('synced', val)
|
|
});
|
|
|
|
// .parentKey and .parentID defined in dataObject.js, but create aliases
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'parentItemID', {
|
|
get: function() this.parentID,
|
|
set: function(val) this.parentID = val
|
|
});
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'parentItemKey', {
|
|
get: function() this.parentKey,
|
|
set: function(val) this.parentKey = val
|
|
});
|
|
|
|
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'firstCreator', {
|
|
get: function() this._firstCreator
|
|
});
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'sortCreator', {
|
|
get: function() this._sortCreator
|
|
});
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'relatedItems', {
|
|
get: function() this._getRelatedItems()
|
|
});
|
|
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'treeViewID', {
|
|
get: function () {
|
|
return this.id
|
|
}
|
|
});
|
|
|
|
Zotero.Item.prototype.getID = function() {
|
|
Zotero.debug('Item.getID() is deprecated -- use Item.id');
|
|
return this._id;
|
|
}
|
|
|
|
Zotero.Item.prototype.getType = function() {
|
|
Zotero.debug('Item.getType() is deprecated -- use Item.itemTypeID');
|
|
return this._itemTypeID;
|
|
}
|
|
|
|
Zotero.Item.prototype.isPrimaryField = function (fieldName) {
|
|
Zotero.debug("Zotero.Item.isPrimaryField() is deprecated -- use Zotero.Items.isPrimaryField()");
|
|
return this.ObjectsClass.isPrimaryField(fieldName);
|
|
}
|
|
|
|
Zotero.Item.prototype._get = function () {
|
|
throw new Error("_get is not valid for items");
|
|
}
|
|
|
|
Zotero.Item.prototype._set = function () {
|
|
throw new Error("_set is not valid for items");
|
|
}
|
|
|
|
Zotero.Item.prototype._setParentKey = function() {
|
|
if (!this.isNote() && !this.isAttachment()) {
|
|
throw new Error("_setParentKey() can only be called on items of type 'note' or 'attachment'");
|
|
}
|
|
|
|
Zotero.Item._super.prototype._setParentKey.apply(this, arguments);
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Public Zotero.Item methods
|
|
//
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
/*
|
|
* Retrieves (and loads from DB, if necessary) an itemData field value
|
|
*
|
|
* @param {String|Integer} field fieldID or fieldName
|
|
* @param {Boolean} [unformatted] Skip any special processing of DB value
|
|
* (e.g. multipart date field)
|
|
* @param {Boolean} includeBaseMapped If true and field is a base field, returns
|
|
* value of type-specific field instead
|
|
* (e.g. 'label' for 'publisher' in 'audioRecording')
|
|
* @return {String} Value as string or empty string if value is not present
|
|
*/
|
|
Zotero.Item.prototype.getField = function(field, unformatted, includeBaseMapped) {
|
|
if (field != 'id') this._disabledCheck();
|
|
|
|
//Zotero.debug('Requesting field ' + field + ' for item ' + this._id, 4);
|
|
|
|
this._requireData('primaryData');
|
|
|
|
// TODO: Fix logic and add sortCreator
|
|
if (field === 'firstCreator' && !this._id) {
|
|
// Hack to get a firstCreator for an unsaved item
|
|
var creatorsData = this.getCreators(true);
|
|
if (creators.length === 0) {
|
|
return "";
|
|
} else if (creators.length === 1) {
|
|
return creatorsData[0].lastName;
|
|
} else if (creators.length === 2) {
|
|
return creatorsData[0].lastName + " " + Zotero.getString('general.and') + " " + creatorsData[1].lastName;
|
|
} else if (creators.length > 3) {
|
|
return creatorsData[0].lastName + " " + Zotero.getString('general.etAl');
|
|
}
|
|
} else if (field === 'id' || this.ObjectsClass.isPrimaryField(field)) {
|
|
var privField = '_' + field;
|
|
//Zotero.debug('Returning ' + (this[privField] ? this[privField] : '') + ' (typeof ' + typeof this[privField] + ')');
|
|
return this[privField];
|
|
} else if (field == 'year') {
|
|
return this.getField('date', true, true).substr(0,4);
|
|
}
|
|
|
|
if (this.isNote()) {
|
|
switch (Zotero.ItemFields.getName(field)) {
|
|
case 'title':
|
|
return this.getNoteTitle();
|
|
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
if (includeBaseMapped) {
|
|
var fieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(
|
|
this._itemTypeID, field
|
|
);
|
|
}
|
|
|
|
if (!fieldID) {
|
|
var fieldID = Zotero.ItemFields.getID(field);
|
|
}
|
|
|
|
let value = this._itemData[fieldID];
|
|
|
|
if (value === undefined) {
|
|
//Zotero.debug("Field '" + field + "' doesn't exist for item type " + this._itemTypeID + " in Item.getField()");
|
|
return '';
|
|
}
|
|
|
|
// If the item is identified (has an id or key), this field has to be populated
|
|
if (this._identified && value === null && !this._loaded.itemData) {
|
|
throw new Zotero.Exception.UnloadedDataException(
|
|
"Item data not loaded and field '" + field + "' not set for item " + this.libraryKey,
|
|
"itemData"
|
|
);
|
|
}
|
|
|
|
value = (value !== null && value !== false) ? value : '';
|
|
|
|
if (!unformatted) {
|
|
// Multipart date fields
|
|
// TEMP - filingDate
|
|
if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date') || field == 'filingDate') {
|
|
value = Zotero.Date.multipartToStr(value);
|
|
}
|
|
}
|
|
//Zotero.debug('Returning ' + value);
|
|
return value;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {Boolean} asNames
|
|
* @return {Integer[]|String[]}
|
|
*/
|
|
Zotero.Item.prototype.getUsedFields = function(asNames) {
|
|
this._requireData('itemData');
|
|
|
|
return Object.keys(this._itemData)
|
|
.filter(id => this._itemData[id] !== false && this._itemData[id] !== null)
|
|
.map(id => asNames ? Zotero.ItemFields.getName(id) : parseInt(id));
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
* Populate basic item data from a database row
|
|
*/
|
|
Zotero.Item.prototype.loadFromRow = function(row, reload) {
|
|
// If necessary or reloading, set the type and reinitialize this._itemData
|
|
if (reload || (!this._itemTypeID && row.itemTypeID)) {
|
|
this.setType(row.itemTypeID, true);
|
|
}
|
|
|
|
this._parseRowData(row);
|
|
this._finalizeLoadFromRow(row);
|
|
}
|
|
|
|
Zotero.Item.prototype._parseRowData = function(row) {
|
|
var primaryFields = this.ObjectsClass.primaryFields;
|
|
for (let i=0; i<primaryFields.length; i++) {
|
|
let col = primaryFields[i];
|
|
|
|
try {
|
|
var val = row[col];
|
|
}
|
|
catch (e) {
|
|
Zotero.debug('Skipping missing field ' + col);
|
|
continue;
|
|
}
|
|
|
|
//Zotero.debug("Setting field '" + col + "' to '" + val + "' for item " + this.id);
|
|
|
|
switch (col) {
|
|
// Unchanged
|
|
case 'libraryID':
|
|
case 'itemTypeID':
|
|
case 'attachmentSyncState':
|
|
case 'attachmentSyncedHash':
|
|
case 'attachmentSyncedModificationTime':
|
|
break;
|
|
|
|
case 'itemID':
|
|
col = 'id';
|
|
break;
|
|
|
|
// Integer or 0
|
|
case 'version':
|
|
case 'numNotes':
|
|
case 'numNotesTrashed':
|
|
case 'numNotesEmbedded':
|
|
case 'numNotesEmbeddedTrashed':
|
|
case 'numAttachments':
|
|
case 'numAttachmentsTrashed':
|
|
val = val ? parseInt(val) : 0;
|
|
break;
|
|
|
|
// Value or false
|
|
case 'parentKey':
|
|
val = val || false;
|
|
break;
|
|
|
|
// Integer or false if falsy
|
|
case 'parentID':
|
|
val = val ? parseInt(val) : false;
|
|
break;
|
|
|
|
case 'attachmentLinkMode':
|
|
val = val !== null ? parseInt(val) : false;
|
|
break;
|
|
|
|
case 'attachmentPath':
|
|
// Ignore .zotero* files that were relinked before we started blocking them
|
|
if (!val || val.startsWith('.zotero')) {
|
|
val = '';
|
|
}
|
|
break;
|
|
|
|
// Boolean
|
|
case 'synced':
|
|
case 'deleted':
|
|
val = !!val;
|
|
break;
|
|
|
|
default:
|
|
val = val ? val : '';
|
|
}
|
|
|
|
this['_' + col] = val;
|
|
}
|
|
}
|
|
|
|
Zotero.Item.prototype._finalizeLoadFromRow = function(row) {
|
|
this._loaded.primaryData = true;
|
|
this._clearChanged('primaryData');
|
|
this._identified = true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Set or change the item's type
|
|
*/
|
|
Zotero.Item.prototype.setType = function(itemTypeID, loadIn) {
|
|
if (itemTypeID == this._itemTypeID) {
|
|
return true;
|
|
}
|
|
|
|
// Adjust 'note' data type based on whether the item is an attachment or note
|
|
var isAttachment = Zotero.ItemTypes.getID('attachment') == itemTypeID;
|
|
var isNote = Zotero.ItemTypes.getID('note') == itemTypeID;
|
|
this._skipDataTypeLoad.note = !(isAttachment || isNote);
|
|
|
|
var oldItemTypeID = this._itemTypeID;
|
|
if (oldItemTypeID) {
|
|
if (loadIn) {
|
|
throw new Error('Cannot change type in loadIn mode');
|
|
}
|
|
|
|
// Changing the item type can affect fields and creators, so they need to be loaded
|
|
this._requireData('itemData');
|
|
this._requireData('creators');
|
|
|
|
var copiedFields = [];
|
|
var newNotifierFields = [];
|
|
|
|
// Special cases handled below
|
|
var bookTypeID = Zotero.ItemTypes.getID('book');
|
|
var bookSectionTypeID = Zotero.ItemTypes.getID('bookSection');
|
|
|
|
var obsoleteFields = this.getFieldsNotInType(itemTypeID);
|
|
if (obsoleteFields) {
|
|
// Move bookTitle to title and clear short title when going from
|
|
// bookSection to book if there's not also a title
|
|
if (oldItemTypeID == bookSectionTypeID && itemTypeID == bookTypeID) {
|
|
var titleFieldID = Zotero.ItemFields.getID('title');
|
|
var bookTitleFieldID = Zotero.ItemFields.getID('bookTitle');
|
|
var shortTitleFieldID = Zotero.ItemFields.getID('shortTitle');
|
|
if (this._itemData[bookTitleFieldID] && !this._itemData[titleFieldID]) {
|
|
copiedFields.push([titleFieldID, this._itemData[bookTitleFieldID]]);
|
|
newNotifierFields.push(titleFieldID);
|
|
if (this._itemData[shortTitleFieldID]) {
|
|
this.setField(shortTitleFieldID, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let oldFieldID of obsoleteFields) {
|
|
// Try to get a base type for this field
|
|
var baseFieldID =
|
|
Zotero.ItemFields.getBaseIDFromTypeAndField(oldItemTypeID, oldFieldID);
|
|
|
|
if (baseFieldID) {
|
|
var newFieldID =
|
|
Zotero.ItemFields.getFieldIDFromTypeAndBase(itemTypeID, baseFieldID);
|
|
|
|
// If so, save value to copy to new field
|
|
if (newFieldID) {
|
|
copiedFields.push([newFieldID, this.getField(oldFieldID)]);
|
|
}
|
|
}
|
|
|
|
// Clear old field
|
|
/*
|
|
delete this._itemData[oldFieldID];
|
|
if (!this._changed.itemData) {
|
|
this._changed.itemData = {};
|
|
}
|
|
this._changed.itemData[oldFieldID] = true;
|
|
*/
|
|
this.setField(oldFieldID, false);
|
|
}
|
|
}
|
|
|
|
// Move title to bookTitle and clear shortTitle when going from book to bookSection
|
|
if (oldItemTypeID == bookTypeID && itemTypeID == bookSectionTypeID) {
|
|
var titleFieldID = Zotero.ItemFields.getID('title');
|
|
var bookTitleFieldID = Zotero.ItemFields.getID('bookTitle');
|
|
var shortTitleFieldID = Zotero.ItemFields.getID('shortTitle');
|
|
if (this._itemData[titleFieldID]) {
|
|
copiedFields.push([bookTitleFieldID, this._itemData[titleFieldID]]);
|
|
newNotifierFields.push(bookTitleFieldID);
|
|
this.setField(titleFieldID, false);
|
|
}
|
|
if (this._itemData[shortTitleFieldID]) {
|
|
this.setField(shortTitleFieldID, false);
|
|
}
|
|
}
|
|
|
|
for (var fieldID in this._itemData) {
|
|
if (this._itemData[fieldID] &&
|
|
(!obsoleteFields || obsoleteFields.indexOf(fieldID) == -1)) {
|
|
copiedFields.push([fieldID, this.getField(fieldID)]);
|
|
}
|
|
}
|
|
}
|
|
|
|
this._itemTypeID = itemTypeID;
|
|
|
|
// If there's an existing type
|
|
if (oldItemTypeID) {
|
|
// Reset custom creator types to the default
|
|
let creators = this.getCreators();
|
|
if (creators) {
|
|
let removeAll = !Zotero.CreatorTypes.itemTypeHasCreators(itemTypeID);
|
|
for (let i=0; i<creators.length; i++) {
|
|
// Remove all creators if new item type doesn't have any
|
|
if (removeAll) {
|
|
this.removeCreator(i);
|
|
continue;
|
|
}
|
|
|
|
if (!Zotero.CreatorTypes.isValidForItemType(creators[i].creatorTypeID, itemTypeID)) {
|
|
// Convert existing primary creator type to new item type's
|
|
// primary creator type, or contributor (creatorTypeID 2)
|
|
// if none or not currently primary
|
|
let oldPrimary = Zotero.CreatorTypes.getPrimaryIDForType(oldItemTypeID);
|
|
let newPrimary = false;
|
|
if (oldPrimary == creators[i].creatorTypeID) {
|
|
newPrimary = Zotero.CreatorTypes.getPrimaryIDForType(itemTypeID);
|
|
}
|
|
creators[i].creatorTypeID = newPrimary ? newPrimary : 2;
|
|
|
|
this.setCreator(i, creators[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize this._itemData with type-specific fields
|
|
this._itemData = {};
|
|
var fields = Zotero.ItemFields.getItemTypeFields(itemTypeID);
|
|
for (let fieldID of fields) {
|
|
this._itemData[fieldID] = null;
|
|
}
|
|
|
|
// DEBUG: clear change item data?
|
|
|
|
if (copiedFields) {
|
|
for (let f of copiedFields) {
|
|
// For fields that we moved to different fields in the new type
|
|
// (e.g., book -> bookTitle), mark the old value as explicitly
|
|
// false in previousData (since otherwise it would be null)
|
|
if (newNotifierFields.indexOf(f[0]) != -1) {
|
|
this._markFieldChange(Zotero.ItemFields.getName(f[0]), false);
|
|
this.setField(f[0], f[1]);
|
|
}
|
|
// For fields that haven't changed, clear from previousData
|
|
// after setting
|
|
else {
|
|
this.setField(f[0], f[1]);
|
|
this._clearFieldChange(Zotero.ItemFields.getName(f[0]));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (loadIn) {
|
|
this._loaded['itemData'] = false;
|
|
}
|
|
else {
|
|
if (oldItemTypeID) {
|
|
this._markFieldChange('itemType', Zotero.ItemTypes.getName(oldItemTypeID));
|
|
}
|
|
if (!this._changed.primaryData) {
|
|
this._changed.primaryData = {};
|
|
}
|
|
this._changed.primaryData.itemTypeID = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Find existing fields from current type that aren't in another
|
|
*
|
|
* If _allowBaseConversion_, don't return fields that can be converted
|
|
* via base fields (e.g. label => publisher => studio)
|
|
*/
|
|
Zotero.Item.prototype.getFieldsNotInType = function (itemTypeID, allowBaseConversion) {
|
|
var fieldIDs = [];
|
|
|
|
for (var field in this._itemData) {
|
|
if (this._itemData[field]) {
|
|
var fieldID = Zotero.ItemFields.getID(field);
|
|
if (Zotero.ItemFields.isValidForType(fieldID, itemTypeID)) {
|
|
continue;
|
|
}
|
|
|
|
if (allowBaseConversion) {
|
|
var baseID = Zotero.ItemFields.getBaseIDFromTypeAndField(this.itemTypeID, field);
|
|
if (baseID) {
|
|
var newFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(itemTypeID, baseID);
|
|
if (newFieldID) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
fieldIDs.push(fieldID);
|
|
}
|
|
}
|
|
/*
|
|
var sql = "SELECT fieldID FROM itemTypeFields WHERE itemTypeID=?1 AND "
|
|
+ "fieldID IN (SELECT fieldID FROM itemData WHERE itemID=?2) AND "
|
|
+ "fieldID NOT IN (SELECT fieldID FROM itemTypeFields WHERE itemTypeID=?3)";
|
|
|
|
if (allowBaseConversion) {
|
|
// Not the type-specific field for a base field in the new type
|
|
sql += " AND fieldID NOT IN (SELECT fieldID FROM baseFieldMappings "
|
|
+ "WHERE itemTypeID=?1 AND baseFieldID IN "
|
|
+ "(SELECT fieldID FROM itemTypeFields WHERE itemTypeID=?3)) AND ";
|
|
// And not a base field with a type-specific field in the new type
|
|
sql += "fieldID NOT IN (SELECT baseFieldID FROM baseFieldMappings "
|
|
+ "WHERE itemTypeID=?3) AND ";
|
|
// And not the type-specific field for a base field that has
|
|
// a type-specific field in the new type
|
|
sql += "fieldID NOT IN (SELECT fieldID FROM baseFieldMappings "
|
|
+ "WHERE itemTypeID=?1 AND baseFieldID IN "
|
|
+ "(SELECT baseFieldID FROM baseFieldMappings WHERE itemTypeID=?3))";
|
|
}
|
|
|
|
return Zotero.DB.columnQuery(sql, [this.itemTypeID, this.id, { int: itemTypeID }]);
|
|
*/
|
|
if (!fieldIDs.length) {
|
|
return false;
|
|
}
|
|
|
|
return fieldIDs;
|
|
}
|
|
|
|
|
|
/*
|
|
* Set a field value, loading existing itemData first if necessary
|
|
*
|
|
* Field can be passed as fieldID or fieldName
|
|
*/
|
|
Zotero.Item.prototype.setField = function(field, value, loadIn) {
|
|
this._disabledCheck();
|
|
|
|
if (value === undefined) {
|
|
throw new Error(`'${field}' value cannot be undefined`);
|
|
}
|
|
|
|
// Normalize values
|
|
if (typeof value == 'number') {
|
|
value = "" + value;
|
|
}
|
|
else if (typeof value == 'string') {
|
|
value = value.trim().normalize();
|
|
}
|
|
if (value === "" || value === null || value === false) {
|
|
value = false;
|
|
}
|
|
|
|
//Zotero.debug("Setting field '" + field + "' to '" + value + "' (loadIn: " + (loadIn ? 'true' : 'false') + ") for item " + this.id + " ");
|
|
|
|
if (!field) {
|
|
throw new Error("Field not specified");
|
|
}
|
|
|
|
if (field == 'id' || field == 'libraryID' || field == 'key') {
|
|
return this._setIdentifier(field, value);
|
|
}
|
|
|
|
// Primary field
|
|
if (this.ObjectsClass.isPrimaryField(field)) {
|
|
this._requireData('primaryData');
|
|
|
|
if (loadIn) {
|
|
throw new Error('Cannot set primary field ' + field + ' in loadIn mode in Zotero.Item.setField()');
|
|
}
|
|
|
|
switch (field) {
|
|
case 'itemTypeID':
|
|
break;
|
|
|
|
case 'dateAdded':
|
|
case 'dateModified':
|
|
// Accept ISO dates
|
|
if (Zotero.Date.isISODate(value)) {
|
|
let d = Zotero.Date.isoToDate(value);
|
|
value = Zotero.Date.dateToSQL(d, true);
|
|
}
|
|
|
|
// Make sure it's valid
|
|
let date = Zotero.Date.sqlToDate(value, true);
|
|
if (!date) throw new Error("Invalid SQL date: " + value);
|
|
|
|
value = Zotero.Date.dateToSQL(date, true);
|
|
break;
|
|
|
|
case 'version':
|
|
value = parseInt(value);
|
|
break;
|
|
|
|
case 'synced':
|
|
value = !!value;
|
|
break;
|
|
|
|
default:
|
|
throw new Error('Primary field ' + field + ' cannot be changed in Zotero.Item.setField()');
|
|
|
|
}
|
|
|
|
/*
|
|
if (!Zotero.ItemFields.validate(field, value)) {
|
|
throw("Value '" + value + "' of type " + typeof value + " does not validate for field '" + field + "' in Zotero.Item.setField()");
|
|
}
|
|
*/
|
|
|
|
// If field value has changed
|
|
if (this['_' + field] === value && field != 'synced') {
|
|
Zotero.debug("Field '" + field + "' has not changed", 4);
|
|
return false;
|
|
}
|
|
|
|
Zotero.debug("Field '" + field + "' has changed from '" + this['_' + field] + "' to '" + value + "'", 4);
|
|
|
|
// Save a copy of the field before modifying
|
|
this._markFieldChange(field, this['_' + field]);
|
|
|
|
if (field == 'itemTypeID') {
|
|
this.setType(value, loadIn);
|
|
}
|
|
else {
|
|
|
|
this['_' + field] = value;
|
|
|
|
if (!this._changed.primaryData) {
|
|
this._changed.primaryData = {};
|
|
}
|
|
this._changed.primaryData[field] = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (!loadIn) {
|
|
this._requireData('itemData');
|
|
}
|
|
|
|
let itemTypeID = this.itemTypeID;
|
|
if (!itemTypeID) {
|
|
throw new Error('Item type must be set before setting field data');
|
|
}
|
|
|
|
var fieldID = Zotero.ItemFields.getID(field);
|
|
if (!fieldID) {
|
|
throw new Error('"' + field + '" is not a valid itemData field');
|
|
}
|
|
|
|
if (loadIn && this.isNote() && field == 110) { // title
|
|
this._noteTitle = value ? value : "";
|
|
return true;
|
|
}
|
|
|
|
// Make sure to use type-specific field ID if available
|
|
fieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(itemTypeID, fieldID) || fieldID;
|
|
|
|
if (value !== false && !Zotero.ItemFields.isValidForType(fieldID, itemTypeID)) {
|
|
var msg = "'" + field + "' is not a valid field for type " + itemTypeID;
|
|
|
|
if (loadIn) {
|
|
Zotero.debug(msg + " -- ignoring value '" + value + "'", 2);
|
|
return false;
|
|
}
|
|
else {
|
|
throw new Error(msg);
|
|
}
|
|
}
|
|
|
|
// If not a multiline field, strip newlines
|
|
if (typeof value == 'string' && !Zotero.ItemFields.isMultiline(fieldID)) {
|
|
value = value.replace(/[\r\n]+/g, " ");;
|
|
}
|
|
|
|
if (fieldID == Zotero.ItemFields.getID('ISBN')) {
|
|
// Hyphenate ISBNs, but only if everything is in expected format and valid
|
|
let isbns = ('' + value).trim().split(/\s*[,;]\s*|\s+/),
|
|
newISBNs = '',
|
|
failed = false;
|
|
for (let i=0; i<isbns.length; i++) {
|
|
let isbn = Zotero.Utilities.Internal.hyphenateISBN(isbns[i]);
|
|
if (!isbn) {
|
|
failed = true;
|
|
break;
|
|
}
|
|
|
|
newISBNs += ' ' + isbn;
|
|
}
|
|
|
|
if (!failed) value = newISBNs.substr(1);
|
|
}
|
|
|
|
if (!loadIn) {
|
|
// Save date field as multipart date
|
|
// TEMP - filingDate
|
|
if (value !== false
|
|
&& (Zotero.ItemFields.isFieldOfBase(fieldID, 'date') || field == 'filingDate')
|
|
&& !Zotero.Date.isMultipart(value)) {
|
|
value = Zotero.Date.strToMultipart(value);
|
|
}
|
|
// Validate access date
|
|
else if (fieldID == Zotero.ItemFields.getID('accessDate')) {
|
|
if (value && value != 'CURRENT_TIMESTAMP') {
|
|
// Accept ISO dates
|
|
if (Zotero.Date.isISODate(value)) {
|
|
let d = Zotero.Date.isoToDate(value);
|
|
value = Zotero.Date.dateToSQL(d, true);
|
|
}
|
|
|
|
if (!Zotero.Date.isSQLDate(value) && !Zotero.Date.isSQLDateTime(value)) {
|
|
Zotero.logError(`Discarding invalid ${field} '${value}' for `
|
|
+ `item ${this.libraryKey} in setField()`);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If existing value, make sure it's actually changing
|
|
if ((this._itemData[fieldID] === null && value === false)
|
|
|| (this._itemData[fieldID] !== null && this._itemData[fieldID] === value)) {
|
|
return false;
|
|
}
|
|
|
|
// Save a copy of the field before modifying
|
|
this._markFieldChange(
|
|
Zotero.ItemFields.getName(field), this._itemData[fieldID]
|
|
);
|
|
}
|
|
|
|
this._itemData[fieldID] = value;
|
|
|
|
if (!loadIn) {
|
|
if (!this._changed.itemData) {
|
|
this._changed.itemData = {};
|
|
}
|
|
this._changed.itemData[fieldID] = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Get the title for an item for display in the interface
|
|
*
|
|
* This is the same as the standard title field (with includeBaseMapped on)
|
|
* except for letters and interviews, which get placeholder titles in
|
|
* square braces (e.g. "[Letter to Thoreau]"), and cases
|
|
*/
|
|
Zotero.Item.prototype.getDisplayTitle = function (includeAuthorAndDate) {
|
|
if (this._displayTitle !== null) {
|
|
return this._displayTitle;
|
|
}
|
|
return this._displayTitle = this.getField('title', false, true);
|
|
}
|
|
|
|
|
|
/**
|
|
* Update the generated display title from the loaded data
|
|
*/
|
|
Zotero.Item.prototype.updateDisplayTitle = function () {
|
|
var title = this.getField('title', false, true);
|
|
var itemTypeID = this.itemTypeID;
|
|
var itemTypeName = Zotero.ItemTypes.getName(itemTypeID);
|
|
|
|
if (title === "" && (itemTypeID == 8 || itemTypeID == 10)) { // 'letter' and 'interview' itemTypeIDs
|
|
var creatorsData = this.getCreators();
|
|
var authors = [];
|
|
var participants = [];
|
|
for (let i=0; i<creatorsData.length; i++) {
|
|
let creatorData = creatorsData[i];
|
|
let creatorTypeID = creatorsData[i].creatorTypeID;
|
|
if ((itemTypeID == 8 && creatorTypeID == 16) || // 'letter'
|
|
(itemTypeID == 10 && creatorTypeID == 7)) { // 'interview'
|
|
participants.push(creatorData);
|
|
}
|
|
else if ((itemTypeID == 8 && creatorTypeID == 1) || // 'letter'/'author'
|
|
(itemTypeID == 10 && creatorTypeID == 6)) { // 'interview'/'interviewee'
|
|
authors.push(creatorData);
|
|
}
|
|
}
|
|
|
|
var strParts = [];
|
|
if (participants.length > 0) {
|
|
let names = [];
|
|
let max = Math.min(4, participants.length);
|
|
for (let i=0; i<max; i++) {
|
|
names.push(
|
|
participants[i].name !== undefined
|
|
? participants[i].name
|
|
: participants[i].lastName
|
|
);
|
|
}
|
|
switch (names.length) {
|
|
case 1:
|
|
var str = 'oneParticipant';
|
|
break;
|
|
|
|
case 2:
|
|
var str = 'twoParticipants';
|
|
break;
|
|
|
|
case 3:
|
|
var str = 'threeParticipants';
|
|
break;
|
|
|
|
default:
|
|
var str = 'manyParticipants';
|
|
}
|
|
strParts.push(Zotero.getString('pane.items.' + itemTypeName + '.' + str, names));
|
|
}
|
|
else {
|
|
strParts.push(Zotero.ItemTypes.getLocalizedString(itemTypeID));
|
|
}
|
|
|
|
title = '[' + strParts.join('; ') + ']';
|
|
}
|
|
else if (itemTypeID == 17) { // 'case' itemTypeID
|
|
if (title) { // common law cases always have case names
|
|
var reporter = this.getField('reporter');
|
|
if (reporter) {
|
|
title = title + ' (' + reporter + ')';
|
|
} else {
|
|
var court = this.getField('court');
|
|
if (court) {
|
|
title = title + ' (' + court + ')';
|
|
}
|
|
}
|
|
}
|
|
else { // civil law cases have only shortTitle as case name
|
|
var strParts = [];
|
|
var caseinfo = "";
|
|
|
|
var part = this.getField('court');
|
|
if (part) {
|
|
strParts.push(part);
|
|
}
|
|
|
|
part = Zotero.Date.multipartToSQL(this.getField('date', true, true));
|
|
if (part) {
|
|
strParts.push(part);
|
|
}
|
|
|
|
var creatorData = this.getCreator(0);
|
|
if (creatorData && creatorData.creatorTypeID === 1) { // author
|
|
strParts.push(creatorData.lastName);
|
|
}
|
|
|
|
title = '[' + strParts.join(', ') + ']';
|
|
}
|
|
}
|
|
|
|
this._displayTitle = title;
|
|
};
|
|
|
|
|
|
/*
|
|
* Returns the number of creators for this item
|
|
*/
|
|
Zotero.Item.prototype.numCreators = function() {
|
|
this._requireData('creators');
|
|
return this._creators.length;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.hasCreatorAt = function(pos) {
|
|
this._requireData('creators');
|
|
return !!this._creators[pos];
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {Integer} pos
|
|
* @return {Object|Boolean} The internal creator data object at the given position, or FALSE if none
|
|
*/
|
|
Zotero.Item.prototype.getCreator = function (pos) {
|
|
this._requireData('creators');
|
|
if (!this._creators[pos]) {
|
|
return false;
|
|
}
|
|
var creator = {};
|
|
for (let i in this._creators[pos]) {
|
|
creator[i] = this._creators[pos][i];
|
|
}
|
|
return creator;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {Integer} pos
|
|
* @return {Object|Boolean} The API JSON creator data at the given position, or FALSE if none
|
|
*/
|
|
Zotero.Item.prototype.getCreatorJSON = function (pos) {
|
|
this._requireData('creators');
|
|
return this._creators[pos] ? Zotero.Creators.internalToJSON(this._creators[pos]) : false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns creator data in internal format
|
|
*
|
|
* @return {Array<Object>} An array of internal creator data objects
|
|
* ('firstName', 'lastName', 'fieldMode', 'creatorTypeID')
|
|
*/
|
|
Zotero.Item.prototype.getCreators = function () {
|
|
this._requireData('creators');
|
|
// Create copies of the creator data objects
|
|
return this._creators.map(function (data) {
|
|
var creator = {};
|
|
for (let i in data) {
|
|
creator[i] = data[i];
|
|
}
|
|
return creator;
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* @return {Array<Object>} An array of creator data objects in API JSON format
|
|
* ('firstName'/'lastName' or 'name', 'creatorType')
|
|
*/
|
|
Zotero.Item.prototype.getCreatorsJSON = function () {
|
|
this._requireData('creators');
|
|
return this._creators.map(function (data) Zotero.Creators.internalToJSON(data));
|
|
}
|
|
|
|
|
|
/**
|
|
* Set or update the creator at the specified position
|
|
*
|
|
* @param {Integer} orderIndex
|
|
* @param {Object} Creator data in internal or API JSON format:
|
|
* <ul>
|
|
* <li>'name' or 'firstName'/'lastName', or 'firstName'/'lastName'/'fieldMode'</li>
|
|
* <li>'creatorType' (can be name or id) or 'creatorTypeID'</li>
|
|
* </ul>
|
|
*/
|
|
Zotero.Item.prototype.setCreator = function (orderIndex, data) {
|
|
var itemTypeID = this._itemTypeID;
|
|
if (!itemTypeID) {
|
|
throw new Error('Item type must be set before setting creators');
|
|
}
|
|
|
|
this._requireData('creators');
|
|
|
|
data = Zotero.Creators.cleanData(data);
|
|
|
|
if (data.creatorTypeID === undefined) {
|
|
throw new Error("Creator data must include a valid 'creatorType' or 'creatorTypeID' property");
|
|
}
|
|
|
|
// If creatorTypeID isn't valid for this type, use the primary type
|
|
if (!data.creatorTypeID || !Zotero.CreatorTypes.isValidForItemType(data.creatorTypeID, itemTypeID)) {
|
|
var msg = "Creator type '" + Zotero.CreatorTypes.getName(data.creatorTypeID) + "' "
|
|
+ "isn't valid for " + Zotero.ItemTypes.getName(itemTypeID)
|
|
+ " -- changing to primary creator";
|
|
Zotero.debug(msg, 2);
|
|
Components.utils.reportError(msg);
|
|
data.creatorTypeID = Zotero.CreatorTypes.getPrimaryIDForType(itemTypeID);
|
|
}
|
|
|
|
// If creator at this position hasn't changed, cancel
|
|
let previousData = this._creators[orderIndex];
|
|
if (previousData
|
|
&& previousData.creatorTypeID === data.creatorTypeID
|
|
&& previousData.fieldMode === data.fieldMode
|
|
&& previousData.firstName === data.firstName
|
|
&& previousData.lastName === data.lastName) {
|
|
Zotero.debug("Creator in position " + orderIndex + " hasn't changed", 4);
|
|
return false;
|
|
}
|
|
|
|
// Save copy of old creators for save() and notifier
|
|
if (!this._changed.creators) {
|
|
this._changed.creators = {};
|
|
this._markFieldChange('creators', this._getOldCreators());
|
|
}
|
|
this._changed.creators[orderIndex] = true;
|
|
this._creators[orderIndex] = data;
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {Object[]} data - An array of creator data in internal or API JSON format
|
|
*/
|
|
Zotero.Item.prototype.setCreators = function (data) {
|
|
// If empty array, clear all existing creators
|
|
if (!data.length) {
|
|
while (this.hasCreatorAt(0)) {
|
|
this.removeCreator(0);
|
|
}
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
this.setCreator(i, data[i]);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Remove a creator and shift others down
|
|
*/
|
|
Zotero.Item.prototype.removeCreator = function(orderIndex, allowMissing) {
|
|
var creatorData = this.getCreator(orderIndex);
|
|
if (!creatorData && !allowMissing) {
|
|
throw new Error('No creator exists at position ' + orderIndex);
|
|
}
|
|
|
|
// Save copy of old creators for notifier
|
|
if (!this._changed.creators) {
|
|
this._changed.creators = {};
|
|
|
|
var oldCreators = this._getOldCreators();
|
|
this._markFieldChange('creators', oldCreators);
|
|
}
|
|
|
|
// Shift creator orderIndexes down, going to length+1 so we clear the last one
|
|
for (var i=orderIndex, max=this._creators.length+1; i<max; i++) {
|
|
var next = this._creators[i+1] ? this._creators[i+1] : false;
|
|
if (next) {
|
|
this._creators[i] = next;
|
|
}
|
|
else {
|
|
this._creators.splice(i, 1);
|
|
}
|
|
|
|
this._changed.creators[i] = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// Define 'deleted' property (and any others that follow the same pattern in the future)
|
|
for (let name of ['deleted']) {
|
|
let prop = '_' + name;
|
|
|
|
Zotero.defineProperty(Zotero.Item.prototype, name, {
|
|
get: function() {
|
|
if (!this.id) {
|
|
return false;
|
|
}
|
|
if (this[prop] !== null) {
|
|
return this[prop];
|
|
}
|
|
this._requireData('primaryData');
|
|
},
|
|
set: function(val) {
|
|
val = !!val;
|
|
|
|
if (this[prop] == val) {
|
|
Zotero.debug(Zotero.Utilities.capitalize(name)
|
|
+ " state hasn't changed for item " + this.id);
|
|
return;
|
|
}
|
|
this._markFieldChange(name, !!this[prop]);
|
|
this._changed[name] = true;
|
|
this[prop] = val;
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {Zotero.Item}
|
|
* @return {Boolean}
|
|
*/
|
|
Zotero.Item.prototype.addRelatedItem = function (item) {
|
|
if (!(item instanceof Zotero.Item)) {
|
|
throw new Error("'item' must be a Zotero.Item");
|
|
}
|
|
|
|
if (item == this) {
|
|
Zotero.debug("Can't relate item to itself in Zotero.Item.addRelatedItem()", 2);
|
|
return false;
|
|
}
|
|
|
|
if (!this.libraryID) {
|
|
this.libraryID = Zotero.Libraries.userLibraryID;
|
|
}
|
|
|
|
if (item.libraryID != this.libraryID) {
|
|
throw new Error("Cannot relate item to an item in a different library");
|
|
}
|
|
|
|
return this.addRelation(Zotero.Relations.relatedItemPredicate, Zotero.URI.getItemURI(item));
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {Zotero.Item}
|
|
*/
|
|
Zotero.Item.prototype.removeRelatedItem = Zotero.Promise.coroutine(function* (item) {
|
|
if (!(item instanceof Zotero.Item)) {
|
|
throw new Error("'item' must be a Zotero.Item");
|
|
}
|
|
|
|
return this.removeRelation(Zotero.Relations.relatedItemPredicate, Zotero.URI.getItemURI(item));
|
|
});
|
|
|
|
|
|
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.get(this.libraryID).filesEditable
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
Zotero.Item.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
|
|
if (!this.itemTypeID) {
|
|
throw new Error("Item type must be set before saving");
|
|
}
|
|
return Zotero.Item._super.prototype._initSave.apply(this, arguments);
|
|
})
|
|
|
|
Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
|
|
Zotero.DB.requireTransaction();
|
|
|
|
var isNew = env.isNew;
|
|
var options = env.options;
|
|
var libraryType = env.libraryType = Zotero.Libraries.get(env.libraryID).libraryType;
|
|
|
|
var itemTypeID = this.itemTypeID;
|
|
|
|
var reloadParentChildItems = {};
|
|
|
|
//
|
|
// Primary fields
|
|
//
|
|
// If available id value, use it -- otherwise we'll use autoincrement
|
|
var itemID = this._id = this.id ? this.id : Zotero.ID.get('items');
|
|
|
|
env.sqlColumns.push(
|
|
'itemTypeID',
|
|
'dateAdded'
|
|
);
|
|
env.sqlValues.push(
|
|
{ int: itemTypeID },
|
|
this.dateAdded ? this.dateAdded : Zotero.DB.transactionDateTime
|
|
);
|
|
|
|
// If a new item and Date Modified hasn't been provided, or an existing item and
|
|
// Date Modified hasn't changed from its previous value and skipDateModifiedUpdate wasn't
|
|
// passed, use the current timestamp
|
|
if (!this.dateModified
|
|
|| ((!this._changed.primaryData || !this._changed.primaryData.dateModified)
|
|
&& !options.skipDateModifiedUpdate)) {
|
|
env.sqlColumns.push('dateModified');
|
|
env.sqlValues.push(Zotero.DB.transactionDateTime);
|
|
}
|
|
// Otherwise, if a new Date Modified was provided, use that. (This would also work when
|
|
// skipDateModifiedUpdate was passed and there's an existing value, but in that case we
|
|
// can just not change the field at all.)
|
|
else if (this._changed.primaryData && this._changed.primaryData.dateModified) {
|
|
env.sqlColumns.push('dateModified');
|
|
env.sqlValues.push(this.dateModified);
|
|
}
|
|
|
|
if (isNew) {
|
|
env.sqlColumns.unshift('itemID');
|
|
env.sqlValues.unshift(parseInt(itemID));
|
|
|
|
let sql = "INSERT INTO items (" + env.sqlColumns.join(", ") + ") "
|
|
+ "VALUES (" + env.sqlValues.map(function () "?").join() + ")";
|
|
yield Zotero.DB.queryAsync(sql, env.sqlValues);
|
|
|
|
if (!env.options.skipNotifier) {
|
|
Zotero.Notifier.queue('add', 'item', itemID, env.notifierData, env.options.notifierQueue);
|
|
}
|
|
}
|
|
else {
|
|
let sql = "UPDATE items SET " + env.sqlColumns.join("=?, ") + "=? WHERE itemID=?";
|
|
env.sqlValues.push(parseInt(itemID));
|
|
yield Zotero.DB.queryAsync(sql, env.sqlValues);
|
|
|
|
if (!env.options.skipNotifier) {
|
|
Zotero.Notifier.queue('modify', 'item', itemID, env.notifierData, env.options.notifierQueue);
|
|
}
|
|
}
|
|
|
|
//
|
|
// 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 = 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 = !isNew && 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
|
|
var parentItemKey = this.parentKey;
|
|
var parentItemID = this.ObjectsClass.getIDFromLibraryAndKey(this.libraryID, parentItemKey) || null;
|
|
if (this._changed.parentKey) {
|
|
if (isNew) {
|
|
if (!parentItemID) {
|
|
// TODO: clear caches?
|
|
let msg = "Parent item " + this.libraryID + "/" + parentItemKey + " not found";
|
|
let e = new Error(msg);
|
|
e.name = "ZoteroMissingObjectError";
|
|
throw e;
|
|
}
|
|
|
|
let newParentItemNotifierData = {};
|
|
//newParentItemNotifierData[newParentItem.id] = {};
|
|
if (!env.options.skipNotifier) {
|
|
Zotero.Notifier.queue(
|
|
'modify', 'item', parentItemID, newParentItemNotifierData, env.options.notifierQueue
|
|
);
|
|
}
|
|
|
|
switch (Zotero.ItemTypes.getName(itemTypeID)) {
|
|
case 'note':
|
|
case 'attachment':
|
|
reloadParentChildItems[parentItemID] = true;
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
let type = Zotero.ItemTypes.getName(itemTypeID);
|
|
let Type = type[0].toUpperCase() + type.substr(1);
|
|
|
|
if (parentItemKey) {
|
|
if (!parentItemID) {
|
|
// TODO: clear caches
|
|
let msg = "Parent item " + this.libraryID + "/" + parentItemKey + " not found";
|
|
let e = new Error(msg);
|
|
e.name = "ZoteroMissingObjectError";
|
|
throw e;
|
|
}
|
|
|
|
let newParentItemNotifierData = {};
|
|
//newParentItemNotifierData[newParentItem.id] = {};
|
|
if (!env.options.skipNotifier) {
|
|
Zotero.Notifier.queue(
|
|
'modify',
|
|
'item',
|
|
parentItemID,
|
|
newParentItemNotifierData,
|
|
env.options.notifierQueue
|
|
);
|
|
}
|
|
}
|
|
|
|
let oldParentKey = this._previousData.parentKey;
|
|
let oldParentItemID;
|
|
if (oldParentKey) {
|
|
oldParentItemID = this.ObjectsClass.getIDFromLibraryAndKey(this.libraryID, oldParentKey);
|
|
if (oldParentItemID) {
|
|
let oldParentItemNotifierData = {};
|
|
//oldParentItemNotifierData[oldParentItemID] = {};
|
|
if (!env.options.skipNotifier) {
|
|
Zotero.Notifier.queue(
|
|
'modify',
|
|
'item',
|
|
oldParentItemID,
|
|
oldParentItemNotifierData,
|
|
env.options.notifierQueue
|
|
);
|
|
}
|
|
}
|
|
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.length) {
|
|
let parentItem = yield this.ObjectsClass.getByLibraryAndKeyAsync(
|
|
this.libraryID, parentItemKey
|
|
);
|
|
for (let i=0; i<changedCollections.length; i++) {
|
|
parentItem.addToCollection(changedCollections[i]);
|
|
this.removeFromCollection(changedCollections[i]);
|
|
|
|
if (!env.options.skipNotifier) {
|
|
Zotero.Notifier.queue(
|
|
'remove',
|
|
'collection-item',
|
|
changedCollections[i] + '-' + this.id,
|
|
{},
|
|
env.options.notifierQueue
|
|
);
|
|
}
|
|
}
|
|
let parentOptions = {
|
|
skipDateModifiedUpdate: true
|
|
};
|
|
// Apply options (e.g., skipNotifier) from outer save
|
|
for (let o in env.options) {
|
|
if (!o.startsWith('skip')) continue;
|
|
parentOptions[o] = env.options[o];
|
|
}
|
|
yield parentItem.save(parentOptions);
|
|
}
|
|
}
|
|
|
|
// Update DB, if not a note or attachment we're changing below
|
|
if (!this._changed.attachmentData &&
|
|
(!this._changed.note || !this.isNote())) {
|
|
let sql = "UPDATE item" + Type + "s SET parentItemID=? "
|
|
+ "WHERE itemID=?";
|
|
let bindParams = [parentItemID, this.id];
|
|
yield Zotero.DB.queryAsync(sql, bindParams);
|
|
}
|
|
|
|
// Update the counts of the previous and new sources
|
|
if (oldParentItemID) {
|
|
reloadParentChildItems[oldParentItemID] = true;
|
|
}
|
|
if (parentItemID) {
|
|
reloadParentChildItems[parentItemID] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (libraryType == 'publications' && !this.isRegularItem() && !parentItemID) {
|
|
throw new Error("Top-level attachments and notes cannot be added to My Publications");
|
|
}
|
|
|
|
// Trashed status
|
|
if (this._changed.deleted) {
|
|
if (this._deleted) {
|
|
if (libraryType == 'publications') {
|
|
throw new Error("Items in My Publications cannot be moved to trash");
|
|
}
|
|
|
|
sql = "REPLACE INTO deletedItems (itemID) VALUES (?)";
|
|
}
|
|
else {
|
|
// If undeleting, remove any merge-tracking relations
|
|
let predicate = Zotero.Relations.replacedItemPredicate;
|
|
let thisURI = Zotero.URI.getItemURI(this);
|
|
let mergeItems = Zotero.Relations.getByPredicateAndObject(
|
|
'item', predicate, thisURI
|
|
);
|
|
for (let mergeItem of mergeItems) {
|
|
mergeItem.removeRelation(predicate, thisURI);
|
|
yield mergeItem.save({
|
|
skipDateModifiedUpdate: true
|
|
});
|
|
}
|
|
|
|
sql = "DELETE FROM deletedItems WHERE itemID=?";
|
|
}
|
|
yield Zotero.DB.queryAsync(sql, itemID);
|
|
|
|
// Refresh trash
|
|
if (!env.options.skipNotifier) {
|
|
Zotero.Notifier.queue('refresh', 'trash', this.libraryID, {}, env.options.notifierQueue);
|
|
if (this._deleted) {
|
|
Zotero.Notifier.queue('trash', 'item', this.id, {}, env.options.notifierQueue);
|
|
}
|
|
}
|
|
|
|
if (parentItemID) {
|
|
reloadParentChildItems[parentItemID] = true;
|
|
}
|
|
}
|
|
|
|
// Note
|
|
if ((isNew && this.isNote()) || this._changed.note) {
|
|
if (!isNew) {
|
|
if (this._noteText === null || this._noteTitle === null) {
|
|
throw new Error("Cached note values not set with "
|
|
+ "this._changed.note set to true");
|
|
}
|
|
}
|
|
|
|
let parent = this.isNote() ? this.parentID : null;
|
|
let noteText = this._noteText ? this._noteText : '';
|
|
// Add <div> wrapper if not present
|
|
if (!noteText.match(/^<div class="zotero-note znv[0-9]+">[\s\S]*<\/div>$/)) {
|
|
// Keep consistent with getNote()
|
|
noteText = '<div class="zotero-note znv1">' + noteText + '</div>';
|
|
}
|
|
|
|
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 (parentItemID) {
|
|
reloadParentChildItems[parentItemID] = true;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Attachment
|
|
//
|
|
if (!isNew) {
|
|
// If attachment title changes, update parent attachments
|
|
if (this._changed.itemData && this._changed.itemData[110] && this.isAttachment() && parentItemID) {
|
|
reloadParentChildItems[parentItemID] = true;
|
|
}
|
|
}
|
|
|
|
if (this._changed.attachmentData) {
|
|
let sql = "REPLACE INTO itemAttachments "
|
|
+ "(itemID, parentItemID, linkMode, contentType, charsetID, path, "
|
|
+ "syncState, storageModTime, storageHash) "
|
|
+ "VALUES (?,?,?,?,?,?,?,?,?)";
|
|
let linkMode = this.attachmentLinkMode;
|
|
let contentType = this.attachmentContentType;
|
|
let charsetID = this.attachmentCharset
|
|
? Zotero.CharacterSets.getID(this.attachmentCharset)
|
|
: null;
|
|
let path = this.attachmentPath;
|
|
let syncState = this.attachmentSyncState;
|
|
let storageModTime = this.attachmentSyncedModificationTime;
|
|
let storageHash = this.attachmentSyncedHash;
|
|
|
|
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE && libraryType != 'user') {
|
|
throw new Error("Linked files can only be added to user library");
|
|
}
|
|
|
|
let params = [
|
|
itemID,
|
|
parentItemID,
|
|
{ int: linkMode },
|
|
contentType ? { string: contentType } : null,
|
|
charsetID ? { int: charsetID } : null,
|
|
path ? { string: path } : null,
|
|
syncState !== undefined ? syncState : 0,
|
|
storageModTime !== undefined ? storageModTime : null,
|
|
storageHash || null
|
|
];
|
|
yield Zotero.DB.queryAsync(sql, params);
|
|
|
|
// Clear cached child attachments of the parent
|
|
if (!isNew && parentItemID) {
|
|
reloadParentChildItems[parentItemID] = 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(x => JSON.stringify(x));
|
|
let newTagsJSON = newTags.map(x => JSON.stringify(x));
|
|
let toAdd = Zotero.Utilities.arrayDiff(newTagsJSON, oldTagsJSON).map(x => JSON.parse(x));
|
|
let toRemove = Zotero.Utilities.arrayDiff(oldTagsJSON, newTagsJSON).map(x => JSON.parse(x));
|
|
|
|
for (let i=0; i<toAdd.length; i++) {
|
|
let tag = toAdd[i];
|
|
let tagID = yield Zotero.Tags.create(tag.tag);
|
|
let tagType = tag.type ? tag.type : 0;
|
|
// "OR REPLACE" allows changing type
|
|
let sql = "INSERT OR REPLACE INTO itemTags (itemID, tagID, type) VALUES (?, ?, ?)";
|
|
yield Zotero.DB.queryAsync(sql, [this.id, tagID, tagType]);
|
|
|
|
let notifierData = {};
|
|
notifierData[this.id + '-' + tagID] = {
|
|
libraryID: this.libraryID,
|
|
tag: tag.tag,
|
|
type: tagType
|
|
};
|
|
if (!env.options.skipNotifier) {
|
|
Zotero.Notifier.queue(
|
|
'add', 'item-tag', this.id + '-' + tagID, notifierData, env.options.notifierQueue
|
|
);
|
|
}
|
|
}
|
|
|
|
if (toRemove.length) {
|
|
for (let i=0; i<toRemove.length; i++) {
|
|
let tag = toRemove[i];
|
|
let tagID = Zotero.Tags.getID(tag.tag);
|
|
let sql = "DELETE FROM itemTags WHERE itemID=? AND tagID=? AND type=?";
|
|
yield Zotero.DB.queryAsync(sql, [this.id, tagID, tag.type ? tag.type : 0]);
|
|
let notifierData = {};
|
|
notifierData[this.id + '-' + tagID] = {
|
|
libraryID: this.libraryID,
|
|
tag: tag.tag
|
|
};
|
|
|
|
if (!env.options.skipNotifier) {
|
|
Zotero.Notifier.queue(
|
|
'remove', 'item-tag', this.id + '-' + tagID, notifierData, env.options.notifierQueue
|
|
);
|
|
}
|
|
}
|
|
Zotero.Prefs.set('purge.tags', true);
|
|
}
|
|
}
|
|
|
|
// Collections
|
|
if (this._changed.collections) {
|
|
if (libraryType == 'publications') {
|
|
throw new Error("Items in My Publications cannot be added to collections");
|
|
}
|
|
|
|
let oldCollections = this._previousData.collections || [];
|
|
let newCollections = this._collections;
|
|
|
|
let toAdd = Zotero.Utilities.arrayDiff(newCollections, oldCollections);
|
|
let toRemove = Zotero.Utilities.arrayDiff(oldCollections, newCollections);
|
|
|
|
env.collectionsAdded = toAdd;
|
|
env.collectionsRemoved = toRemove;
|
|
|
|
if (toAdd.length) {
|
|
for (let i=0; i<toAdd.length; i++) {
|
|
let collectionID = toAdd[i];
|
|
|
|
let sql = "SELECT IFNULL(MAX(orderIndex)+1, 0) FROM collectionItems "
|
|
+ "WHERE collectionID=?";
|
|
let orderIndex = yield Zotero.DB.valueQueryAsync(sql, collectionID);
|
|
|
|
sql = "INSERT OR IGNORE INTO collectionItems "
|
|
+ "(collectionID, itemID, orderIndex) VALUES (?, ?, ?)";
|
|
yield Zotero.DB.queryAsync(sql, [collectionID, this.id, orderIndex]);
|
|
|
|
if (!env.options.skipNotifier) {
|
|
Zotero.Notifier.queue(
|
|
'add',
|
|
'collection-item',
|
|
collectionID + '-' + this.id,
|
|
{},
|
|
env.options.notifierQueue
|
|
);
|
|
}
|
|
}
|
|
|
|
// Add this item to any loaded collections' cached item lists after commit
|
|
Zotero.DB.addCurrentCallback("commit", function () {
|
|
for (let i = 0; i < toAdd.length; i++) {
|
|
this.ContainerObjectsClass.registerChildItem(toAdd[i], this.id);
|
|
}
|
|
}.bind(this));
|
|
}
|
|
|
|
if (toRemove.length) {
|
|
let sql = "DELETE FROM collectionItems WHERE itemID=? AND collectionID IN ("
|
|
+ toRemove.join(',')
|
|
+ ")";
|
|
yield Zotero.DB.queryAsync(sql, this.id);
|
|
|
|
for (let i=0; i<toRemove.length; i++) {
|
|
let collectionID = toRemove[i];
|
|
|
|
if (!env.options.skipNotifier) {
|
|
Zotero.Notifier.queue(
|
|
'remove',
|
|
'collection-item',
|
|
collectionID + '-' + this.id,
|
|
{},
|
|
env.options.notifierQueue
|
|
);
|
|
}
|
|
}
|
|
|
|
// Remove this item from any loaded collections' cached item lists after commit
|
|
Zotero.DB.addCurrentCallback("commit", function () {
|
|
for (let i = 0; i < toRemove.length; i++) {
|
|
this.ContainerObjectsClass.unregisterChildItem(toRemove[i], this.id);
|
|
}
|
|
}.bind(this));
|
|
}
|
|
}
|
|
|
|
// Update child item counts and contents
|
|
if (reloadParentChildItems) {
|
|
for (let parentItemID in reloadParentChildItems) {
|
|
let parentItem = yield this.ObjectsClass.getAsync(parentItemID);
|
|
yield parentItem.reload(['primaryData', 'childItems'], true);
|
|
parentItem.clearBestAttachmentState();
|
|
}
|
|
}
|
|
|
|
Zotero.DB.requireTransaction();
|
|
});
|
|
|
|
Zotero.Item.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
|
|
if (!env.skipCache) {
|
|
// Always reload primary data. DataObject.reload() only reloads changed data types, so
|
|
// it won't reload, say, dateModified and firstCreator if only creator data was changed
|
|
// and not primaryData.
|
|
yield this.loadPrimaryData(true);
|
|
yield this.reload();
|
|
// If new, there's no other data we don't have, so we can mark everything as loaded
|
|
if (env.isNew) {
|
|
this._markAllDataTypeLoadStates(true);
|
|
}
|
|
this._clearChanged();
|
|
}
|
|
|
|
return env.isNew ? this.id : true;
|
|
});
|
|
|
|
|
|
Zotero.Item.prototype.isRegularItem = function() {
|
|
return !(this.isNote() || this.isAttachment());
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.isTopLevelItem = function () {
|
|
return this.isRegularItem() || !this.parentKey;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.numChildren = function(includeTrashed) {
|
|
return this.numNotes(includeTrashed) + this.numAttachments(includeTrashed);
|
|
}
|
|
|
|
|
|
/**
|
|
* @return {String|FALSE} Key of the parent item for an attachment or note, or FALSE if none
|
|
*/
|
|
Zotero.Item.prototype.getSourceKey = function() {
|
|
Zotero.debug("Zotero.Item.prototype.getSource() is deprecated -- use .parentKey");
|
|
return this._parentKey;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.setSourceKey = function(sourceItemKey) {
|
|
Zotero.debug("Zotero.Item.prototype.setSourceKey() is deprecated -- use .parentKey");
|
|
return this.parentKey = sourceItemKey;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////
|
|
//
|
|
// Methods dealing with note items
|
|
//
|
|
////////////////////////////////////////////////////////
|
|
Zotero.Item.prototype.incrementNumNotes = function () {
|
|
this._numNotes++;
|
|
}
|
|
|
|
Zotero.Item.prototype.incrementNumNotesTrashed = function () {
|
|
this._numNotesTrashed++;
|
|
}
|
|
|
|
Zotero.Item.prototype.incrementNumNotesEmbedded = function () {
|
|
this._numNotesEmbedded++;
|
|
}
|
|
|
|
Zotero.Item.prototype.incrementNumNotesTrashed = function () {
|
|
this._numNotesEmbeddedTrashed++;
|
|
}
|
|
|
|
Zotero.Item.prototype.decrementNumNotes = function () {
|
|
this._numNotes--;
|
|
}
|
|
|
|
Zotero.Item.prototype.decrementNumNotesTrashed = function () {
|
|
this._numNotesTrashed--;
|
|
}
|
|
|
|
Zotero.Item.prototype.decrementNumNotesEmbedded = function () {
|
|
this._numNotesEmbedded--;
|
|
}
|
|
|
|
Zotero.Item.prototype.decrementNumNotesTrashed = function () {
|
|
this._numNotesEmbeddedTrashed--;
|
|
}
|
|
|
|
|
|
/**
|
|
* Determine if an item is a note
|
|
**/
|
|
Zotero.Item.prototype.isNote = function() {
|
|
return Zotero.ItemTypes.getName(this.itemTypeID) == 'note';
|
|
}
|
|
|
|
|
|
/**
|
|
* Update an item note
|
|
*
|
|
* Note: This can only be called on saved notes and attachments
|
|
**/
|
|
Zotero.Item.prototype.updateNote = function(text) {
|
|
throw ('updateNote() removed -- use setNote() and save()');
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns number of child notes of item
|
|
*
|
|
* @param {Boolean} includeTrashed Include trashed child items in count
|
|
* @param {Boolean} includeEmbedded Include notes embedded in attachments
|
|
* @return {Integer}
|
|
*/
|
|
Zotero.Item.prototype.numNotes = function(includeTrashed, includeEmbedded) {
|
|
if (this.isNote()) {
|
|
throw ("numNotes() cannot be called on items of type 'note'");
|
|
}
|
|
var cacheKey = '_numNotes';
|
|
if (includeTrashed && includeEmbedded) {
|
|
return this[cacheKey] + this[cacheKey + "EmbeddedTrashed"];
|
|
}
|
|
else if (includeTrashed) {
|
|
return this[cacheKey] + this[cacheKey + "Trashed"];
|
|
}
|
|
else if (includeEmbedded) {
|
|
return this[cacheKey] + this[cacheKey + "Embedded"];
|
|
}
|
|
return this[cacheKey];
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the first line of the note for display in the items list
|
|
*
|
|
* @return {String}
|
|
*/
|
|
Zotero.Item.prototype.getNoteTitle = function() {
|
|
if (!this.isNote() && !this.isAttachment()) {
|
|
throw ("getNoteTitle() can only be called on notes and attachments");
|
|
}
|
|
if (this._noteTitle !== null) {
|
|
return this._noteTitle;
|
|
}
|
|
this._requireData('itemData');
|
|
return "";
|
|
};
|
|
|
|
|
|
Zotero.Item.prototype.hasNote = Zotero.Promise.coroutine(function* () {
|
|
if (!this.isNote() && !this.isAttachment()) {
|
|
throw new Error("hasNote() can only be called on notes and attachments");
|
|
}
|
|
|
|
if (this._hasNote !== null) {
|
|
return this._hasNote;
|
|
}
|
|
|
|
if (!this._id) {
|
|
return false;
|
|
}
|
|
|
|
var sql = "SELECT COUNT(*) FROM itemNotes WHERE itemID=? "
|
|
+ "AND note!='' AND note!=?";
|
|
var hasNote = !!(yield Zotero.DB.valueQueryAsync(sql, [this._id, Zotero.Notes.defaultNote]));
|
|
|
|
this._hasNote = hasNote;
|
|
return hasNote;
|
|
});
|
|
|
|
|
|
/**
|
|
* Get the text of an item note
|
|
**/
|
|
Zotero.Item.prototype.getNote = function() {
|
|
if (!this.isNote() && !this.isAttachment()) {
|
|
throw ("getNote() can only be called on notes and attachments");
|
|
}
|
|
|
|
// Store access time for later garbage collection
|
|
this._noteAccessTime = new Date();
|
|
|
|
if (this._noteText !== null) {
|
|
return this._noteText;
|
|
}
|
|
|
|
this._requireData('note');
|
|
return "";
|
|
}
|
|
|
|
|
|
/**
|
|
* Set an item note
|
|
*
|
|
* Note: This can only be called on notes and attachments
|
|
**/
|
|
Zotero.Item.prototype.setNote = function(text) {
|
|
if (!this.isNote() && !this.isAttachment()) {
|
|
throw ("updateNote() can only be called on notes and attachments");
|
|
}
|
|
|
|
if (typeof text != 'string') {
|
|
throw ("text must be a string in Zotero.Item.setNote() (was " + typeof text + ")");
|
|
}
|
|
|
|
text = text
|
|
// Strip control characters
|
|
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "")
|
|
.trim();
|
|
|
|
var oldText = this.getNote();
|
|
if (text === oldText) {
|
|
Zotero.debug("Note hasn't changed", 4);
|
|
return false;
|
|
}
|
|
|
|
this._hasNote = text !== '';
|
|
this._noteText = text;
|
|
this._noteTitle = Zotero.Notes.noteToTitle(text);
|
|
if (this.isNote()) {
|
|
this._displayTitle = this._noteTitle;
|
|
}
|
|
|
|
this._markFieldChange('note', oldText);
|
|
this._changed.note = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns child notes of this item
|
|
*
|
|
* @param {Boolean} includeTrashed Include trashed child items
|
|
* @param {Boolean} includeEmbedded Include embedded attachment notes
|
|
* @return {Integer[]} Array of itemIDs
|
|
*/
|
|
Zotero.Item.prototype.getNotes = function(includeTrashed) {
|
|
if (this.isNote()) {
|
|
throw new Error("getNotes() cannot be called on items of type 'note'");
|
|
}
|
|
|
|
this._requireData('childItems');
|
|
|
|
if (!this._notes) {
|
|
return [];
|
|
}
|
|
|
|
var sortChronologically = Zotero.Prefs.get('sortNotesChronologically');
|
|
var cacheKey = (sortChronologically ? "chronological" : "alphabetical")
|
|
+ 'With' + (includeTrashed ? '' : 'out') + 'Trashed';
|
|
|
|
if (this._notes[cacheKey]) {
|
|
return this._notes[cacheKey];
|
|
}
|
|
|
|
var rows = this._notes.rows.concat();
|
|
// Remove trashed items if necessary
|
|
if (!includeTrashed) {
|
|
rows = rows.filter(function (row) !row.trashed);
|
|
}
|
|
// Sort by title if necessary
|
|
if (!sortChronologically) {
|
|
var collation = Zotero.getLocaleCollation();
|
|
rows.sort((a, b) => {
|
|
var aTitle = this.ObjectsClass.getSortTitle(a.title);
|
|
var bTitle = this.ObjectsClass.getSortTitle(b.title);
|
|
return collation.compareString(1, aTitle, bTitle);
|
|
});
|
|
}
|
|
var ids = rows.map(row => row.itemID);
|
|
this._notes[cacheKey] = ids;
|
|
return ids;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////
|
|
//
|
|
// Methods dealing with attachments
|
|
//
|
|
// save() is not required for attachment functions
|
|
//
|
|
///////////////////////////////////////////////////////
|
|
/**
|
|
* Determine if an item is an attachment
|
|
**/
|
|
Zotero.Item.prototype.isAttachment = function() {
|
|
return Zotero.ItemTypes.getName(this.itemTypeID) == 'attachment';
|
|
}
|
|
|
|
|
|
/**
|
|
* @return {Promise<Boolean>}
|
|
*/
|
|
Zotero.Item.prototype.isImportedAttachment = function() {
|
|
if (!this.isAttachment()) {
|
|
return false;
|
|
}
|
|
var linkMode = this.attachmentLinkMode;
|
|
if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE || linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* @return {Promise<Boolean>}
|
|
*/
|
|
Zotero.Item.prototype.isWebAttachment = function() {
|
|
if (!this.isAttachment()) {
|
|
return false;
|
|
}
|
|
var linkMode = this.attachmentLinkMode;
|
|
if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE || linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* @return {Promise<Boolean>}
|
|
*/
|
|
Zotero.Item.prototype.isFileAttachment = function() {
|
|
if (!this.isAttachment()) {
|
|
return false;
|
|
}
|
|
return this.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_URL;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns number of child attachments of item
|
|
*
|
|
* @param {Boolean} includeTrashed Include trashed child items in count
|
|
* @return <Integer>
|
|
*/
|
|
Zotero.Item.prototype.numAttachments = function(includeTrashed) {
|
|
if (this.isAttachment()) {
|
|
throw ("numAttachments() cannot be called on attachment items");
|
|
}
|
|
|
|
var cacheKey = '_numAttachments';
|
|
if (includeTrashed) {
|
|
return this[cacheKey] + this[cacheKey + "Trashed"];
|
|
}
|
|
return this[cacheKey];
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.getFile = function () {
|
|
Zotero.debug("Zotero.Item.prototype.getFile() is deprecated -- use getFilePath[Async]()", 2);
|
|
|
|
var path = this.getFilePath();
|
|
if (path) {
|
|
return Zotero.File.pathToFile(path);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the absolute file path for the attachment
|
|
*
|
|
* @return {string|false} - The absolute file path of the attachment, or false for invalid paths
|
|
*/
|
|
Zotero.Item.prototype.getFilePath = function () {
|
|
if (!this.isAttachment()) {
|
|
throw new Error("getFilePath() can only be called on attachment items");
|
|
}
|
|
|
|
var linkMode = this.attachmentLinkMode;
|
|
var path = this.attachmentPath;
|
|
|
|
// No associated files for linked URLs
|
|
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
|
return false;
|
|
}
|
|
|
|
if (!path) {
|
|
Zotero.debug("Attachment path is empty", 2);
|
|
this._updateAttachmentStates(false);
|
|
return false;
|
|
}
|
|
|
|
if (!this._identified) {
|
|
Zotero.debug("Can't get file path for unsaved file");
|
|
return false;
|
|
}
|
|
|
|
// Imported file with relative path
|
|
if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL ||
|
|
linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE) {
|
|
if (path.indexOf("storage:") == -1) {
|
|
Zotero.debug("Invalid attachment path '" + path + "'", 2);
|
|
throw new Error("Invalid path");
|
|
}
|
|
// Strip "storage:"
|
|
path = path.substr(8);
|
|
|
|
// Ignore .zotero* files that were relinked before we started blocking them
|
|
if (path.startsWith(".zotero")) {
|
|
Zotero.debug("Ignoring attachment file " + path, 2);
|
|
return false;
|
|
}
|
|
|
|
return OS.Path.join(
|
|
OS.Path.normalize(Zotero.Attachments.getStorageDirectory(this).path), path
|
|
);
|
|
}
|
|
|
|
// Linked file with relative path
|
|
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE &&
|
|
path.indexOf(Zotero.Attachments.BASE_PATH_PLACEHOLDER) == 0) {
|
|
path = Zotero.Attachments.resolveRelativePath(path);
|
|
if (!path) {
|
|
this._updateAttachmentStates(false);
|
|
}
|
|
return path;
|
|
}
|
|
|
|
// Old-style OS X persistent descriptor (Base64-encoded opaque alias record)
|
|
//
|
|
// These should only exist if they weren't converted in the 80 DB upgrade step because
|
|
// the file couldn't be found.
|
|
if (Zotero.isMac && path.startsWith('AAAA')) {
|
|
let file = Components.classes["@mozilla.org/file/local;1"]
|
|
.createInstance(Components.interfaces.nsILocalFile);
|
|
try {
|
|
file.persistentDescriptor = path;
|
|
}
|
|
catch (e) {
|
|
this._updateAttachmentStates(false);
|
|
return false;
|
|
}
|
|
|
|
// If valid, convert this to a regular string in the background
|
|
Zotero.DB.queryAsync(
|
|
"UPDATE itemAttachments SET path=? WHERE itemID=?",
|
|
[file.leafName, this._id]
|
|
);
|
|
|
|
return file.path;
|
|
}
|
|
|
|
return path;
|
|
};
|
|
|
|
|
|
/**
|
|
* Get the absolute path for the attachment, if the file exists
|
|
*
|
|
* @return {Promise<String|false>} - A promise for either the absolute path of the attachment
|
|
* or false for invalid paths or if the file doesn't exist
|
|
*/
|
|
Zotero.Item.prototype.getFilePathAsync = Zotero.Promise.coroutine(function* () {
|
|
if (!this.isAttachment()) {
|
|
throw new Error("getFilePathAsync() can only be called on attachment items");
|
|
}
|
|
|
|
var linkMode = this.attachmentLinkMode;
|
|
var path = this.attachmentPath;
|
|
|
|
// No associated files for linked URLs
|
|
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
|
this._updateAttachmentStates(false);
|
|
return false;
|
|
}
|
|
|
|
if (!path) {
|
|
Zotero.debug("Attachment path is empty", 2);
|
|
this._updateAttachmentStates(false);
|
|
return false;
|
|
}
|
|
|
|
// Imported file with relative path
|
|
if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL ||
|
|
linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE) {
|
|
if (path.indexOf("storage:") == -1) {
|
|
Zotero.debug("Invalid attachment path '" + path + "'", 2);
|
|
throw new Error("Invalid path");
|
|
}
|
|
|
|
// Strip "storage:"
|
|
path = path.substr(8);
|
|
|
|
// Ignore .zotero* files that were relinked before we started blocking them
|
|
if (path.startsWith(".zotero")) {
|
|
Zotero.debug("Ignoring attachment file " + path, 2);
|
|
this._updateAttachmentStates(false);
|
|
return false;
|
|
}
|
|
|
|
path = OS.Path.join(
|
|
OS.Path.normalize(Zotero.Attachments.getStorageDirectory(this).path), path
|
|
);
|
|
|
|
if (!(yield OS.File.exists(path))) {
|
|
Zotero.debug("Attachment file '" + path + "' not found", 2);
|
|
this._updateAttachmentStates(false);
|
|
return false;
|
|
}
|
|
|
|
this._updateAttachmentStates(true);
|
|
return path;
|
|
}
|
|
|
|
// Linked file with relative path
|
|
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE &&
|
|
path.indexOf(Zotero.Attachments.BASE_PATH_PLACEHOLDER) == 0) {
|
|
path = Zotero.Attachments.resolveRelativePath(path);
|
|
if (!path) {
|
|
this._updateAttachmentStates(false);
|
|
return false;
|
|
}
|
|
if (!(yield OS.File.exists(path))) {
|
|
Zotero.debug("Attachment file '" + path + "' not found", 2);
|
|
this._updateAttachmentStates(false);
|
|
return false;
|
|
}
|
|
|
|
this._updateAttachmentStates(true);
|
|
return path;
|
|
}
|
|
|
|
// Old-style OS X persistent descriptor (Base64-encoded opaque alias record)
|
|
//
|
|
// These should only exist if they weren't converted in the 80 DB upgrade step because
|
|
// the file couldn't be found
|
|
if (Zotero.isMac && path.startsWith('AAAA')) {
|
|
let file = Components.classes["@mozilla.org/file/local;1"]
|
|
.createInstance(Components.interfaces.nsILocalFile);
|
|
try {
|
|
file.persistentDescriptor = path;
|
|
}
|
|
catch (e) {
|
|
this._updateAttachmentStates(false);
|
|
return false;
|
|
}
|
|
|
|
// If valid, convert this to a regular string
|
|
yield Zotero.DB.queryAsync(
|
|
"UPDATE itemAttachments SET path=? WHERE itemID=?",
|
|
[file.leafName, this._id]
|
|
);
|
|
|
|
if (!(yield OS.File.exists(file.path))) {
|
|
Zotero.debug("Attachment file '" + file.path + "' not found", 2);
|
|
this._updateAttachmentStates(false);
|
|
return false;
|
|
}
|
|
|
|
this._updateAttachmentStates(true);
|
|
|
|
return file.path;
|
|
}
|
|
|
|
if (!(yield OS.File.exists(path))) {
|
|
Zotero.debug("Attachment file '" + path + "' not found", 2);
|
|
this._updateAttachmentStates(false);
|
|
return false;
|
|
}
|
|
|
|
this._updateAttachmentStates(true);
|
|
|
|
return path;
|
|
});
|
|
|
|
|
|
/**
|
|
* Update file existence state of this item and best attachment state of parent item
|
|
*/
|
|
Zotero.Item.prototype._updateAttachmentStates = function (exists) {
|
|
this._fileExists = exists;
|
|
|
|
if (this.isTopLevelItem()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
var parentKey = this.parentKey;
|
|
}
|
|
// This can happen during classic sync conflict resolution, if a
|
|
// standalone attachment was modified locally and remotely was changed
|
|
// into a child attachment
|
|
catch (e) {
|
|
Zotero.logError(`Attachment parent ${this.libraryID}/${parentKey} doesn't exist for `
|
|
+ "source key in Zotero.Item.updateAttachmentStates()");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
var item = this.ObjectsClass.getByLibraryAndKey(this.libraryID, parentKey);
|
|
}
|
|
catch (e) {
|
|
if (e instanceof Zotero.Exception.UnloadedDataException) {
|
|
Zotero.logError(`Attachment parent ${this.libraryID}/${parentKey} not yet loaded in `
|
|
+ "Zotero.Item.updateAttachmentStates()");
|
|
return;
|
|
}
|
|
throw e;
|
|
}
|
|
if (!item) {
|
|
Zotero.logError(`Attachment parent ${this.libraryID}/${parentKey} doesn't exist`);
|
|
return;
|
|
}
|
|
item.clearBestAttachmentState();
|
|
};
|
|
|
|
|
|
Zotero.Item.prototype.getFilename = function () {
|
|
Zotero.debug("getFilename() deprecated -- use .attachmentFilename");
|
|
return this.attachmentFilename;
|
|
}
|
|
|
|
|
|
/**
|
|
* Asynchronous check for file existence
|
|
*/
|
|
Zotero.Item.prototype.fileExists = Zotero.Promise.coroutine(function* () {
|
|
if (!this.isAttachment()) {
|
|
throw new Error("Zotero.Item.fileExists() can only be called on attachment items");
|
|
}
|
|
|
|
if (this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
|
throw new Error("Zotero.Item.fileExists() cannot be called on link attachments");
|
|
}
|
|
|
|
return !!(yield this.getFilePathAsync());
|
|
});
|
|
|
|
|
|
/**
|
|
* Synchronous cached check for file existence, used for items view
|
|
*/
|
|
Zotero.Item.prototype.fileExistsCached = function () {
|
|
return this._fileExists;
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* Rename file associated with an attachment
|
|
*
|
|
* -1 Destination file exists -- use _force_ to overwrite
|
|
* -2 Error renaming
|
|
* false Attachment file not found
|
|
*/
|
|
Zotero.Item.prototype.renameAttachmentFile = Zotero.Promise.coroutine(function* (newName, overwrite) {
|
|
var origPath = yield this.getFilePathAsync();
|
|
if (!origPath) {
|
|
Zotero.debug("Attachment file not found in renameAttachmentFile()", 2);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
var origName = OS.Path.basename(origPath);
|
|
var origModDate = (yield OS.File.stat(origPath)).lastModificationDate;
|
|
|
|
newName = Zotero.File.getValidFileName(newName);
|
|
|
|
// Ignore if no change
|
|
if (origName === newName) {
|
|
Zotero.debug("Filename has not changed");
|
|
return true;
|
|
}
|
|
|
|
var destPath = OS.Path.join(OS.Path.dirname(origPath), newName);
|
|
var destName = OS.Path.basename(destPath);
|
|
|
|
// Update mod time and clear hash so the file syncs
|
|
// TODO: use an integer counter instead of mod time for change detection
|
|
// Update mod time first, because it may fail for read-only files on Windows
|
|
yield OS.File.setDates(origPath, null, null);
|
|
var result = yield OS.File.move(origPath, destPath, { noOverwrite: !overwrite })
|
|
// If no overwriting and file exists, return -1
|
|
.catch(OS.File.Error, function (e) {
|
|
if (e.becauseExists) {
|
|
return -1;
|
|
}
|
|
throw e;
|
|
});
|
|
if (result) {
|
|
return result;
|
|
}
|
|
|
|
yield this.relinkAttachmentFile(destPath);
|
|
|
|
if (this.isImportedAttachment()) {
|
|
this.attachmentSyncedHash = null;
|
|
this.attachmentSyncState = "to_upload";
|
|
yield this.saveTx({ skipAll: true });
|
|
}
|
|
|
|
return true;
|
|
}
|
|
catch (e) {
|
|
// Restore original modification date in case we managed to change it
|
|
try {
|
|
OS.File.setDates(origPath, null, origModDate);
|
|
} catch (e) {
|
|
Zotero.debug(e, 2);
|
|
}
|
|
Zotero.debug(e);
|
|
Components.utils.reportError(e);
|
|
return -2;
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* @param {string} path File path
|
|
* @param {Boolean} [skipItemUpdate] Don't update attachment item mod time, so that item doesn't
|
|
* sync. Used when a file needs to be renamed to be accessible but the user doesn't have
|
|
* access to modify the attachment metadata. This also allows a save when the library is
|
|
* read-only.
|
|
*/
|
|
Zotero.Item.prototype.relinkAttachmentFile = Zotero.Promise.coroutine(function* (path, skipItemUpdate) {
|
|
if (path instanceof Components.interfaces.nsIFile) {
|
|
Zotero.debug("WARNING: Zotero.Item.prototype.relinkAttachmentFile() now takes an absolute "
|
|
+ "file path instead of an nsIFile");
|
|
path = path.path;
|
|
}
|
|
|
|
var linkMode = this.attachmentLinkMode;
|
|
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
|
throw new Error('Cannot relink linked URL');
|
|
}
|
|
|
|
var fileName = OS.Path.basename(path);
|
|
if (fileName.endsWith(".lnk")) {
|
|
throw new Error("Cannot relink to Windows shortcut");
|
|
}
|
|
var newPath;
|
|
var newName = Zotero.File.getValidFileName(fileName);
|
|
if (!newName) {
|
|
throw new Error("No valid characters in filename after filtering");
|
|
}
|
|
|
|
// If selected file isn't in the attachment's storage directory,
|
|
// copy it in and use that one instead
|
|
var storageDir = Zotero.Attachments.getStorageDirectory(this).path;
|
|
if (this.isImportedAttachment() && OS.Path.dirname(path) != storageDir) {
|
|
newPath = OS.Path.join(storageDir, newName);
|
|
|
|
// If file with same name already exists in the storage directory,
|
|
// move it out of the way
|
|
let backupCreated = false;
|
|
if (yield OS.File.exists(newPath)) {
|
|
backupCreated = true;
|
|
yield OS.File.move(newPath, newPath + ".bak");
|
|
}
|
|
// Create storage directory if necessary
|
|
else if (!(yield OS.File.exists(storageDir))) {
|
|
yield Zotero.Attachments.createDirectoryForItem(this);
|
|
}
|
|
|
|
let newFile;
|
|
try {
|
|
newFile = Zotero.File.copyToUnique(path, newPath);
|
|
}
|
|
catch (e) {
|
|
// Restore backup file if copying failed
|
|
if (backupCreated) {
|
|
yield OS.File.move(newPath + ".bak", newPath);
|
|
}
|
|
throw e;
|
|
}
|
|
newPath = newFile.path;
|
|
|
|
// Delete backup file
|
|
if (backupCreated) {
|
|
yield OS.File.remove(newPath + ".bak");
|
|
}
|
|
}
|
|
else {
|
|
newPath = OS.Path.join(OS.Path.dirname(path), newName);
|
|
|
|
// Rename file to filtered name if necessary
|
|
if (fileName != newName) {
|
|
Zotero.debug("Renaming file '" + fileName + "' to '" + newName + "'");
|
|
yield OS.File.move(path, newPath, { noOverwrite: true });
|
|
}
|
|
}
|
|
|
|
this.attachmentPath = newPath;
|
|
|
|
yield this.saveTx({
|
|
skipDateModifiedUpdate: true,
|
|
skipClientDateModifiedUpdate: skipItemUpdate,
|
|
skipEditCheck: skipItemUpdate
|
|
});
|
|
|
|
this._updateAttachmentStates(true);
|
|
yield Zotero.Notifier.trigger('refresh', 'item', this.id);
|
|
|
|
return true;
|
|
});
|
|
|
|
|
|
Zotero.Item.prototype.deleteAttachmentFile = Zotero.Promise.coroutine(function* () {
|
|
if (!this.isImportedAttachment()) {
|
|
throw new Error("deleteAttachmentFile() can only be called on imported attachment items");
|
|
}
|
|
|
|
var path = yield this.getFilePathAsync();
|
|
if (!path) {
|
|
Zotero.debug(`File not found for item ${this.libraryKey} in deleteAttachmentFile()`, 2);
|
|
return false;
|
|
}
|
|
|
|
Zotero.debug("Deleting attachment file for item " + this.libraryKey);
|
|
try {
|
|
yield Zotero.File.removeIfExists(path);
|
|
this.attachmentSyncState = "to_download";
|
|
yield this.saveTx({ skipAll: true });
|
|
return true;
|
|
}
|
|
catch (e) {
|
|
Zotero.logError(e);
|
|
return false;
|
|
}
|
|
});
|
|
|
|
|
|
|
|
/*
|
|
* Return a file:/// URL path to files and snapshots
|
|
*/
|
|
Zotero.Item.prototype.getLocalFileURL = function() {
|
|
if (!this.isAttachment) {
|
|
throw ("getLocalFileURL() can only be called on attachment items");
|
|
}
|
|
|
|
var file = this.getFile();
|
|
if (!file) {
|
|
return false;
|
|
}
|
|
|
|
var nsIFPH = Components.classes["@mozilla.org/network/protocol;1?name=file"]
|
|
.getService(Components.interfaces.nsIFileProtocolHandler);
|
|
return nsIFPH.getURLSpecFromFile(file);
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.getAttachmentLinkMode = function() {
|
|
Zotero.debug("getAttachmentLinkMode() deprecated -- use .attachmentLinkMode");
|
|
return this.attachmentLinkMode;
|
|
}
|
|
|
|
/**
|
|
* Link mode of an attachment
|
|
*
|
|
* Possible values specified as constants in Zotero.Attachments
|
|
* (e.g. Zotero.Attachments.LINK_MODE_LINKED_FILE)
|
|
*/
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'attachmentLinkMode', {
|
|
get: function() {
|
|
if (!this.isAttachment()) {
|
|
return undefined;
|
|
}
|
|
return this._attachmentLinkMode;
|
|
},
|
|
set: function(val) {
|
|
if (!this.isAttachment()) {
|
|
throw (".attachmentLinkMode can only be set for attachment items");
|
|
}
|
|
|
|
// Allow 'imported_url', etc.
|
|
if (typeof val == 'string') {
|
|
let code = Zotero.Attachments["LINK_MODE_" + val.toUpperCase()];
|
|
if (code !== undefined) {
|
|
val = code;
|
|
}
|
|
}
|
|
|
|
switch (val) {
|
|
case Zotero.Attachments.LINK_MODE_IMPORTED_FILE:
|
|
case Zotero.Attachments.LINK_MODE_IMPORTED_URL:
|
|
case Zotero.Attachments.LINK_MODE_LINKED_FILE:
|
|
case Zotero.Attachments.LINK_MODE_LINKED_URL:
|
|
break;
|
|
|
|
default:
|
|
throw ("Invalid attachment link mode '" + val
|
|
+ "' in Zotero.Item.attachmentLinkMode setter");
|
|
}
|
|
|
|
if (val === this.attachmentLinkMode) {
|
|
return;
|
|
}
|
|
if (!this._changed.attachmentData) {
|
|
this._changed.attachmentData = {};
|
|
}
|
|
this._changed.attachmentData.linkMode = true;
|
|
this._attachmentLinkMode = val;
|
|
}
|
|
});
|
|
|
|
|
|
Zotero.Item.prototype.getAttachmentMIMEType = function() {
|
|
Zotero.debug("getAttachmentMIMEType() deprecated -- use .attachmentContentType");
|
|
return this.attachmentContentType;
|
|
};
|
|
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'attachmentMIMEType', {
|
|
get: function() {
|
|
Zotero.debug(".attachmentMIMEType deprecated -- use .attachmentContentType");
|
|
return this.attachmentContentType;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Content type of an attachment (e.g. 'text/plain')
|
|
*/
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'attachmentContentType', {
|
|
get: function() {
|
|
if (!this.isAttachment()) {
|
|
return undefined;
|
|
}
|
|
return this._attachmentContentType;
|
|
},
|
|
set: function(val) {
|
|
if (!this.isAttachment()) {
|
|
throw (".attachmentContentType can only be set for attachment items");
|
|
}
|
|
|
|
if (!val) {
|
|
val = '';
|
|
}
|
|
|
|
if (val == this.attachmentContentType) {
|
|
return;
|
|
}
|
|
|
|
if (!this._changed.attachmentData) {
|
|
this._changed.attachmentData = {};
|
|
}
|
|
this._changed.attachmentData.contentType = true;
|
|
this._attachmentContentType = val;
|
|
}
|
|
});
|
|
|
|
|
|
Zotero.Item.prototype.getAttachmentCharset = function() {
|
|
Zotero.debug("getAttachmentCharset() deprecated -- use .attachmentCharset");
|
|
return this.attachmentCharset;
|
|
}
|
|
|
|
|
|
/**
|
|
* Character set of an attachment
|
|
*/
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'attachmentCharset', {
|
|
get: function() {
|
|
if (!this.isAttachment()) {
|
|
return undefined;
|
|
}
|
|
return this._attachmentCharset
|
|
},
|
|
set: function(val) {
|
|
if (!this.isAttachment()) {
|
|
throw (".attachmentCharset can only be set for attachment items");
|
|
}
|
|
|
|
if (typeof val == 'number') {
|
|
throw new Error("Character set must be a string");
|
|
}
|
|
oldVal = this.attachmentCharset;
|
|
|
|
if (val) {
|
|
val = Zotero.CharacterSets.toCanonical(val);
|
|
}
|
|
if (!val) {
|
|
val = "";
|
|
}
|
|
|
|
if (val === oldVal) {
|
|
return;
|
|
}
|
|
|
|
if (!this._changed.attachmentData) {
|
|
this._changed.attachmentData= {};
|
|
}
|
|
this._changed.attachmentData.charset = true;
|
|
this._attachmentCharset = val;
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* Get or set the filename of file attachments
|
|
*
|
|
* This will return the filename for all file attachments, but the filename can only be set
|
|
* for stored file attachments. Linked file attachments should be set using .attachmentPath.
|
|
*/
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'attachmentFilename', {
|
|
get: function () {
|
|
if (!this.isAttachment()) {
|
|
return undefined;
|
|
}
|
|
var path = this.attachmentPath;
|
|
if (!path) {
|
|
return '';
|
|
}
|
|
var prefixedPath = path.match(/^(?:attachments|storage):(.+)$/);
|
|
if (prefixedPath) {
|
|
return prefixedPath[1];
|
|
}
|
|
return OS.Path.basename(path);
|
|
},
|
|
set: function (val) {
|
|
if (!this.isAttachment()) {
|
|
throw new Error("Attachment filename can only be set for attachment items");
|
|
}
|
|
var linkMode = this.attachmentLinkMode;
|
|
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE
|
|
|| linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
|
throw new Error("Attachment filename can only be set for stored files");
|
|
}
|
|
|
|
if (!val) {
|
|
throw new Error("Attachment filename cannot be blank");
|
|
}
|
|
|
|
this.attachmentPath = 'storage:' + val;
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* Returns raw attachment path string as stored in DB
|
|
* (e.g., "storage:foo.pdf", "attachments:foo/bar.pdf", "/Users/foo/Desktop/bar.pdf")
|
|
*
|
|
* Can be set as absolute path or prefixed string ("storage:foo.pdf")
|
|
*/
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'attachmentPath', {
|
|
get: function() {
|
|
if (!this.isAttachment()) {
|
|
return undefined;
|
|
}
|
|
return this._attachmentPath;
|
|
},
|
|
set: function(val) {
|
|
if (!this.isAttachment()) {
|
|
throw new Error(".attachmentPath can only be set for attachment items");
|
|
}
|
|
|
|
if (typeof val != 'string') {
|
|
throw new Error(".attachmentPath must be a string");
|
|
}
|
|
|
|
var linkMode = this.attachmentLinkMode;
|
|
if (linkMode === null) {
|
|
throw new Error("Link mode must be set before setting attachment path");
|
|
}
|
|
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
|
throw new Error('attachmentPath cannot be set for link attachments');
|
|
}
|
|
|
|
if (!val) {
|
|
val = '';
|
|
}
|
|
|
|
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
|
|
if (this._libraryID) {
|
|
let libraryType = Zotero.Libraries.get(this._libraryID).libraryType;
|
|
if (libraryType != 'user') {
|
|
throw new Error("Linked files can only be added to user library");
|
|
}
|
|
}
|
|
|
|
// If base directory is enabled, save attachment within as relative path
|
|
if (Zotero.Prefs.get('saveRelativeAttachmentPath')) {
|
|
val = Zotero.Attachments.getBaseDirectoryRelativePath(val);
|
|
}
|
|
// Otherwise, convert relative path to absolute if possible
|
|
else {
|
|
val = Zotero.Attachments.resolveRelativePath(val) || val;
|
|
}
|
|
}
|
|
else if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL ||
|
|
linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE) {
|
|
if (!val.startsWith('storage:')) {
|
|
let storagePath = Zotero.Attachments.getStorageDirectory(this).path;
|
|
if (!val.startsWith(storagePath)) {
|
|
throw new Error("Imported file path must be within storage directory");
|
|
}
|
|
val = 'storage:' + OS.Path.basename(val);
|
|
}
|
|
}
|
|
|
|
if (val == this.attachmentPath) {
|
|
return;
|
|
}
|
|
|
|
if (!this._changed.attachmentData) {
|
|
this._changed.attachmentData = {};
|
|
}
|
|
this._changed.attachmentData.path = true;
|
|
this._attachmentPath = val;
|
|
}
|
|
});
|
|
|
|
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'attachmentSyncState', {
|
|
get: function() {
|
|
if (!this.isAttachment()) {
|
|
return undefined;
|
|
}
|
|
return this._attachmentSyncState;
|
|
},
|
|
set: function(val) {
|
|
if (!this.isAttachment()) {
|
|
throw new Error("attachmentSyncState can only be set for attachment items");
|
|
}
|
|
|
|
if (typeof val == 'string') {
|
|
val = Zotero.Sync.Storage.Local["SYNC_STATE_" + val.toUpperCase()];
|
|
}
|
|
|
|
switch (this.attachmentLinkMode) {
|
|
case Zotero.Attachments.LINK_MODE_IMPORTED_URL:
|
|
case Zotero.Attachments.LINK_MODE_IMPORTED_FILE:
|
|
break;
|
|
|
|
default:
|
|
throw new Error("attachmentSyncState can only be set for stored files");
|
|
}
|
|
|
|
switch (val) {
|
|
case Zotero.Sync.Storage.Local.SYNC_STATE_TO_UPLOAD:
|
|
case Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD:
|
|
case Zotero.Sync.Storage.Local.SYNC_STATE_IN_SYNC:
|
|
case Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_UPLOAD:
|
|
case Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_DOWNLOAD:
|
|
case Zotero.Sync.Storage.Local.SYNC_STATE_IN_CONFLICT:
|
|
break;
|
|
|
|
default:
|
|
throw new Error("Invalid sync state '" + val + "'");
|
|
}
|
|
|
|
if (val == this.attachmentSyncState) {
|
|
return;
|
|
}
|
|
|
|
if (!this._changed.attachmentData) {
|
|
this._changed.attachmentData = {};
|
|
}
|
|
this._changed.attachmentData.syncState = true;
|
|
this._attachmentSyncState = val;
|
|
}
|
|
});
|
|
|
|
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'attachmentSyncedModificationTime', {
|
|
get: function () {
|
|
if (!this.isFileAttachment()) {
|
|
return undefined;
|
|
}
|
|
return this._attachmentSyncedModificationTime;
|
|
},
|
|
set: function (val) {
|
|
if (!this.isAttachment()) {
|
|
throw ("attachmentSyncedModificationTime can only be set for attachment items");
|
|
}
|
|
|
|
switch (this.attachmentLinkMode) {
|
|
case Zotero.Attachments.LINK_MODE_IMPORTED_URL:
|
|
case Zotero.Attachments.LINK_MODE_IMPORTED_FILE:
|
|
break;
|
|
|
|
default:
|
|
throw new Error("attachmentSyncedModificationTime can only be set for stored files");
|
|
}
|
|
|
|
if (typeof val != 'number') {
|
|
throw new Error("attachmentSyncedModificationTime must be a number");
|
|
}
|
|
if (parseInt(val) != val || val < 0) {
|
|
throw new Error("attachmentSyncedModificationTime must be a timestamp in milliseconds");
|
|
}
|
|
if (val < 10000000000) {
|
|
Zotero.logError("attachmentSyncedModificationTime should be a timestamp in milliseconds "
|
|
+ "-- " + val + " given");
|
|
}
|
|
|
|
if (val == this._attachmentSyncedModificationTime) {
|
|
return;
|
|
}
|
|
|
|
if (!this._changed.attachmentData) {
|
|
this._changed.attachmentData = {};
|
|
}
|
|
this._changed.attachmentData.syncedModificationTime = true;
|
|
this._attachmentSyncedModificationTime = val;
|
|
}
|
|
});
|
|
|
|
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'attachmentSyncedHash', {
|
|
get: function () {
|
|
if (!this.isFileAttachment()) {
|
|
return undefined;
|
|
}
|
|
return this._attachmentSyncedHash;
|
|
},
|
|
set: function (val) {
|
|
if (!this.isAttachment()) {
|
|
throw ("attachmentSyncedHash can only be set for attachment items");
|
|
}
|
|
|
|
switch (this.attachmentLinkMode) {
|
|
case Zotero.Attachments.LINK_MODE_IMPORTED_URL:
|
|
case Zotero.Attachments.LINK_MODE_IMPORTED_FILE:
|
|
break;
|
|
|
|
default:
|
|
throw new Error("attachmentSyncedHash can only be set for stored files");
|
|
}
|
|
|
|
if (val !== null && val.length != 32) {
|
|
throw new Error("Invalid attachment hash '" + val + "'");
|
|
}
|
|
|
|
if (val == this._attachmentSyncedHash) {
|
|
return;
|
|
}
|
|
|
|
if (!this._changed.attachmentData) {
|
|
this._changed.attachmentData = {};
|
|
}
|
|
this._changed.attachmentData.syncedHash = true;
|
|
this._attachmentSyncedHash = val;
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* Modification time of an attachment file
|
|
*
|
|
* Note: This is the mod time of the file itself, not the last-known mod time
|
|
* of the file on the storage server as stored in the database
|
|
*
|
|
* @return {Promise<Number|undefined>} File modification time as timestamp in milliseconds,
|
|
* or undefined if no file
|
|
*/
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'attachmentModificationTime', {
|
|
get: Zotero.Promise.coroutine(function* () {
|
|
if (!this.isFileAttachment()) {
|
|
return undefined;
|
|
}
|
|
|
|
if (!this.id) {
|
|
return undefined;
|
|
}
|
|
|
|
var path = yield this.getFilePathAsync();
|
|
if (!path) {
|
|
return undefined;
|
|
}
|
|
|
|
var fmtime = ((yield OS.File.stat(path)).lastModificationDate).getTime();
|
|
|
|
if (fmtime < 1) {
|
|
Zotero.debug("File mod time " + fmtime + " is less than 1 -- interpreting as 1", 2);
|
|
fmtime = 1;
|
|
}
|
|
|
|
return fmtime;
|
|
})
|
|
});
|
|
|
|
|
|
/**
|
|
* MD5 hash of an attachment file
|
|
*
|
|
* Note: This is the hash of the file itself, not the last-known hash
|
|
* of the file on the storage server as stored in the database
|
|
*
|
|
* @return {Promise<String>} - MD5 hash of file as hex string
|
|
*/
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'attachmentHash', {
|
|
get: Zotero.Promise.coroutine(function* () {
|
|
if (!this.isAttachment()) {
|
|
return undefined;
|
|
}
|
|
|
|
if (!this.id) {
|
|
return undefined;
|
|
}
|
|
|
|
var path = yield this.getFilePathAsync();
|
|
if (!path) {
|
|
return undefined;
|
|
}
|
|
|
|
return Zotero.Utilities.Internal.md5Async(path);
|
|
})
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
* Return plain text of attachment content
|
|
*
|
|
* - Currently works on HTML, PDF and plaintext attachments
|
|
* - Paragraph breaks will be lost in PDF content
|
|
* - For PDFs, will return empty string if Zotero.Fulltext.pdfConverterIsRegistered() is false
|
|
*
|
|
* @return {Promise<String>} - A promise for attachment text or empty string if unavailable
|
|
*/
|
|
Zotero.defineProperty(Zotero.Item.prototype, 'attachmentText', {
|
|
get: Zotero.Promise.coroutine(function* () {
|
|
if (!this.isAttachment()) {
|
|
return undefined;
|
|
}
|
|
|
|
if (!this.id) {
|
|
return null;
|
|
}
|
|
|
|
var file = this.getFile();
|
|
|
|
if (!(yield OS.File.exists(file.path))) {
|
|
file = false;
|
|
}
|
|
|
|
var cacheFile = Zotero.Fulltext.getItemCacheFile(this);
|
|
if (!file) {
|
|
if (cacheFile.exists()) {
|
|
var str = yield Zotero.File.getContentsAsync(cacheFile);
|
|
|
|
return str.trim();
|
|
}
|
|
return '';
|
|
}
|
|
|
|
var contentType = this.attachmentContentType;
|
|
if (!contentType) {
|
|
contentType = yield Zotero.MIME.getMIMETypeFromFile(file);
|
|
if (contentType) {
|
|
this.attachmentContentType = contentType;
|
|
yield this.save();
|
|
}
|
|
}
|
|
|
|
var str;
|
|
if (Zotero.Fulltext.isCachedMIMEType(contentType)) {
|
|
var reindex = false;
|
|
|
|
if (!cacheFile.exists()) {
|
|
Zotero.debug("Regenerating item " + this.id + " full-text cache file");
|
|
reindex = true;
|
|
}
|
|
// Fully index item if it's not yet
|
|
else if (!(yield Zotero.Fulltext.isFullyIndexed(this))) {
|
|
Zotero.debug("Item " + this.id + " is not fully indexed -- caching now");
|
|
reindex = true;
|
|
}
|
|
|
|
if (reindex) {
|
|
if (!Zotero.Fulltext.pdfConverterIsRegistered()) {
|
|
Zotero.debug("PDF converter is unavailable -- returning empty .attachmentText", 3);
|
|
return '';
|
|
}
|
|
yield Zotero.Fulltext.indexItems(this.id, false);
|
|
}
|
|
|
|
if (!cacheFile.exists()) {
|
|
Zotero.debug("Cache file doesn't exist after indexing -- returning empty .attachmentText");
|
|
return '';
|
|
}
|
|
str = yield Zotero.File.getContentsAsync(cacheFile);
|
|
}
|
|
|
|
else if (contentType == 'text/html') {
|
|
str = yield Zotero.File.getContentsAsync(file);
|
|
str = Zotero.Utilities.unescapeHTML(str);
|
|
}
|
|
|
|
else if (contentType == 'text/plain') {
|
|
str = yield Zotero.File.getContentsAsync(file);
|
|
}
|
|
|
|
else {
|
|
return '';
|
|
}
|
|
|
|
return str.trim();
|
|
})
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
* Returns child attachments of this item
|
|
*
|
|
* @param {Boolean} includeTrashed Include trashed child items
|
|
* @return {Integer[]} Array of itemIDs
|
|
*/
|
|
Zotero.Item.prototype.getAttachments = function(includeTrashed) {
|
|
if (this.isAttachment()) {
|
|
throw new Error("getAttachments() cannot be called on attachment items");
|
|
}
|
|
|
|
this._requireData('childItems');
|
|
|
|
if (!this._attachments) {
|
|
return [];
|
|
}
|
|
|
|
var cacheKey = (Zotero.Prefs.get('sortAttachmentsChronologically') ? 'chronological' : 'alphabetical')
|
|
+ 'With' + (includeTrashed ? '' : 'out') + 'Trashed';
|
|
|
|
if (this._attachments[cacheKey]) {
|
|
return this._attachments[cacheKey];
|
|
}
|
|
|
|
var rows = this._attachments.rows.concat();
|
|
// Remove trashed items if necessary
|
|
if (!includeTrashed) {
|
|
rows = rows.filter(function (row) !row.trashed);
|
|
}
|
|
// Sort by title if necessary
|
|
if (!Zotero.Prefs.get('sortAttachmentsChronologically')) {
|
|
var collation = Zotero.getLocaleCollation();
|
|
rows.sort(function (a, b) collation.compareString(1, a.title, b.title));
|
|
}
|
|
var ids = rows.map(row => row.itemID);
|
|
this._attachments[cacheKey] = ids;
|
|
return ids;
|
|
}
|
|
|
|
|
|
/**
|
|
* Looks for attachment in the following order: oldest PDF attachment matching parent URL,
|
|
* oldest non-PDF attachment matching parent URL, oldest PDF attachment not matching URL,
|
|
* old non-PDF attachment not matching URL
|
|
*
|
|
* @return {Promise<Zotero.Item|FALSE>} - A promise for attachment item or FALSE if none
|
|
*/
|
|
Zotero.Item.prototype.getBestAttachment = Zotero.Promise.coroutine(function* () {
|
|
if (!this.isRegularItem()) {
|
|
throw ("getBestAttachment() can only be called on regular items");
|
|
}
|
|
var attachments = yield this.getBestAttachments();
|
|
return attachments ? attachments[0] : false;
|
|
});
|
|
|
|
|
|
/**
|
|
* Looks for attachment in the following order: oldest PDF attachment matching parent URL,
|
|
* oldest PDF attachment not matching parent URL, oldest non-PDF attachment matching parent URL,
|
|
* old non-PDF attachment not matching parent URL
|
|
*
|
|
* @return {Promise<Zotero.Item[]>} - A promise for an array of Zotero items
|
|
*/
|
|
Zotero.Item.prototype.getBestAttachments = Zotero.Promise.coroutine(function* () {
|
|
if (!this.isRegularItem()) {
|
|
throw new Error("getBestAttachments() can only be called on regular items");
|
|
}
|
|
|
|
var url = this.getField('url');
|
|
|
|
var sql = "SELECT IA.itemID FROM itemAttachments IA NATURAL JOIN items I "
|
|
+ "LEFT JOIN itemData ID ON (IA.itemID=ID.itemID AND fieldID=1) "
|
|
+ "LEFT JOIN itemDataValues IDV ON (ID.valueID=IDV.valueID) "
|
|
+ "WHERE parentItemID=? AND linkMode NOT IN (?) "
|
|
+ "AND IA.itemID NOT IN (SELECT itemID FROM deletedItems) "
|
|
+ "ORDER BY contentType='application/pdf' DESC, value=? DESC, dateAdded ASC";
|
|
var itemIDs = yield Zotero.DB.columnQueryAsync(sql, [this.id, Zotero.Attachments.LINK_MODE_LINKED_URL, url]);
|
|
return this.ObjectsClass.get(itemIDs);
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
* Return state of best attachment
|
|
*
|
|
* @return {Promise<Integer>} Promise for 0 (none), 1 (present), -1 (missing)
|
|
*/
|
|
Zotero.Item.prototype.getBestAttachmentState = Zotero.Promise.coroutine(function* () {
|
|
if (this._bestAttachmentState !== null) {
|
|
return this._bestAttachmentState;
|
|
}
|
|
var item = yield this.getBestAttachment();
|
|
if (item) {
|
|
let exists = yield item.fileExists();
|
|
return this._bestAttachmentState = exists ? 1 : -1;
|
|
}
|
|
return this._bestAttachmentState = 0;
|
|
});
|
|
|
|
|
|
/**
|
|
* Return cached state of best attachment for use in items view
|
|
*
|
|
* @return {Integer|null} 0 (none), 1 (present), -1 (missing), null (unavailable)
|
|
*/
|
|
Zotero.Item.prototype.getBestAttachmentStateCached = function () {
|
|
return this._bestAttachmentState;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.clearBestAttachmentState = function () {
|
|
this._bestAttachmentState = null;
|
|
}
|
|
|
|
|
|
//
|
|
// Methods dealing with item tags
|
|
//
|
|
//
|
|
/**
|
|
* Returns all tags assigned to an item
|
|
*
|
|
* @return {Array} Array of tag data in API JSON format
|
|
*/
|
|
Zotero.Item.prototype.getTags = function () {
|
|
this._requireData('tags');
|
|
// BETTER DEEP COPY?
|
|
return JSON.parse(JSON.stringify(this._tags));
|
|
};
|
|
|
|
|
|
/**
|
|
* Check if the item has a given tag
|
|
*
|
|
* @param {String}
|
|
* @return {Boolean}
|
|
*/
|
|
Zotero.Item.prototype.hasTag = function (tagName) {
|
|
this._requireData('tags');
|
|
return this._tags.some(function (tagData) tagData.tag == tagName);
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the assigned type for a given tag of the item
|
|
*/
|
|
Zotero.Item.prototype.getTagType = function (tagName) {
|
|
this._requireData('tags');
|
|
for (let i=0; i<this._tags.length; i++) {
|
|
let tag = this._tags[i];
|
|
if (tag.tag === tagName) {
|
|
return tag.type ? tag.type : 0;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Set the item's tags
|
|
*
|
|
* A separate save() is required to update the database.
|
|
*
|
|
* @param {Array} tags Tag data in API JSON format (e.g., [{tag: 'tag', type: 1}])
|
|
*/
|
|
Zotero.Item.prototype.setTags = function (tags) {
|
|
var oldTags = this.getTags();
|
|
var newTags = tags.concat();
|
|
for (let i=0; i<oldTags.length; i++) {
|
|
oldTags[i] = Zotero.Tags.cleanData(oldTags[i]);
|
|
}
|
|
for (let i=0; i<newTags.length; i++) {
|
|
newTags[i] = Zotero.Tags.cleanData(newTags[i]);
|
|
}
|
|
|
|
// Sort to allow comparison with JSON, which maybe we'll stop doing if it's too slow
|
|
var sorter = function (a, b) {
|
|
if (a.type < b.type) return -1;
|
|
if (a.type > b.type) return 1;
|
|
return a.tag.localeCompare(b.tag);
|
|
};
|
|
oldTags.sort(sorter);
|
|
newTags.sort(sorter);
|
|
|
|
if (JSON.stringify(oldTags) == JSON.stringify(newTags)) {
|
|
Zotero.debug("Tags haven't changed", 4);
|
|
return;
|
|
}
|
|
|
|
this._markFieldChange('tags', this._tags);
|
|
this._changed.tags = true;
|
|
this._tags = newTags;
|
|
}
|
|
|
|
|
|
/**
|
|
* Add a single tag to the item. If type is 1 and an automatic tag with the same name already
|
|
* exists, replace it with a manual one.
|
|
*
|
|
* A separate save() is required to update the database.
|
|
*
|
|
* @param {String} name
|
|
* @param {Number} [type=0]
|
|
*/
|
|
Zotero.Item.prototype.addTag = function (name, type) {
|
|
type = type ? parseInt(type) : 0;
|
|
|
|
var changed = false;
|
|
var tags = this.getTags();
|
|
for (let i=0; i<tags.length; i++) {
|
|
let tag = tags[i];
|
|
if (tag.tag === name) {
|
|
if (tag.type == type) {
|
|
Zotero.debug("Tag '" + name + "' already exists on item " + this.libraryKey);
|
|
return false;
|
|
}
|
|
tag.type = type;
|
|
changed = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!changed) {
|
|
tags.push({
|
|
tag: name,
|
|
type: type
|
|
});
|
|
}
|
|
this.setTags(tags);
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Replace an existing tag with a new manual tag
|
|
*
|
|
* A separate save() is required to update the database.
|
|
*
|
|
* @param {String} oldTag
|
|
* @param {String} newTag
|
|
*/
|
|
Zotero.Item.prototype.replaceTag = function (oldTag, newTag) {
|
|
var tags = this.getTags();
|
|
newTag = newTag.trim();
|
|
|
|
Zotero.debug("REPLACING TAG " + oldTag + " " + newTag);
|
|
|
|
if (newTag === "") {
|
|
Zotero.debug('Not replacing with empty tag', 2);
|
|
return false;
|
|
}
|
|
|
|
var changed = false;
|
|
for (let i=0; i<tags.length; i++) {
|
|
let tag = tags[i];
|
|
if (tag.tag === oldTag) {
|
|
tag.tag = newTag;
|
|
tag.type = 0;
|
|
changed = true;
|
|
}
|
|
}
|
|
if (!changed) {
|
|
Zotero.debug("Tag '" + oldTag + "' not found on item -- not replacing", 2);
|
|
return false;
|
|
}
|
|
this.setTags(tags);
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Remove a tag from the item
|
|
*
|
|
* A separate save() is required to update the database.
|
|
*/
|
|
Zotero.Item.prototype.removeTag = function(tagName) {
|
|
this._requireData('tags');
|
|
var newTags = this._tags.filter(function (tagData) tagData.tag !== tagName);
|
|
if (newTags.length == this._tags.length) {
|
|
Zotero.debug('Cannot remove missing tag ' + tagName + ' from item ' + this.libraryKey);
|
|
return;
|
|
}
|
|
this.setTags(newTags);
|
|
}
|
|
|
|
|
|
/**
|
|
* Remove all tags from the item
|
|
*
|
|
* A separate save() is required to update the database.
|
|
*/
|
|
Zotero.Item.prototype.removeAllTags = function() {
|
|
this._requireData('tags');
|
|
this.setTags([]);
|
|
}
|
|
|
|
|
|
//
|
|
// Methods dealing with collections
|
|
//
|
|
/**
|
|
* Gets the collections the item is in
|
|
*
|
|
* @return {Array<Integer>} An array of collectionIDs for all collections the item belongs to
|
|
*/
|
|
Zotero.Item.prototype.getCollections = function () {
|
|
this._requireData('collections');
|
|
return this._collections.concat();
|
|
};
|
|
|
|
|
|
/**
|
|
* Sets the collections the item is in
|
|
*
|
|
* A separate save() (with options.skipDateModifiedUpdate, possibly) is required to save changes.
|
|
*
|
|
* @param {Array<String|Integer>} collectionIDsOrKeys Collection ids or keys
|
|
*/
|
|
Zotero.Item.prototype.setCollections = function (collectionIDsOrKeys) {
|
|
if (!this.libraryID) {
|
|
this.libraryID = Zotero.Libraries.userLibraryID;
|
|
}
|
|
|
|
this._requireData('collections');
|
|
|
|
if (!collectionIDsOrKeys) {
|
|
collectionIDsOrKeys = [];
|
|
}
|
|
|
|
// Convert any keys to ids
|
|
var collectionIDs = collectionIDsOrKeys.map(function (val) {
|
|
if (parseInt(val) == val) {
|
|
return parseInt(val);
|
|
}
|
|
var id = this.ContainerObjectsClass.getIDFromLibraryAndKey(this.libraryID, val);
|
|
if (!id) {
|
|
let e = new Error("Collection " + val + " not found for item " + this.libraryKey);
|
|
e.name = "ZoteroObjectNotFoundError";
|
|
throw e;
|
|
}
|
|
return id;
|
|
}.bind(this));
|
|
collectionIDs = Zotero.Utilities.arrayUnique(collectionIDs);
|
|
|
|
if (Zotero.Utilities.arrayEquals(this._collections, collectionIDs)) {
|
|
Zotero.debug("Collections have not changed for item " + this.id);
|
|
return;
|
|
}
|
|
|
|
this._markFieldChange("collections", this._collections);
|
|
this._collections = collectionIDs;
|
|
this._changed.collections = true;
|
|
};
|
|
|
|
|
|
/**
|
|
* Add this item to a collection
|
|
*
|
|
* A separate save() (with options.skipDateModifiedUpdate, possibly) is required to save changes.
|
|
*
|
|
* @param {Number} collectionID
|
|
*/
|
|
Zotero.Item.prototype.addToCollection = function (collectionIDOrKey) {
|
|
if (!this.libraryID) {
|
|
this.libraryID = Zotero.Libraries.userLibraryID;
|
|
}
|
|
|
|
var collectionID = parseInt(collectionIDOrKey) == collectionIDOrKey
|
|
? parseInt(collectionIDOrKey)
|
|
: this.ContainerObjectsClass.getIDFromLibraryAndKey(this.libraryID, collectionIDOrKey)
|
|
|
|
if (!collectionID) {
|
|
throw new Error("Invalid collection '" + collectionIDOrKey + "'");
|
|
}
|
|
|
|
this._requireData('collections');
|
|
if (this._collections.indexOf(collectionID) != -1) {
|
|
Zotero.debug("Item is already in collection " + collectionID);
|
|
return;
|
|
}
|
|
this.setCollections(this._collections.concat(collectionID));
|
|
};
|
|
|
|
|
|
/**
|
|
* Remove this item from a collection
|
|
*
|
|
* A separate save() (with options.skipDateModifiedUpdate, possibly) is required to save changes.
|
|
*
|
|
* @param {Number} collectionID
|
|
*/
|
|
Zotero.Item.prototype.removeFromCollection = function (collectionIDOrKey) {
|
|
if (!this.libraryID) {
|
|
this.libraryID = Zotero.Libraries.userLibraryID;
|
|
}
|
|
|
|
var collectionID = parseInt(collectionIDOrKey) == collectionIDOrKey
|
|
? parseInt(collectionIDOrKey)
|
|
: this.ContainerObjectsClass.getIDFromLibraryAndKey(this.libraryID, collectionIDOrKey)
|
|
|
|
if (!collectionID) {
|
|
throw new Error("Invalid collection '" + collectionIDOrKey + "'");
|
|
}
|
|
|
|
Zotero.debug("REMOVING FROM COLLECTION");
|
|
Zotero.debug(this._collections);
|
|
|
|
this._requireData('collections');
|
|
var pos = this._collections.indexOf(collectionID);
|
|
if (pos == -1) {
|
|
Zotero.debug("Item is not in collection " + collectionID);
|
|
return;
|
|
}
|
|
this.setCollections(this._collections.slice(0, pos).concat(this._collections.slice(pos + 1)));
|
|
};
|
|
|
|
|
|
/**
|
|
* Determine whether the item belongs to a given collectionID
|
|
**/
|
|
Zotero.Item.prototype.inCollection = function (collectionID) {
|
|
this._requireData('collections');
|
|
return this._collections.indexOf(collectionID) != -1;
|
|
};
|
|
|
|
|
|
Zotero.Item.prototype.getImageSrc = function() {
|
|
var itemType = Zotero.ItemTypes.getName(this.itemTypeID);
|
|
if (itemType == 'attachment') {
|
|
var linkMode = this.attachmentLinkMode;
|
|
|
|
// Quick hack to use PDF icon for imported files and URLs --
|
|
// extend to support other document types later
|
|
if ((linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE ||
|
|
linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL) &&
|
|
this.attachmentContentType == 'application/pdf') {
|
|
itemType += '-pdf';
|
|
}
|
|
else if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE) {
|
|
itemType += "-file";
|
|
}
|
|
else if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
|
|
itemType += "-link";
|
|
}
|
|
else if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL) {
|
|
itemType += "-snapshot";
|
|
}
|
|
else if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
|
itemType += "-web-link";
|
|
}
|
|
}
|
|
|
|
return Zotero.ItemTypes.getImageSrc(itemType);
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.getImageSrcWithTags = Zotero.Promise.coroutine(function* () {
|
|
//Zotero.debug("Generating tree image for item " + this.id);
|
|
|
|
var uri = this.getImageSrc();
|
|
|
|
var tags = this.getTags();
|
|
if (!tags.length) {
|
|
return uri;
|
|
}
|
|
|
|
var tagColors = Zotero.Tags.getColors(this.libraryID);
|
|
var colorData = [];
|
|
for (let i=0; i<tags.length; i++) {
|
|
let tag = tags[i];
|
|
let data = tagColors.get(tag.tag);
|
|
if (data) {
|
|
colorData.push(data);
|
|
}
|
|
}
|
|
if (!colorData.length) {
|
|
return uri;
|
|
}
|
|
colorData.sort(function (a, b) {
|
|
return a.position - b.position;
|
|
});
|
|
var colors = colorData.map(function (val) val.color);
|
|
return Zotero.Tags.generateItemsListImage(colors, uri);
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
* Compares this item to another
|
|
*
|
|
* Returns a two-element array containing two objects with the differing values,
|
|
* or FALSE if no differences
|
|
*
|
|
* @param {Zotero.Item} item Zotero.Item to compare this item to
|
|
* @param {Boolean} includeMatches Include all fields, even those that aren't different
|
|
* @param {Boolean} ignoreFields If no fields other than those specified
|
|
* are different, just return false --
|
|
* only works for primary fields
|
|
*/
|
|
Zotero.Item.prototype.diff = function (item, includeMatches, ignoreFields) {
|
|
var diff = [];
|
|
|
|
if (!ignoreFields) {
|
|
ignoreFields = [];
|
|
}
|
|
|
|
var thisData = this.serialize();
|
|
var otherData = item.serialize();
|
|
|
|
var numDiffs = this.ObjectsClass.diff(thisData, otherData, diff, includeMatches);
|
|
|
|
diff[0].creators = [];
|
|
diff[1].creators = [];
|
|
// TODO: creators?
|
|
// TODO: tags?
|
|
// TODO: related?
|
|
// TODO: annotations
|
|
|
|
var changed = false;
|
|
|
|
changed = thisData.parentKey != otherData.parentKey;
|
|
if (includeMatches || changed) {
|
|
diff[0].parentKey = thisData.parentKey;
|
|
diff[1].parentKey = otherData.parentKey;
|
|
|
|
if (changed) {
|
|
numDiffs++;
|
|
}
|
|
}
|
|
|
|
if (thisData.attachment) {
|
|
for (var field in thisData.attachment) {
|
|
changed = thisData.attachment[field] != otherData.attachment[field];
|
|
if (includeMatches || changed) {
|
|
if (!diff[0].attachment) {
|
|
diff[0].attachment = {};
|
|
diff[1].attachment = {};
|
|
}
|
|
diff[0].attachment[field] = thisData.attachment[field];
|
|
diff[1].attachment[field] = otherData.attachment[field];
|
|
}
|
|
|
|
if (changed) {
|
|
numDiffs++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (thisData.note != undefined) {
|
|
// Whitespace and entity normalization
|
|
//
|
|
// Ideally this would all be fixed elsewhere so we didn't have to
|
|
// convert on every sync diff
|
|
//
|
|
// TEMP: Using a try/catch to avoid unexpected errors in 2.1 releases
|
|
try {
|
|
var thisNote = thisData.note;
|
|
var otherNote = otherData.note;
|
|
|
|
// Stop non-Unix newlines from triggering erroneous conflicts
|
|
thisNote = thisNote.replace(/\r\n?/g, "\n");
|
|
otherNote = otherNote.replace(/\r\n?/g, "\n");
|
|
|
|
// Normalize multiple spaces (due to differences TinyMCE, Z.U.text2html(),
|
|
// and the server)
|
|
var re = /( | |\u00a0 |\u00a0\u00a0)/g;
|
|
thisNote = thisNote.replace(re, " ");
|
|
otherNote = otherNote.replace(re, " ");
|
|
|
|
// Normalize new paragraphs
|
|
var re = /<p>( |\u00a0)<\/p>/g;
|
|
thisNote = thisNote.replace(re, "<p> </p>");
|
|
otherNote = otherNote.replace(re, "<p> </p>");
|
|
|
|
// Unencode XML entities
|
|
thisNote = thisNote.replace(/&/g, "&");
|
|
otherNote = otherNote.replace(/&/g, "&");
|
|
thisNote = thisNote.replace(/'/g, "'");
|
|
otherNote = otherNote.replace(/'/g, "'");
|
|
thisNote = thisNote.replace(/"/g, '"');
|
|
otherNote = otherNote.replace(/"/g, '"');
|
|
thisNote = thisNote.replace(/</g, "<");
|
|
otherNote = otherNote.replace(/</g, "<");
|
|
thisNote = thisNote.replace(/>/g, ">");
|
|
otherNote = otherNote.replace(/>/g, ">");
|
|
|
|
changed = thisNote != otherNote;
|
|
}
|
|
catch (e) {
|
|
Zotero.debug(e);
|
|
Components.utils.reportError(e);
|
|
changed = thisNote != otherNote;
|
|
}
|
|
|
|
if (includeMatches || changed) {
|
|
diff[0].note = thisNote;
|
|
diff[1].note = otherNote;
|
|
}
|
|
|
|
if (changed) {
|
|
numDiffs++;
|
|
}
|
|
}
|
|
|
|
//Zotero.debug(thisData);
|
|
//Zotero.debug(otherData);
|
|
//Zotero.debug(diff);
|
|
|
|
if (numDiffs == 0) {
|
|
return false;
|
|
}
|
|
if (ignoreFields.length && diff[0].primary) {
|
|
if (includeMatches) {
|
|
throw ("ignoreFields cannot be used if includeMatches is set");
|
|
}
|
|
var realDiffs = numDiffs;
|
|
for (let field of ignoreFields) {
|
|
if (diff[0].primary[field] != undefined) {
|
|
realDiffs--;
|
|
if (realDiffs == 0) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return diff;
|
|
}
|
|
|
|
|
|
/**
|
|
* Compare multiple items against this item and return fields that differ
|
|
*
|
|
* Currently compares only item data, not primary fields
|
|
*/
|
|
Zotero.Item.prototype.multiDiff = function (otherItems, ignoreFields) {
|
|
var thisData = this.toJSON();
|
|
|
|
var alternatives = {};
|
|
var hasDiffs = false;
|
|
|
|
for (let i = 0; i < otherItems.length; i++) {
|
|
let otherData = otherItems[i].toJSON();
|
|
let changeset = Zotero.DataObjectUtilities.diff(thisData, otherData, ignoreFields);
|
|
|
|
for (let i = 0; i < changeset.length; i++) {
|
|
let change = changeset[i];
|
|
|
|
if (change.op == 'delete') {
|
|
continue;
|
|
}
|
|
|
|
if (!alternatives[change.field]) {
|
|
hasDiffs = true;
|
|
alternatives[change.field] = [change.value];
|
|
}
|
|
else if (alternatives[change.field].indexOf(change.value) == -1) {
|
|
hasDiffs = true;
|
|
alternatives[change.field].push(change.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasDiffs) {
|
|
return false;
|
|
}
|
|
|
|
return alternatives;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns an unsaved copy of the item without itemID and key
|
|
*
|
|
* This is used to duplicate items and copy them between libraries.
|
|
*
|
|
* @param {Number} [libraryID] - libraryID of the new item, or the same as original if omitted
|
|
* @param {Boolean} [options.skipTags=false] - Skip tags
|
|
* @param {Boolean} [options.includeCollections=false] - Add new item to all collections
|
|
* @return {Promise<Zotero.Item>}
|
|
*/
|
|
Zotero.Item.prototype.clone = function (libraryID, options = {}) {
|
|
Zotero.debug('Cloning item ' + this.id);
|
|
|
|
if (libraryID !== undefined && libraryID !== null && typeof libraryID !== 'number') {
|
|
throw new Error("libraryID must be null or an integer");
|
|
}
|
|
|
|
if (libraryID === undefined || libraryID === null) {
|
|
libraryID = this.libraryID;
|
|
}
|
|
var sameLibrary = libraryID == this.libraryID;
|
|
|
|
var newItem = new Zotero.Item;
|
|
newItem.libraryID = libraryID;
|
|
newItem.setType(this.itemTypeID);
|
|
|
|
var fieldIDs = this.getUsedFields();
|
|
for (let i = 0; i < fieldIDs.length; i++) {
|
|
let fieldID = fieldIDs[i];
|
|
newItem.setField(fieldID, this.getField(fieldID));
|
|
}
|
|
|
|
// Regular item
|
|
if (this.isRegularItem()) {
|
|
newItem.setCreators(this.getCreators());
|
|
}
|
|
else {
|
|
newItem.setNote(this.getNote());
|
|
if (sameLibrary) {
|
|
var parent = this.parentKey;
|
|
if (parent) {
|
|
newItem.parentKey = parent;
|
|
}
|
|
}
|
|
|
|
if (this.isAttachment()) {
|
|
newItem.attachmentLinkMode = this.attachmentLinkMode;
|
|
newItem.attachmentContentType = this.attachmentContentType;
|
|
newItem.attachmentCharset = this.attachmentCharset;
|
|
if (sameLibrary) {
|
|
if (this.attachmentPath) {
|
|
newItem.attachmentPath = this.attachmentPath;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!options.skipTags) {
|
|
newItem.setTags(this.getTags());
|
|
}
|
|
|
|
if (options.includeCollections) {
|
|
if (!sameLibrary) {
|
|
throw new Error("Can't include collections when cloning to different library");
|
|
}
|
|
newItem.setCollections(this.getCollections());
|
|
}
|
|
|
|
if (sameLibrary) {
|
|
// DEBUG: this will add reverse-only relateds too
|
|
newItem.setRelations(this.getRelations());
|
|
}
|
|
|
|
return newItem;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
|
|
Zotero.DB.requireTransaction();
|
|
|
|
// Remove item from parent collections
|
|
var parentCollectionIDs = this.collections;
|
|
if (parentCollectionIDs) {
|
|
for (var i=0; i<parentCollectionIDs.length; i++) {
|
|
let parentCollection = yield Zotero.Collections.getAsync(parentCollectionIDs[i]);
|
|
yield parentCollection.removeItem(this.id);
|
|
}
|
|
}
|
|
|
|
var parentItem = this.parentKey;
|
|
parentItem = parentItem
|
|
? (yield this.ObjectsClass.getByLibraryAndKeyAsync(this.libraryID, parentItem))
|
|
: null;
|
|
|
|
// // Delete associated attachment files
|
|
if (this.isAttachment()) {
|
|
let linkMode = this.getAttachmentLinkMode();
|
|
// If link only, nothing to delete
|
|
if (linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
|
try {
|
|
let file = Zotero.Attachments.getStorageDirectory(this);
|
|
yield OS.File.removeDir(file.path, {
|
|
ignoreAbsent: true,
|
|
ignorePermissions: true
|
|
});
|
|
}
|
|
catch (e) {
|
|
Zotero.debug(e, 2);
|
|
Components.utils.reportError(e);
|
|
}
|
|
}
|
|
|
|
// Zotero.Sync.EventListeners.ChangeListener needs to know if this was a storage file
|
|
env.notifierData[this.id].storageDeleteLog = this.isImportedAttachment();
|
|
}
|
|
// Regular item
|
|
else {
|
|
let sql = "SELECT itemID FROM itemNotes WHERE parentItemID=?1 UNION "
|
|
+ "SELECT itemID FROM itemAttachments WHERE parentItemID=?1";
|
|
let toDelete = yield Zotero.DB.columnQueryAsync(sql, [this.id]);
|
|
for (let i=0; i<toDelete.length; i++) {
|
|
let obj = yield this.ObjectsClass.getAsync(toDelete[i]);
|
|
// Copy all options other than 'tx', which would cause a deadlock
|
|
let options = {};
|
|
Object.assign(options, env.options);
|
|
delete options.tx;
|
|
yield obj.erase(options);
|
|
}
|
|
}
|
|
|
|
// Flag related items for notification
|
|
// TEMP: Do something with relations
|
|
|
|
// Clear fulltext cache
|
|
if (this.isAttachment()) {
|
|
yield Zotero.Fulltext.clearItemWords(this.id);
|
|
//Zotero.Fulltext.clearItemContent(this.id);
|
|
}
|
|
|
|
yield Zotero.DB.queryAsync('DELETE FROM items WHERE itemID=?', this.id);
|
|
|
|
if (parentItem) {
|
|
yield parentItem.reload(['primaryData', 'childItems'], true);
|
|
parentItem.clearBestAttachmentState();
|
|
}
|
|
|
|
Zotero.Prefs.set('purge.items', true);
|
|
Zotero.Prefs.set('purge.creators', true);
|
|
Zotero.Prefs.set('purge.tags', true);
|
|
});
|
|
|
|
|
|
Zotero.Item.prototype.isCollection = function() {
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Populate the object's data from an API JSON data object
|
|
*/
|
|
Zotero.Item.prototype.fromJSON = function (json) {
|
|
if (!json.itemType && !this._itemTypeID) {
|
|
throw new Error("itemType property not provided");
|
|
}
|
|
|
|
let itemTypeID = Zotero.ItemTypes.getID(json.itemType);
|
|
if (!itemTypeID) {
|
|
let e = new Error(`Invalid item type '${json.itemType}'`);
|
|
e.name = "ZoteroUnknownTypeError";
|
|
throw e;
|
|
}
|
|
this.setType(itemTypeID);
|
|
|
|
var isValidForType = {};
|
|
var setFields = {};
|
|
|
|
// Primary data
|
|
for (let field in json) {
|
|
let val = json[field];
|
|
|
|
switch (field) {
|
|
case 'key':
|
|
case 'version':
|
|
case 'synced':
|
|
case 'itemType':
|
|
case 'note':
|
|
// Use?
|
|
case 'md5':
|
|
case 'mtime':
|
|
break;
|
|
|
|
case 'accessDate':
|
|
case 'dateAdded':
|
|
case 'dateModified':
|
|
if (val) {
|
|
let d = Zotero.Date.isoToDate(val);
|
|
if (!d) {
|
|
Zotero.logError("Discarding invalid " + field + " '" + val
|
|
+ "' for item " + this.libraryKey);
|
|
continue;
|
|
}
|
|
val = Zotero.Date.dateToSQL(d, true);
|
|
}
|
|
if (field == 'accessDate') {
|
|
this.setField(field, val);
|
|
setFields[field] = true;
|
|
}
|
|
else {
|
|
this[field] = val;
|
|
}
|
|
break;
|
|
|
|
case 'parentItem':
|
|
this.parentKey = val;
|
|
break;
|
|
|
|
case 'deleted':
|
|
this.deleted = !!val;
|
|
break;
|
|
|
|
case 'creators':
|
|
this.setCreators(json.creators);
|
|
break;
|
|
|
|
case 'tags':
|
|
this.setTags(json.tags);
|
|
break;
|
|
|
|
case 'collections':
|
|
this.setCollections(json.collections);
|
|
break;
|
|
|
|
case 'relations':
|
|
this.setRelations(json.relations);
|
|
break;
|
|
|
|
//
|
|
// Attachment metadata
|
|
//
|
|
case 'linkMode':
|
|
this.attachmentLinkMode = Zotero.Attachments["LINK_MODE_" + val.toUpperCase()];
|
|
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);
|
|
}
|
|
else {
|
|
this.attachmentFilename = val;
|
|
}
|
|
break;
|
|
|
|
case 'path':
|
|
this.attachmentPath = val;
|
|
break;
|
|
|
|
// Item fields
|
|
default:
|
|
let fieldID = Zotero.ItemFields.getID(field);
|
|
if (!fieldID) {
|
|
Zotero.logError("Discarding unknown JSON field '" + field + "' for item "
|
|
+ this.libraryKey);
|
|
continue;
|
|
}
|
|
// Convert to base-mapped field if necessary, so that setFields has the base-mapped field
|
|
// when it's checked for values from getUsedFields() below
|
|
let origFieldID = fieldID;
|
|
let origField = field;
|
|
fieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(itemTypeID, fieldID) || fieldID;
|
|
if (origFieldID != fieldID) {
|
|
field = Zotero.ItemFields.getName(fieldID);
|
|
}
|
|
isValidForType[field] = Zotero.ItemFields.isValidForType(fieldID, this.itemTypeID);
|
|
if (!isValidForType[field]) {
|
|
Zotero.logError("Discarding invalid field '" + origField + "' for type " + itemTypeID
|
|
+ " for item " + this.libraryKey);
|
|
continue;
|
|
}
|
|
this.setField(field, json[origField]);
|
|
setFields[field] = true;
|
|
}
|
|
}
|
|
|
|
// Clear existing fields not specified
|
|
var previousFields = this.getUsedFields(true);
|
|
for (let field of previousFields) {
|
|
if (!setFields[field] && isValidForType[field] !== false) {
|
|
this.setField(field, false);
|
|
}
|
|
}
|
|
|
|
// Both notes and attachments might have parents and notes
|
|
if (this.isNote() || this.isAttachment()) {
|
|
let parentKey = json.parentItem;
|
|
this.parentKey = parentKey ? parentKey : false;
|
|
|
|
let note = json.note;
|
|
this.setNote(note !== undefined ? note : "");
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {Object} options
|
|
*/
|
|
Zotero.Item.prototype.toJSON = function (options = {}) {
|
|
var env = this._preToJSON(options);
|
|
var mode = env.mode;
|
|
|
|
var obj = env.obj = {};
|
|
obj.key = this.key;
|
|
obj.version = this.version;
|
|
obj.itemType = Zotero.ItemTypes.getName(this.itemTypeID);
|
|
|
|
// Fields
|
|
for (let i in this._itemData) {
|
|
let val = this.getField(i) + '';
|
|
if (val !== '' || mode == 'full') {
|
|
obj[Zotero.ItemFields.getName(i)] = val;
|
|
}
|
|
}
|
|
|
|
// Creators
|
|
if (this.isRegularItem()) {
|
|
obj.creators = this.getCreatorsJSON();
|
|
}
|
|
else {
|
|
var parent = this.parentKey;
|
|
if (parent || mode == 'full') {
|
|
obj.parentItem = parent ? parent : false;
|
|
}
|
|
|
|
// Attachment fields
|
|
if (this.isAttachment()) {
|
|
let linkMode = this.attachmentLinkMode;
|
|
obj.linkMode = Zotero.Attachments.linkModeToName(linkMode);
|
|
|
|
obj.contentType = this.attachmentContentType;
|
|
obj.charset = this.attachmentCharset;
|
|
|
|
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
|
|
obj.path = this.attachmentPath;
|
|
}
|
|
else if (linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
|
obj.filename = this.attachmentFilename;
|
|
}
|
|
|
|
if (this.isImportedAttachment() && !options.skipStorageProperties) {
|
|
if (options.syncedStorageProperties) {
|
|
obj.mtime = this.attachmentSyncedModificationTime;
|
|
obj.md5 = this.attachmentSyncedHash;
|
|
}
|
|
else {
|
|
// TEMP
|
|
//obj.mtime = (yield this.attachmentModificationTime) || null;
|
|
//obj.md5 = (yield this.attachmentHash) || null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Notes and embedded attachment notes
|
|
let note = this.getNote();
|
|
if (note !== "" || mode == 'full' || (mode == 'new' && this.isNote())) {
|
|
obj.note = note;
|
|
}
|
|
}
|
|
|
|
// Tags
|
|
obj.tags = [];
|
|
var tags = this.getTags();
|
|
for (let i=0; i<tags.length; i++) {
|
|
obj.tags.push(tags[i]);
|
|
}
|
|
|
|
// Collections
|
|
if (this.isTopLevelItem()) {
|
|
obj.collections = this.getCollections().map(function (id) {
|
|
var { libraryID, key } = this.ContainerObjectsClass.getLibraryAndKeyFromID(id);
|
|
if (!key) {
|
|
throw new Error("Item collection " + id + " not found");
|
|
}
|
|
return key;
|
|
}.bind(this));
|
|
}
|
|
|
|
// Relations
|
|
obj.relations = this.getRelations()
|
|
|
|
// Deleted
|
|
let deleted = this.deleted;
|
|
if (deleted || mode == 'full') {
|
|
obj.deleted = deleted ? 1 : 0;
|
|
}
|
|
|
|
if (obj.accessDate) obj.accessDate = Zotero.Date.sqlToISO8601(obj.accessDate);
|
|
|
|
if (this.dateAdded) {
|
|
obj.dateAdded = Zotero.Date.sqlToISO8601(this.dateAdded);
|
|
}
|
|
if (this.dateModified) {
|
|
obj.dateModified = Zotero.Date.sqlToISO8601(this.dateModified);
|
|
}
|
|
|
|
var json = this._postToJSON(env);
|
|
if (options.skipStorageProperties) {
|
|
delete json.md5;
|
|
delete json.mtime;
|
|
}
|
|
return json;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.toResponseJSON = function (options = {}) {
|
|
// Default to showing synced storage properties, since that's what the API does, and this function
|
|
// is generally used to emulate the API
|
|
if (options.syncedStorageProperties === undefined) {
|
|
options.syncedStorageProperties = true;
|
|
}
|
|
|
|
var json = this.constructor._super.prototype.toResponseJSON.call(this, options);
|
|
|
|
// creatorSummary
|
|
var firstCreator = this.getField('firstCreator');
|
|
if (firstCreator) {
|
|
json.meta.creatorSummary = firstCreator;
|
|
}
|
|
// parsedDate
|
|
var parsedDate = Zotero.Date.multipartToSQL(this.getField('date', true, true));
|
|
if (parsedDate) {
|
|
// 0000?
|
|
json.meta.parsedDate = parsedDate;
|
|
}
|
|
// numChildren
|
|
if (this.isRegularItem()) {
|
|
json.meta.numChildren = this.numChildren();
|
|
}
|
|
return json;
|
|
};
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Asynchronous load methods
|
|
//
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
/**
|
|
* Return an item in the specified library equivalent to this item
|
|
*
|
|
* @return {Promise<Zotero.Item>}
|
|
*/
|
|
Zotero.Item.prototype.getLinkedItem = function (libraryID, bidirectional) {
|
|
return this._getLinkedObject(libraryID, bidirectional);
|
|
};
|
|
|
|
|
|
/**
|
|
* Add a linked-object relation pointing to the given item
|
|
*
|
|
* Does not require a separate save()
|
|
*
|
|
* @return {Promise}
|
|
*/
|
|
Zotero.Item.prototype.addLinkedItem = Zotero.Promise.coroutine(function* (item) {
|
|
return this._addLinkedObject(item);
|
|
});
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Private methods
|
|
//
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Returns related items this item points to
|
|
*
|
|
* @return {String[]} - Keys of related items
|
|
*/
|
|
Zotero.Item.prototype._getRelatedItems = function () {
|
|
this._requireData('relations');
|
|
|
|
var predicate = Zotero.Relations.relatedItemPredicate;
|
|
|
|
var relatedItemURIs = this.getRelationsByPredicate(predicate);
|
|
|
|
// Pull out object values from related-item relations, turn into items, and pull out keys
|
|
var keys = [];
|
|
for (let i=0; i<relatedItemURIs.length; i++) {
|
|
let {libraryID, key} = Zotero.URI.getURIItemLibraryKey(relatedItemURIs[i]);
|
|
if (key) {
|
|
keys.push(key);
|
|
}
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
|
|
/**
|
|
* @return {Object} Return a copy of the creators, with additional 'id' properties
|
|
*/
|
|
Zotero.Item.prototype._getOldCreators = function () {
|
|
var oldCreators = {};
|
|
for (i=0; i<this._creators.length; i++) {
|
|
let old = {};
|
|
for (let field in this._creators[i]) {
|
|
old[field] = this._creators[i][field];
|
|
}
|
|
// Add 'id' property for efficient DB updates
|
|
old.id = this._creatorIDs[i];
|
|
oldCreators[i] = old;
|
|
}
|
|
return oldCreators;
|
|
}
|