4556 lines
113 KiB
JavaScript
4556 lines
113 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 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 General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***** END LICENSE BLOCK *****
|
|
*/
|
|
|
|
|
|
/*
|
|
* Constructor for Item object
|
|
*
|
|
* Generally should be called through Zotero.Items rather than directly
|
|
*/
|
|
Zotero.Item = function(itemTypeOrID) {
|
|
if (arguments[1] || arguments[2]) {
|
|
throw ("Zotero.Item constructor only takes one parameter");
|
|
}
|
|
|
|
this._disabled = false;
|
|
this._init();
|
|
|
|
if (itemTypeOrID) {
|
|
// setType initializes type-specific properties in this._itemData
|
|
this.setType(Zotero.ItemTypes.getID(itemTypeOrID));
|
|
}
|
|
}
|
|
|
|
Zotero.Item.prototype._init = function () {
|
|
// Primary fields
|
|
this._id = null;
|
|
this._libraryID = null
|
|
this._key = null;
|
|
this._itemTypeID = null;
|
|
this._dateAdded = null;
|
|
this._dateModified = null;
|
|
this._firstCreator = null;
|
|
this._numNotes = null;
|
|
this._numAttachments = null;
|
|
|
|
this._creators = [];
|
|
this._itemData = null;
|
|
this._sourceItem = null;
|
|
|
|
this._primaryDataLoaded = false;
|
|
this._creatorsLoaded = false;
|
|
this._itemDataLoaded = false;
|
|
this._relatedItemsLoaded = false;
|
|
|
|
this._changed = false;
|
|
this._changedPrimaryData = false;
|
|
this._changedItemData = false;
|
|
this._changedCreators = false;
|
|
this._changedDeleted = false;
|
|
this._changedNote = false;
|
|
this._changedSource = false;
|
|
this._changedAttachmentData = false;
|
|
|
|
this._previousData = null;
|
|
|
|
this._deleted = null;
|
|
this._noteTitle = null;
|
|
this._noteText = null;
|
|
this._noteAccessTime = null;
|
|
|
|
this._attachmentLinkMode = null;
|
|
this._attachmentMIMEType = null;
|
|
this._attachmentCharset;
|
|
this._attachmentPath = null;
|
|
this._attachmentSyncState;
|
|
|
|
this._relatedItems = false;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.__defineGetter__('objectType', function () { return 'item'; });
|
|
Zotero.Item.prototype.__defineGetter__('id', function () { return this.getField('id'); });
|
|
Zotero.Item.prototype.__defineGetter__('itemID', function () {
|
|
Zotero.debug("Item.itemID is deprecated -- use Item.id");
|
|
return this.id;
|
|
});
|
|
Zotero.Item.prototype.__defineSetter__('id', function (val) { this.setField('id', val); });
|
|
Zotero.Item.prototype.__defineGetter__('libraryID', function () { return this.getField('libraryID'); });
|
|
Zotero.Item.prototype.__defineSetter__('libraryID', function (val) { this.setField('libraryID', val); });
|
|
Zotero.Item.prototype.__defineGetter__('key', function () { return this.getField('key'); });
|
|
Zotero.Item.prototype.__defineSetter__('key', function (val) { this.setField('key', val) });
|
|
Zotero.Item.prototype.__defineGetter__('itemTypeID', function () { return this.getField('itemTypeID'); });
|
|
Zotero.Item.prototype.__defineGetter__('dateAdded', function () { return this.getField('dateAdded'); });
|
|
Zotero.Item.prototype.__defineGetter__('dateModified', function () { return this.getField('dateModified'); });
|
|
Zotero.Item.prototype.__defineGetter__('firstCreator', function () { return this.getField('firstCreator'); });
|
|
|
|
Zotero.Item.prototype.__defineGetter__('relatedItems', function () { var ids = this._getRelatedItems(true); return ids ? ids : []; });
|
|
Zotero.Item.prototype.__defineSetter__('relatedItems', function (arr) { this._setRelatedItems(arr); });
|
|
Zotero.Item.prototype.__defineGetter__('relatedItemsReverse', function () { var ids = this._getRelatedItemsReverse(); return ids ? ids : []; });
|
|
Zotero.Item.prototype.__defineGetter__('relatedItemsBidirectional', function () { var ids = this._getRelatedItemsBidirectional(); return ids ? ids : []; });
|
|
|
|
|
|
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.getField('itemTypeID');
|
|
}
|
|
|
|
Zotero.Item.prototype.isPrimaryField = function (fieldName) {
|
|
Zotero.debug("Zotero.Item.isPrimaryField() is deprecated -- use Zotero.Items.isPrimaryField()");
|
|
return Zotero.Items.isPrimaryField(fieldName);
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Public Zotero.Item methods
|
|
//
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Check if item exists in the database
|
|
*
|
|
* @return bool TRUE if the item exists, FALSE if not
|
|
*/
|
|
Zotero.Item.prototype.exists = function() {
|
|
if (!this.id) {
|
|
throw ('itemID not set in Zotero.Item.exists()');
|
|
}
|
|
|
|
var sql = "SELECT COUNT(*) FROM items WHERE itemID=?";
|
|
return !!Zotero.DB.valueQuery(sql, this.id);
|
|
}
|
|
|
|
|
|
/*
|
|
* Retrieves (and loads from DB, if necessary) an itemData field value
|
|
*
|
|
* Field can be passed as fieldID or fieldName
|
|
*
|
|
* If |unformatted| is true, skip any special processing of DB value
|
|
* (e.g. multipart date field) (default false)
|
|
*
|
|
* If |includeBaseMapped| is true and field is a base field, returns value of
|
|
* type-specific field instead (e.g. 'label' for 'publisher' in 'audioRecording')
|
|
*/
|
|
Zotero.Item.prototype.getField = function(field, unformatted, includeBaseMapped) {
|
|
// We don't allow access after saving to force use of the centrally cached
|
|
// object, but we make an exception for the id
|
|
if (field != 'id') {
|
|
this._disabledCheck();
|
|
}
|
|
|
|
//Zotero.debug('Requesting field ' + field + ' for item ' + this._id, 4);
|
|
|
|
if ((this._id || this._key) && !this._primaryDataLoaded) {
|
|
this.loadPrimaryData(true);
|
|
}
|
|
|
|
if (field == 'id' || Zotero.Items.isPrimaryField(field)) {
|
|
var privField = '_' + field;
|
|
//Zotero.debug('Returning ' + (this[privField] ? this[privField] : '') + ' (typeof ' + typeof this[privField] + ')');
|
|
return this[privField];
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
if (typeof this._itemData[fieldID] == 'undefined') {
|
|
//Zotero.debug("Field '" + field + "' doesn't exist for item type " + this._itemTypeID + " in Item.getField()");
|
|
return '';
|
|
}
|
|
|
|
if (this.id && this._itemData[fieldID] === null && !this._itemDataLoaded) {
|
|
this._loadItemData();
|
|
}
|
|
|
|
var value = this._itemData[fieldID] ? this._itemData[fieldID] : '';
|
|
|
|
if (!unformatted) {
|
|
// Multipart date fields
|
|
if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date')) {
|
|
value = Zotero.Date.multipartToStr(value);
|
|
}
|
|
}
|
|
//Zotero.debug('Returning ' + value);
|
|
return value;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {Boolean} asNames
|
|
* @return {Integer{}|String[]}
|
|
*/
|
|
Zotero.Item.prototype.getUsedFields = function(asNames) {
|
|
if (!this.id) {
|
|
return [];
|
|
}
|
|
var sql = "SELECT fieldID FROM itemData WHERE itemID=?";
|
|
if (asNames) {
|
|
sql = "SELECT fieldName FROM fields WHERE fieldID IN (" + sql + ")";
|
|
}
|
|
var fields = Zotero.DB.columnQuery(sql, this.id);
|
|
if (!fields) {
|
|
return [];
|
|
}
|
|
return fields;
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* Build object from database
|
|
*/
|
|
Zotero.Item.prototype.loadPrimaryData = function(allowFail) {
|
|
var id = this._id;
|
|
var key = this._key;
|
|
var libraryID = this._libraryID;
|
|
|
|
if (!id && !key) {
|
|
throw ('ID or key not set in Zotero.Item.loadPrimaryData()');
|
|
}
|
|
|
|
var columns = [], join = [], where = [];
|
|
for each(var field in Zotero.Items.primaryFields) {
|
|
var colSQL = null, joinSQL = null, whereSQL = null;
|
|
|
|
// If field not already set
|
|
if (field == 'itemID' || this['_' + field] === null) {
|
|
// Parts should be the same as query in Zotero.Items._load, just
|
|
// without itemID clause
|
|
switch (field) {
|
|
case 'itemID':
|
|
case 'itemTypeID':
|
|
case 'dateAdded':
|
|
case 'dateModified':
|
|
case 'key':
|
|
colSQL = 'I.' + field;
|
|
break;
|
|
|
|
case 'firstCreator':
|
|
colSQL = Zotero.Items.getFirstCreatorSQL();
|
|
break;
|
|
|
|
case 'numNotes':
|
|
colSQL = '(SELECT COUNT(*) FROM itemNotes INo '
|
|
+ 'WHERE sourceItemID=I.itemID AND INo.itemID '
|
|
+ 'NOT IN (SELECT itemID FROM deletedItems)) AS numNotes';
|
|
break;
|
|
|
|
case 'numAttachments':
|
|
colSQL = '(SELECT COUNT(*) FROM itemAttachments IA '
|
|
+ 'WHERE sourceItemID=I.itemID AND IA.itemID '
|
|
+ 'NOT IN (SELECT itemID FROM deletedItems)) AS numAttachments';
|
|
break;
|
|
}
|
|
if (colSQL) {
|
|
columns.push(colSQL);
|
|
}
|
|
if (joinSQL) {
|
|
join.push(joinSQL);
|
|
}
|
|
if (whereSQL) {
|
|
where.push(whereSQL);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!columns.length) {
|
|
throw ("No columns to load in Zotero.Item.loadPrimaryData()");
|
|
}
|
|
|
|
var sql = 'SELECT ' + columns.join(', ') + " FROM items I "
|
|
+ (join.length ? join.join(' ') + ' ' : '') + "WHERE ";
|
|
if (id) {
|
|
sql += "itemID=? ";
|
|
var params = id;
|
|
}
|
|
else {
|
|
sql += "key=? ";
|
|
var params = [key];
|
|
if (libraryID) {
|
|
sql += "AND libraryID=? ";
|
|
params.push(libraryID);
|
|
}
|
|
else {
|
|
sql += "AND libraryID IS NULL ";
|
|
}
|
|
}
|
|
sql += (where.length ? ' AND ' + where.join(' AND ') : '');
|
|
var row = Zotero.DB.rowQuery(sql, params);
|
|
|
|
if (!row) {
|
|
if (allowFail) {
|
|
this._primaryDataLoaded = true;
|
|
return false;
|
|
}
|
|
throw ("Item " + (id ? id : libraryID + "/" + key)
|
|
+ " not found in Zotero.Item.loadPrimaryData()");
|
|
}
|
|
|
|
this.loadFromRow(row);
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Populate basic item data from a database row
|
|
*/
|
|
Zotero.Item.prototype.loadFromRow = function(row, reload) {
|
|
if (reload) {
|
|
this._init();
|
|
}
|
|
|
|
// If necessary or reloading, set the type, initialize this._itemData,
|
|
// and reset _itemDataLoaded
|
|
if (reload || (!this._itemTypeID && row.itemTypeID)) {
|
|
this.setType(row.itemTypeID, true);
|
|
}
|
|
|
|
for (var col in row) {
|
|
if (col == 'clientDateModified') {
|
|
continue;
|
|
}
|
|
|
|
// Only accept primary field data through loadFromRow()
|
|
if (Zotero.Items.isPrimaryField(col)) {
|
|
//Zotero.debug("Setting field '" + col + "' to '" + row[col] + "' for item " + this.id);
|
|
switch (col) {
|
|
case 'itemID':
|
|
this._id = row[col];
|
|
break;
|
|
|
|
case 'libraryID':
|
|
this['_' + col] = row[col] ? row[col] : null;
|
|
break;
|
|
|
|
case 'numNotes':
|
|
case 'numAttachments':
|
|
this['_' + col] = row[col] ? parseInt(row[col]) : 0;
|
|
break;
|
|
|
|
default:
|
|
this['_' + col] = row[col] ? row[col] : '';
|
|
}
|
|
}
|
|
else {
|
|
Zotero.debug(col + ' is not a valid primary field');
|
|
}
|
|
}
|
|
|
|
this._primaryDataLoaded = true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Check if any data fields have changed since last save
|
|
*/
|
|
Zotero.Item.prototype.hasChanged = function() {
|
|
return !!(this._changed
|
|
|| this._changedPrimaryData
|
|
|| this._changedItemData
|
|
|| this._changedCreators
|
|
|| this._changedDeleted
|
|
|| this._changedNote
|
|
|| this._changedSource
|
|
|| this._changedAttachmentData);
|
|
}
|
|
|
|
|
|
/*
|
|
* Set or change the item's type
|
|
*/
|
|
Zotero.Item.prototype.setType = function(itemTypeID, loadIn) {
|
|
if (itemTypeID == this._itemTypeID) {
|
|
return true;
|
|
}
|
|
|
|
// If there's an existing type
|
|
if (this._itemTypeID) {
|
|
if (loadIn) {
|
|
throw ('Cannot change type in loadIn mode in Zotero.Item.setType()');
|
|
}
|
|
|
|
if (this.id && !this._itemDataLoaded) {
|
|
this._loadItemData();
|
|
}
|
|
|
|
var copiedFields = [];
|
|
|
|
// 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 (this._itemTypeID == 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]]);
|
|
if (this._itemData[shortTitleFieldID]) {
|
|
this.setField(shortTitleFieldID, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
for each(var oldFieldID in obsoleteFields) {
|
|
// Try to get a base type for this field
|
|
var baseFieldID =
|
|
Zotero.ItemFields.getBaseIDFromTypeAndField(this.itemTypeID, 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._changedItemData) {
|
|
this._changedItemData = {};
|
|
}
|
|
this._changedItemData[oldFieldID] = true;
|
|
*/
|
|
this.setField(oldFieldID, false);
|
|
}
|
|
}
|
|
|
|
// Move title to bookTitle and clear shortTitle when going from book to bookSection
|
|
if (this._itemTypeID == 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]]);
|
|
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)]);
|
|
}
|
|
}
|
|
|
|
// And reset custom creator types to the default
|
|
var creators = this.getCreators();
|
|
if (creators) {
|
|
for (var i in creators) {
|
|
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
|
|
var oldPrimary = Zotero.CreatorTypes.getPrimaryIDForType(this.getType());
|
|
if (oldPrimary == creators[i].creatorTypeID) {
|
|
var newPrimary = Zotero.CreatorTypes.getPrimaryIDForType(itemTypeID);
|
|
}
|
|
var target = newPrimary ? newPrimary : 2;
|
|
|
|
this.setCreator(i, creators[i].ref, target);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this._itemTypeID = itemTypeID;
|
|
|
|
// Initialize this._itemData with type-specific fields
|
|
this._itemData = {};
|
|
var fields = Zotero.ItemFields.getItemTypeFields(itemTypeID);
|
|
for each(var fieldID in fields) {
|
|
this._itemData[fieldID] = null;
|
|
}
|
|
|
|
// DEBUG: clear change item data?
|
|
|
|
if (copiedFields) {
|
|
for each(var f in copiedFields) {
|
|
this.setField(f[0], f[1]);
|
|
}
|
|
}
|
|
|
|
if (loadIn) {
|
|
this._itemDataLoaded = false;
|
|
}
|
|
else {
|
|
if (!this._changedPrimaryData) {
|
|
this._changedPrimaryData = {};
|
|
}
|
|
this._changedPrimaryData['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;
|
|
}
|
|
|
|
|
|
/**
|
|
* Return an array of collectionIDs for all collections the item belongs to
|
|
**/
|
|
Zotero.Item.prototype.getCollections = function() {
|
|
return Zotero.DB.columnQuery("SELECT collectionID FROM collectionItems "
|
|
+ "WHERE itemID=" + this.id);
|
|
}
|
|
|
|
|
|
/**
|
|
* Determine whether the item belongs to a given collectionID
|
|
**/
|
|
Zotero.Item.prototype.inCollection = function(collectionID) {
|
|
return !!parseInt(Zotero.DB.valueQuery("SELECT COUNT(*) "
|
|
+ "FROM collectionItems WHERE collectionID=" + collectionID + " AND "
|
|
+ "itemID=" + this.id));
|
|
}
|
|
|
|
|
|
/*
|
|
* 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) {
|
|
if (typeof value == 'string') {
|
|
value = Zotero.Utilities.prototype.trim(value);
|
|
}
|
|
|
|
this._disabledCheck();
|
|
|
|
//Zotero.debug("Setting field '" + field + "' to '" + value + "' (loadIn: " + (loadIn ? 'true' : 'false') + ")");
|
|
|
|
if (!field) {
|
|
throw ("Field not specified in Item.setField()");
|
|
}
|
|
|
|
// Set id, libraryID, and key without loading data first
|
|
switch (field) {
|
|
case 'id':
|
|
case 'libraryID':
|
|
case 'key':
|
|
if (value == this['_' + field]) {
|
|
return;
|
|
}
|
|
|
|
if (this._primaryDataLoaded) {
|
|
throw ("Cannot set " + field + " after object is already loaded in Zotero.Item.setField()");
|
|
}
|
|
//this._checkValue(field, val);
|
|
this['_' + field] = value;
|
|
return;
|
|
}
|
|
|
|
if (this._id || this._key) {
|
|
if (!this._primaryDataLoaded) {
|
|
this.loadPrimaryData(true);
|
|
}
|
|
}
|
|
else {
|
|
this._primaryDataLoaded = true;
|
|
}
|
|
|
|
// Primary field
|
|
if (Zotero.Items.isPrimaryField(field)) {
|
|
if (loadIn) {
|
|
throw('Cannot set primary field ' + field + ' in loadIn mode in Zotero.Item.setField()');
|
|
}
|
|
|
|
switch (field) {
|
|
case 'itemID':
|
|
case 'firstCreator':
|
|
case 'numNotes':
|
|
case 'numAttachments':
|
|
throw ('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) {
|
|
Zotero.debug("Field '" + field + "' has changed from '" + this['_' + field] + "' to '" + value + "'", 4);
|
|
|
|
// Save a copy of the object before modifying
|
|
if (this.id && this.exists() && !this._previousData) {
|
|
this._previousData = this.serialize();
|
|
}
|
|
if (field == 'itemTypeID') {
|
|
this.setType(value, loadIn);
|
|
}
|
|
else {
|
|
this['_' + field] = value;
|
|
|
|
if (!this._changedPrimaryData) {
|
|
this._changedPrimaryData = {};
|
|
}
|
|
this._changedPrimaryData[field] = true;
|
|
}
|
|
}
|
|
else {
|
|
Zotero.debug("Field '" + field + "' has not changed", 4);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (!this.itemTypeID) {
|
|
throw ('Item type must be set before setting field data');
|
|
}
|
|
|
|
// If existing item, load field data first unless we're already in
|
|
// the middle of a load
|
|
if (this.id) {
|
|
if (!loadIn && !this._itemDataLoaded) {
|
|
this._loadItemData();
|
|
}
|
|
}
|
|
else {
|
|
this._itemDataLoaded = true;
|
|
}
|
|
|
|
var fieldID = Zotero.ItemFields.getID(field);
|
|
|
|
if (!fieldID) {
|
|
throw ('"' + field + '" is not a valid itemData field.');
|
|
}
|
|
|
|
if (loadIn && this.isNote() && field == 110) { // title
|
|
this._noteTitle = value;
|
|
return true;
|
|
}
|
|
|
|
if (!Zotero.ItemFields.isValidForType(fieldID, this.itemTypeID)) {
|
|
var msg = '"' + field + "' is not a valid field for type " + this.itemTypeID;
|
|
|
|
if (loadIn) {
|
|
Zotero.debug(msg + " -- ignoring value '" + value + "'", 2);
|
|
return false;
|
|
}
|
|
else {
|
|
throw (msg);
|
|
}
|
|
}
|
|
|
|
if (!loadIn) {
|
|
// Save date field as multipart date
|
|
if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date') &&
|
|
!Zotero.Date.isMultipart(value)) {
|
|
value = Zotero.Date.strToMultipart(value);
|
|
}
|
|
// Validate access date
|
|
else if (fieldID == Zotero.ItemFields.getID('accessDate')) {
|
|
if (value && (!Zotero.Date.isSQLDate(value) &&
|
|
!Zotero.Date.isSQLDateTime(value) &&
|
|
value != 'CURRENT_TIMESTAMP')) {
|
|
Zotero.debug("Discarding invalid accessDate '" + value
|
|
+ "' in Item.setField()");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// If existing value, make sure it's actually changing
|
|
if ((!this._itemData[fieldID] && !value) ||
|
|
(this._itemData[fieldID] && this._itemData[fieldID]==value)) {
|
|
return false;
|
|
}
|
|
|
|
// Save a copy of the object before modifying
|
|
if (this.id && this.exists() && !this._previousData) {
|
|
this._previousData = this.serialize();
|
|
}
|
|
}
|
|
|
|
this._itemData[fieldID] = value;
|
|
|
|
if (!loadIn) {
|
|
if (!this._changedItemData) {
|
|
this._changedItemData = {};
|
|
}
|
|
this._changedItemData[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]")
|
|
*/
|
|
Zotero.Item.prototype.getDisplayTitle = function (includeAuthorAndDate) {
|
|
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 creators = this.getCreators();
|
|
var authors = [];
|
|
var participants = [];
|
|
if (creators) {
|
|
for each(var creator in creators) {
|
|
if ((itemTypeID == 8 && creator.creatorTypeID == 16) || // 'letter'/'recipient'
|
|
(itemTypeID == 10 && creator.creatorTypeID == 7)) { // 'interview'/'interviewer'
|
|
participants.push(creator);
|
|
}
|
|
else if ((itemTypeID == 8 && creator.creatorTypeID == 1) || // 'letter'/'author'
|
|
(itemTypeID == 10 && creator.creatorTypeID == 6)) { // 'interview'/'interviewee'
|
|
authors.push(creator);
|
|
}
|
|
}
|
|
}
|
|
|
|
var strParts = [];
|
|
|
|
if (includeAuthorAndDate) {
|
|
var names = [];
|
|
for each(author in authors) {
|
|
names.push(author.ref.lastName);
|
|
}
|
|
|
|
// TODO: Use same logic as getFirstCreatorSQL() (including "et al.")
|
|
if (names.length) {
|
|
strParts.push(Zotero.localeJoin(names, ', '));
|
|
}
|
|
}
|
|
|
|
if (participants.length > 0) {
|
|
var names = [];
|
|
for each(participant in participants) {
|
|
names.push(participant.ref.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.getString('itemTypes.' + itemTypeName));
|
|
}
|
|
|
|
if (includeAuthorAndDate) {
|
|
var d = this.getField('date');
|
|
if (d) {
|
|
strParts.push(d);
|
|
}
|
|
}
|
|
|
|
title = '[';
|
|
title += Zotero.localeJoin(strParts, '; ');
|
|
title += ']';
|
|
}
|
|
|
|
return title;
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns the number of creators for this item
|
|
*/
|
|
Zotero.Item.prototype.numCreators = function() {
|
|
if (this.id && !this._creatorsLoaded) {
|
|
this._loadCreators();
|
|
}
|
|
return this._creators.length;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.hasCreatorAt = function(pos) {
|
|
if (this.id && !this._creatorsLoaded) {
|
|
this._loadCreators();
|
|
}
|
|
|
|
return !!this._creators[pos];
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns an array of the creator data at the given position, or false if none
|
|
*
|
|
* Note: Creator data array is returned by reference
|
|
*/
|
|
Zotero.Item.prototype.getCreator = function(pos) {
|
|
if (this.id && !this._creatorsLoaded) {
|
|
this._loadCreators();
|
|
}
|
|
|
|
return this._creators[pos] ? this._creators[pos] : false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Return the position of the given creator, or FALSE if not found
|
|
*/
|
|
Zotero.Item.prototype.getCreatorPosition = function(creatorID) {
|
|
if (this.id && !this._creatorsLoaded) {
|
|
this._loadCreators();
|
|
}
|
|
|
|
for (var pos in this._creators) {
|
|
if (this._creators[pos].creatorID == creatorID) {
|
|
return pos;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns a multidimensional array of creators, or an empty array if none
|
|
*
|
|
* Note: Creator data array is returned by reference
|
|
*/
|
|
Zotero.Item.prototype.getCreators = function() {
|
|
if (this.id && !this._creatorsLoaded) {
|
|
this._loadCreators();
|
|
}
|
|
|
|
return this._creators;
|
|
}
|
|
|
|
|
|
/*
|
|
* Set or update the creator at the specified position
|
|
*
|
|
* |orderIndex|: the position of this creator in the item (from 0)
|
|
* |creatorTypeIDOrName|: id or type name
|
|
*/
|
|
Zotero.Item.prototype.setCreator = function(orderIndex, creator, creatorTypeIDOrName) {
|
|
if (this.id) {
|
|
if (!this._creatorsLoaded) {
|
|
this._loadCreators();
|
|
}
|
|
}
|
|
else {
|
|
this._creatorsLoaded = true;
|
|
}
|
|
|
|
if (!(creator instanceof Zotero.Creator)) {
|
|
throw ('Creator must be a Zotero.Creator object in Zotero.Item.setCreator()');
|
|
}
|
|
|
|
var creatorTypeID = Zotero.CreatorTypes.getID(creatorTypeIDOrName);
|
|
|
|
if (!creatorTypeID) {
|
|
creatorTypeID = 1;
|
|
}
|
|
|
|
// If creator at this position hasn't changed, cancel
|
|
if (this._creators[orderIndex] &&
|
|
this._creators[orderIndex].ref.id == creator.id &&
|
|
this._creators[orderIndex].creatorTypeID == creatorTypeID &&
|
|
!creator.hasChanged()) {
|
|
Zotero.debug("Creator in position " + orderIndex + " hasn't changed", 4);
|
|
return false;
|
|
}
|
|
|
|
this._creators[orderIndex] = {
|
|
ref: creator,
|
|
creatorTypeID: creatorTypeID
|
|
};
|
|
|
|
if (!this._changedCreators) {
|
|
this._changedCreators = {};
|
|
}
|
|
this._changedCreators[orderIndex] = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Remove a creator and shift others down
|
|
*/
|
|
Zotero.Item.prototype.removeCreator = function(orderIndex) {
|
|
if (this.id && !this._creatorsLoaded) {
|
|
this._loadCreators();
|
|
}
|
|
|
|
var creator = this.getCreator(orderIndex);
|
|
if (!creator) {
|
|
throw ('No creator exists at position ' + orderIndex
|
|
+ ' in Zotero.Item.removeCreator()');
|
|
}
|
|
|
|
if (creator.ref.countLinkedItems() == 1) {
|
|
Zotero.Prefs.set('purge.creators', true);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
if (!this._changedCreators) {
|
|
this._changedCreators = {};
|
|
}
|
|
this._changedCreators[i] = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.__defineGetter__('deleted', function () {
|
|
if (this._deleted !== null) {
|
|
return this._deleted;
|
|
}
|
|
|
|
if (!this.id) {
|
|
return false;
|
|
}
|
|
|
|
var sql = "SELECT COUNT(*) FROM deletedItems WHERE itemID=?";
|
|
var deleted = !!Zotero.DB.valueQuery(sql, this.id);
|
|
this._deleted = deleted;
|
|
return deleted;
|
|
});
|
|
|
|
|
|
Zotero.Item.prototype.__defineSetter__('deleted', function (val) {
|
|
var deleted = !!val;
|
|
|
|
if (this.deleted == deleted) {
|
|
Zotero.debug("Deleted state hasn't changed for item " + this.id);
|
|
return;
|
|
}
|
|
|
|
if (!this._changedDeleted) {
|
|
this._changedDeleted = true;
|
|
}
|
|
this._deleted = deleted;
|
|
});
|
|
|
|
|
|
Zotero.Item.prototype.addRelatedItem = function (itemID) {
|
|
var parsedInt = parseInt(itemID);
|
|
if (parsedInt != itemID) {
|
|
throw ("itemID '" + itemID + "' not an integer in Zotero.Item.addRelatedItem()");
|
|
}
|
|
itemID = parsedInt;
|
|
|
|
if (itemID == this.id) {
|
|
Zotero.debug("Can't relate item to itself in Zotero.Item.addRelatedItem()", 2);
|
|
return false;
|
|
}
|
|
|
|
var current = this._getRelatedItems(true);
|
|
if (current && current.indexOf(itemID) != -1) {
|
|
Zotero.debug("Item " + this.id + " already related to item "
|
|
+ itemID + " in Zotero.Item.addItem()");
|
|
return false;
|
|
}
|
|
|
|
var item = Zotero.Items.get(itemID);
|
|
if (!item) {
|
|
throw ("Can't relate item to invalid item " + itemID
|
|
+ " in Zotero.Item.addRelatedItem()");
|
|
}
|
|
/*
|
|
var otherCurrent = item.relatedItems;
|
|
if (otherCurrent.length && otherCurrent.indexOf(this.id) != -1) {
|
|
Zotero.debug("Other item " + itemID + " already related to item "
|
|
+ this.id + " in Zotero.Item.addItem()");
|
|
return false;
|
|
}
|
|
*/
|
|
|
|
this._prepFieldChange('relatedItems');
|
|
this._relatedItems.push(item);
|
|
return true;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.removeRelatedItem = function (itemID) {
|
|
var parsedInt = parseInt(itemID);
|
|
if (parsedInt != itemID) {
|
|
throw ("itemID '" + itemID + "' not an integer in Zotero.Item.removeRelatedItem()");
|
|
}
|
|
itemID = parsedInt;
|
|
|
|
var current = this._getRelatedItems(true);
|
|
if (current) {
|
|
var index = current.indexOf(itemID);
|
|
}
|
|
|
|
if (!current || index == -1) {
|
|
Zotero.debug("Item " + this.id + " isn't related to item "
|
|
+ itemID + " in Zotero.Item.removeRelatedItem()");
|
|
return false;
|
|
}
|
|
|
|
this._prepFieldChange('relatedItems');
|
|
this._relatedItems.splice(index, 1);
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Save changes back to database
|
|
*
|
|
* Returns true on item update or itemID of new item
|
|
*/
|
|
Zotero.Item.prototype.save = function() {
|
|
Zotero.Items.editCheck(this);
|
|
|
|
if (!this.hasChanged()) {
|
|
Zotero.debug('Item ' + this.id + ' has not changed', 4);
|
|
return false;
|
|
}
|
|
|
|
// Make sure there are no gaps in the creator indexes
|
|
var creators = this.getCreators();
|
|
var lastPos = -1;
|
|
for (var pos in creators) {
|
|
if (pos != lastPos + 1) {
|
|
throw ("Creator index " + pos + " out of sequence in Zotero.Item.save()");
|
|
}
|
|
lastPos++;
|
|
}
|
|
|
|
var ZU = new Zotero.Utilities;
|
|
|
|
Zotero.DB.beginTransaction();
|
|
|
|
var isNew = !this.id || !this.exists();
|
|
|
|
try {
|
|
//
|
|
// New item, insert and return id
|
|
//
|
|
if (isNew) {
|
|
Zotero.debug('Saving data for new item to database');
|
|
|
|
var sqlColumns = [];
|
|
var sqlValues = [];
|
|
|
|
//
|
|
// Primary fields
|
|
//
|
|
|
|
// If available id value, use it -- otherwise we'll use autoincrement
|
|
var itemID = this.id ? this.id : Zotero.ID.get('items');
|
|
if (itemID) {
|
|
sqlColumns.push('itemID');
|
|
sqlValues.push({ int: itemID });
|
|
}
|
|
|
|
var key = this.key ? this.key : this._generateKey();
|
|
|
|
sqlColumns.push(
|
|
'itemTypeID',
|
|
'dateAdded',
|
|
'dateModified',
|
|
'clientDateModified',
|
|
'libraryID',
|
|
'key'
|
|
);
|
|
sqlValues.push(
|
|
{ int: this.getField('itemTypeID') },
|
|
this.dateAdded ? this.dateAdded : Zotero.DB.transactionDateTime,
|
|
this.dateModified ? this.dateModified : Zotero.DB.transactionDateTime,
|
|
Zotero.DB.transactionDateTime,
|
|
this.libraryID ? this.libraryID : null,
|
|
key
|
|
);
|
|
|
|
// Begin history transaction
|
|
// No associated id yet, so we use false
|
|
//Zotero.History.begin('add-item', false);
|
|
|
|
//
|
|
// Primary fields
|
|
//
|
|
var sql = "INSERT INTO items (" + sqlColumns.join(', ') + ') VALUES (';
|
|
// Insert placeholders for bind parameters
|
|
for (var i=0; i<sqlValues.length; i++) {
|
|
sql += '?, ';
|
|
}
|
|
sql = sql.substring(0, sql.length-2) + ")";
|
|
|
|
// Save basic data to items table
|
|
var insertID = Zotero.DB.query(sql, sqlValues);
|
|
if (!itemID) {
|
|
itemID = insertID;
|
|
}
|
|
|
|
//Zotero.History.setAssociatedID(itemID);
|
|
//Zotero.History.add('items', 'itemID', itemID);
|
|
|
|
//
|
|
// ItemData
|
|
//
|
|
if (this._changedItemData) {
|
|
// Use manual bound parameters to speed things up
|
|
sql = "SELECT valueID FROM itemDataValues WHERE value=?";
|
|
var valueStatement = Zotero.DB.getStatement(sql);
|
|
|
|
sql = "INSERT INTO itemDataValues VALUES (?,?)";
|
|
var insertValueStatement = Zotero.DB.getStatement(sql);
|
|
|
|
sql = "INSERT INTO itemData VALUES (?,?,?)";
|
|
var insertStatement = Zotero.DB.getStatement(sql);
|
|
|
|
for (fieldID in this._changedItemData) {
|
|
var value = this.getField(fieldID, true);
|
|
if (!value) {
|
|
continue;
|
|
}
|
|
|
|
if (Zotero.ItemFields.getID('accessDate') == fieldID
|
|
&& this.getField(fieldID) == 'CURRENT_TIMESTAMP') {
|
|
value = Zotero.DB.transactionDateTime;
|
|
}
|
|
|
|
var dataType = ZU.getSQLDataType(value);
|
|
|
|
switch (dataType) {
|
|
case 32:
|
|
valueStatement.bindInt32Parameter(0, value);
|
|
break;
|
|
|
|
case 64:
|
|
valueStatement.bindInt64Parameter(0, value);
|
|
break;
|
|
|
|
default:
|
|
valueStatement.bindUTF8StringParameter(0, value);
|
|
}
|
|
if (valueStatement.executeStep()) {
|
|
var valueID = valueStatement.getInt32(0);
|
|
}
|
|
else {
|
|
var valueID = null;
|
|
}
|
|
|
|
valueStatement.reset();
|
|
|
|
if (!valueID) {
|
|
valueID = Zotero.ID.get('itemDataValues');
|
|
insertValueStatement.bindInt32Parameter(0, valueID);
|
|
|
|
switch (dataType) {
|
|
case 32:
|
|
insertValueStatement.
|
|
bindInt32Parameter(1, value);
|
|
break;
|
|
|
|
case 64:
|
|
insertValueStatement.
|
|
bindInt64Parameter(1, value);
|
|
break;
|
|
|
|
default:
|
|
insertValueStatement.
|
|
bindUTF8StringParameter(1, value);
|
|
}
|
|
|
|
try {
|
|
insertValueStatement.execute();
|
|
}
|
|
catch (e) {
|
|
throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']');
|
|
}
|
|
}
|
|
|
|
insertStatement.bindInt32Parameter(0, itemID);
|
|
insertStatement.bindInt32Parameter(1, fieldID);
|
|
insertStatement.bindInt32Parameter(2, valueID);
|
|
|
|
try {
|
|
insertStatement.execute();
|
|
}
|
|
catch(e) {
|
|
throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']');
|
|
}
|
|
|
|
/*
|
|
Zotero.History.add('itemData', 'itemID-fieldID',
|
|
[itemID, fieldID]);
|
|
*/
|
|
}
|
|
}
|
|
|
|
//
|
|
// Creators
|
|
//
|
|
if (this._changedCreators) {
|
|
for (var orderIndex in this._changedCreators) {
|
|
Zotero.debug('Adding creator in position ' + orderIndex, 4);
|
|
var creator = this.getCreator(orderIndex);
|
|
|
|
/*
|
|
if (!creator.ref.exists()) {
|
|
throw ("Creator in position " + orderIndex + " doesn't exist");
|
|
}
|
|
*/
|
|
|
|
if (!creator) {
|
|
continue;
|
|
}
|
|
|
|
if (creator.ref.hasChanged()) {
|
|
Zotero.debug("Auto-saving changed creator " + creator.ref.id);
|
|
creator.ref.save();
|
|
}
|
|
|
|
sql = 'INSERT INTO itemCreators VALUES (?, ?, ?, ?)';
|
|
Zotero.DB.query(sql,
|
|
[{ int: itemID }, { int: creator.ref.id },
|
|
{ int: creator.creatorTypeID }, { int: orderIndex }]);
|
|
|
|
/*
|
|
Zotero.History.add('itemCreators',
|
|
'itemID-creatorID-creatorTypeID',
|
|
[this.id, creatorID, creator['creatorTypeID']]);
|
|
*/
|
|
}
|
|
}
|
|
|
|
|
|
if (this._changedDeleted) {
|
|
if (this.deleted) {
|
|
sql = "REPLACE INTO deletedItems (itemID) VALUES (?)";
|
|
}
|
|
else {
|
|
sql = "DELETE FROM deletedItems WHERE itemID=?";
|
|
}
|
|
Zotero.DB.query(sql, itemID);
|
|
}
|
|
|
|
|
|
// Note
|
|
if (this.isNote() || this._changedNote) {
|
|
sql = "INSERT INTO itemNotes "
|
|
+ "(itemID, sourceItemID, note, title) VALUES (?,?,?,?)";
|
|
var parent = this.isNote() ? this.getSource() : null;
|
|
var noteText = this._noteText ? this._noteText : '';
|
|
// Add <div> wrapper if not present
|
|
if (!noteText.match(/^<div class="zotero-note znv[0-9]+">[\s\S]*<\/div>$/)) {
|
|
noteText = '<div class="zotero-note znv1">' + noteText + '</div>';
|
|
}
|
|
|
|
var bindParams = [
|
|
itemID,
|
|
parent ? parent : null,
|
|
noteText,
|
|
this._noteTitle ? this._noteTitle : ''
|
|
];
|
|
Zotero.DB.query(sql, bindParams);
|
|
}
|
|
|
|
|
|
// Attachment
|
|
if (this.isAttachment()) {
|
|
var sql = "INSERT INTO itemAttachments (itemID, sourceItemID, linkMode, "
|
|
+ "mimeType, charsetID, path, syncState) VALUES (?,?,?,?,?,?,?)";
|
|
var parent = this.getSource();
|
|
var linkMode = this.attachmentLinkMode;
|
|
var mimeType = this.attachmentMIMEType;
|
|
var charsetID = this.attachmentCharset;
|
|
var path = this.attachmentPath;
|
|
var syncState = this.attachmentSyncState;
|
|
|
|
var bindParams = [
|
|
itemID,
|
|
parent ? parent : null,
|
|
{ int: linkMode },
|
|
mimeType ? { string: mimeType } : null,
|
|
charsetID ? { int: charsetID } : null,
|
|
path ? { string: path } : null,
|
|
syncState ? { int: syncState } : 0
|
|
];
|
|
Zotero.DB.query(sql, bindParams);
|
|
}
|
|
|
|
|
|
// Parent item
|
|
if (this._sourceItem) {
|
|
if (typeof this._sourceItem == 'number') {
|
|
var newSourceItem = Zotero.Items.get(this._sourceItem);
|
|
}
|
|
else {
|
|
var newSourceItem = Zotero.Items.getByLibraryAndKey(this.libraryID, this._sourceItem);
|
|
}
|
|
|
|
if (!newSourceItem) {
|
|
// TODO: clear caches?
|
|
var msg = "Cannot set source to invalid item " + this._sourceItem;
|
|
var e = new Zotero.Error(msg, "MISSING_OBJECT");
|
|
}
|
|
|
|
var newSourceItemNotifierData = {};
|
|
newSourceItemNotifierData[newSourceItem.id] = {
|
|
old: newSourceItem.serialize()
|
|
};
|
|
Zotero.Notifier.trigger('modify', 'item', newSourceItem.id, newSourceItemNotifierData);
|
|
|
|
switch (Zotero.ItemTypes.getName(this.itemTypeID)) {
|
|
case 'note':
|
|
newSourceItem.incrementNoteCount();
|
|
break;
|
|
case 'attachment':
|
|
newSourceItem.incrementAttachmentCount();
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// Related items
|
|
if (this._changed.relatedItems) {
|
|
var removed = [];
|
|
var newids = [];
|
|
var currentIDs = this._getRelatedItems(true);
|
|
if (!currentIDs) {
|
|
currentIDs = [];
|
|
}
|
|
|
|
if (this._previousData && this._previousData.related) {
|
|
for each(var id in this._previousData.related) {
|
|
if (currentIDs.indexOf(id) == -1) {
|
|
removed.push(id);
|
|
}
|
|
}
|
|
}
|
|
for each(var id in currentIDs) {
|
|
if (this._previousData && this._previousData.related &&
|
|
this._previousData.related.indexOf(id) != -1) {
|
|
continue;
|
|
}
|
|
newids.push(id);
|
|
}
|
|
|
|
if (removed.length) {
|
|
var sql = "DELETE FROM itemSeeAlso WHERE itemID=? "
|
|
+ "AND linkedItemID IN ("
|
|
+ removed.map(function () '?').join()
|
|
+ ")";
|
|
Zotero.DB.query(sql, [itemID].concat(removed));
|
|
}
|
|
|
|
if (newids.length) {
|
|
var sql = "INSERT INTO itemSeeAlso (itemID, linkedItemID) VALUES (?,?)";
|
|
var insertStatement = Zotero.DB.getStatement(sql);
|
|
|
|
for each(var linkedItemID in newids) {
|
|
insertStatement.bindInt32Parameter(0, itemID);
|
|
insertStatement.bindInt32Parameter(1, linkedItemID);
|
|
|
|
try {
|
|
insertStatement.execute();
|
|
}
|
|
catch (e) {
|
|
throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']');
|
|
}
|
|
}
|
|
}
|
|
|
|
Zotero.Notifier.trigger('modify', 'item', removed.concat(newids));
|
|
}
|
|
}
|
|
|
|
//
|
|
// Existing item, update
|
|
//
|
|
else {
|
|
Zotero.debug('Updating database with new item data', 4);
|
|
|
|
// Begin history transaction
|
|
//Zotero.History.begin('modify-item', this.id);
|
|
|
|
//
|
|
// Primary fields
|
|
//
|
|
//Zotero.History.modify('items', 'itemID', this.id);
|
|
|
|
|
|
var sql = "UPDATE items SET ";
|
|
var sqlValues = [];
|
|
|
|
var updateFields = [
|
|
'itemTypeID',
|
|
'dateAdded',
|
|
'dateModified',
|
|
'clientDateModified',
|
|
'libraryID',
|
|
'key'
|
|
];
|
|
for each(field in updateFields) {
|
|
if (this._changedPrimaryData && this._changedPrimaryData[field]) {
|
|
sql += field + '=?, ';
|
|
sqlValues.push(this.getField(field));
|
|
}
|
|
else if (field == 'dateModified' || field == 'clientDateModified') {
|
|
sql += field + '=?, ';
|
|
sqlValues.push(Zotero.DB.transactionDateTime);
|
|
}
|
|
}
|
|
|
|
sql = sql.substr(0, sql.length-2) + " WHERE itemID=?";
|
|
sqlValues.push({ int: this.id });
|
|
|
|
Zotero.DB.query(sql, sqlValues);
|
|
|
|
|
|
//
|
|
// ItemData
|
|
//
|
|
if (this._changedItemData) {
|
|
var del = [];
|
|
|
|
sql = "SELECT valueID FROM itemDataValues WHERE value=?";
|
|
var valueStatement = Zotero.DB.getStatement(sql);
|
|
|
|
sql = "INSERT INTO itemDataValues VALUES (?,?)";
|
|
var insertStatement = Zotero.DB.getStatement(sql);
|
|
|
|
sql = "REPLACE INTO itemData VALUES (?,?,?)";
|
|
var replaceStatement = Zotero.DB.getStatement(sql);
|
|
|
|
for (fieldID in this._changedItemData) {
|
|
var value = this.getField(fieldID, true);
|
|
|
|
// If field changed and is empty, mark row for deletion
|
|
if (!value) {
|
|
del.push(fieldID);
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
// Field exists
|
|
if (this._preChangeArray[Zotero.ItemFields.getName(fieldID)]) {
|
|
Zotero.History.modify('itemData', 'itemID-fieldID',
|
|
[this.id, fieldID]);
|
|
}
|
|
// Field is new
|
|
else {
|
|
Zotero.History.add('itemData', 'itemID-fieldID',
|
|
[this.id, fieldID]);
|
|
}
|
|
*/
|
|
|
|
if (Zotero.ItemFields.getID('accessDate') == fieldID
|
|
&& this.getField(fieldID) == 'CURRENT_TIMESTAMP') {
|
|
value = Zotero.DB.transactionDateTime;
|
|
}
|
|
|
|
var dataType = ZU.getSQLDataType(value);
|
|
|
|
switch (dataType) {
|
|
case 32:
|
|
valueStatement.bindInt32Parameter(0, value);
|
|
break;
|
|
|
|
case 64:
|
|
valueStatement.bindInt64Parameter(0, value);
|
|
break;
|
|
|
|
default:
|
|
valueStatement.bindUTF8StringParameter(0, value);
|
|
}
|
|
if (valueStatement.executeStep()) {
|
|
var valueID = valueStatement.getInt32(0);
|
|
}
|
|
else {
|
|
var valueID = null;
|
|
}
|
|
|
|
valueStatement.reset();
|
|
|
|
// Create data row if necessary
|
|
if (!valueID) {
|
|
valueID = Zotero.ID.get('itemDataValues');
|
|
insertStatement.bindInt32Parameter(0, valueID);
|
|
|
|
// If this is changed, search.js also needs to
|
|
// change
|
|
switch (dataType) {
|
|
case 32:
|
|
insertStatement.
|
|
bindInt32Parameter(1, value);
|
|
break;
|
|
|
|
case 64:
|
|
insertStatement.
|
|
bindInt64Parameter(1, value);
|
|
break;
|
|
|
|
default:
|
|
insertStatement.
|
|
bindUTF8StringParameter(1, value);
|
|
}
|
|
|
|
try {
|
|
insertStatement.execute();
|
|
}
|
|
catch (e) {
|
|
throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']');
|
|
}
|
|
}
|
|
|
|
replaceStatement.bindInt32Parameter(0, this.id);
|
|
replaceStatement.bindInt32Parameter(1, fieldID);
|
|
replaceStatement.bindInt32Parameter(2, valueID);
|
|
|
|
try {
|
|
replaceStatement.execute();
|
|
}
|
|
catch (e) {
|
|
throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']');
|
|
}
|
|
}
|
|
|
|
// Delete blank fields
|
|
if (del.length) {
|
|
/*
|
|
// Add to history
|
|
for (var i in del) {
|
|
Zotero.History.remove('itemData', 'itemID-fieldID',
|
|
[this.id, del[i]]);
|
|
}
|
|
*/
|
|
|
|
sql = 'DELETE from itemData WHERE itemID=? '
|
|
+ 'AND fieldID IN ('
|
|
+ del.map(function () '?').join()
|
|
+ ')';
|
|
Zotero.DB.query(sql, [this.id].concat(del));
|
|
}
|
|
}
|
|
|
|
//
|
|
// Creators
|
|
//
|
|
if (this._changedCreators) {
|
|
for (var orderIndex in this._changedCreators) {
|
|
Zotero.debug('Creator ' + orderIndex + ' has changed', 4);
|
|
|
|
var creator = this.getCreator(orderIndex);
|
|
|
|
/*
|
|
if (!creator.ref.exists()) {
|
|
throw ("Creator in position " + orderIndex + " doesn't exist");
|
|
}
|
|
*/
|
|
|
|
/*
|
|
// Delete at position
|
|
Zotero.History.remove('itemCreators', 'itemID-orderIndex',
|
|
[this.id, orderIndex]);
|
|
*/
|
|
|
|
var sql2 = 'DELETE FROM itemCreators WHERE itemID=?'
|
|
+ ' AND orderIndex=?';
|
|
Zotero.DB.query(sql2, [{ int: this.id }, { int: orderIndex }]);
|
|
|
|
if (!creator) {
|
|
continue;
|
|
}
|
|
|
|
if (creator.ref.hasChanged()) {
|
|
Zotero.debug("Auto-saving changed creator " + creator.ref.id);
|
|
creator.ref.save();
|
|
}
|
|
|
|
sql = "INSERT INTO itemCreators VALUES (?,?,?,?)";
|
|
|
|
sqlValues = [
|
|
{ int: this.id },
|
|
{ int: creator.ref.id },
|
|
{ int: creator.creatorTypeID },
|
|
{ int: orderIndex }
|
|
];
|
|
|
|
Zotero.DB.query(sql, sqlValues);
|
|
|
|
/*
|
|
Zotero.History.add('itemCreators',
|
|
'itemID-creatorID-creatorTypeID',
|
|
[this.id, creatorID, creator['creatorTypeID']]);
|
|
*/
|
|
}
|
|
}
|
|
|
|
|
|
if (this._changedDeleted) {
|
|
if (this.deleted) {
|
|
sql = "REPLACE INTO deletedItems (itemID) VALUES (?)";
|
|
}
|
|
else {
|
|
sql = "DELETE FROM deletedItems WHERE itemID=?";
|
|
}
|
|
Zotero.DB.query(sql, this.id);
|
|
}
|
|
|
|
|
|
// Note
|
|
if (this._changedNote) {
|
|
if (this._noteText === null || this._noteTitle === null) {
|
|
throw ('Cached note values not set with this._changedNote '
|
|
+ ' set to true in Item.save()');
|
|
}
|
|
|
|
var parent = this.isNote() ? this.getSource() : null;
|
|
var noteText = this._noteText;
|
|
// Add <div> wrapper if not present
|
|
if (!noteText.match(/^<div class="zotero-note znv[0-9]+">[\s\S]*<\/div>$/)) {
|
|
noteText = '<div class="zotero-note znv1">' + noteText + '</div>';
|
|
}
|
|
|
|
var sql = "SELECT COUNT(*) FROM itemNotes WHERE itemID=?";
|
|
if (Zotero.DB.valueQuery(sql, this.id)) {
|
|
sql = "UPDATE itemNotes SET sourceItemID=?, note=?, title=? WHERE itemID=?";
|
|
var bindParams = [
|
|
parent ? parent : null,
|
|
noteText,
|
|
this._noteTitle,
|
|
this.id
|
|
];
|
|
}
|
|
// Row might not yet exist for new embedded attachment notes
|
|
else {
|
|
sql = "INSERT INTO itemNotes "
|
|
+ "(itemID, sourceItemID, note, title) VALUES (?,?,?,?)";
|
|
var bindParams = [
|
|
this.id,
|
|
parent ? parent : null,
|
|
noteText,
|
|
this._noteTitle
|
|
];
|
|
}
|
|
Zotero.DB.query(sql, bindParams);
|
|
}
|
|
|
|
|
|
// Attachment
|
|
if (this._changedAttachmentData) {
|
|
var sql = "UPDATE itemAttachments SET sourceItemID=?, "
|
|
+ "linkMode=?, mimeType=?, charsetID=?, path=?, syncState=? "
|
|
+ "WHERE itemID=?";
|
|
var parent = this.getSource();
|
|
var linkMode = this.attachmentLinkMode;
|
|
var mimeType = this.attachmentMIMEType;
|
|
var charsetID = this.attachmentCharset;
|
|
var path = this.attachmentPath;
|
|
var syncState = this.attachmentSyncState;
|
|
|
|
var bindParams = [
|
|
parent ? parent : null,
|
|
{ int: linkMode },
|
|
mimeType ? { string: mimeType } : null,
|
|
charsetID ? { int: charsetID } : null,
|
|
path ? { string: path } : null,
|
|
syncState ? { int: syncState } : 0,
|
|
this.id
|
|
];
|
|
Zotero.DB.query(sql, bindParams);
|
|
}
|
|
|
|
|
|
// Parent
|
|
if (this._changedSource) {
|
|
var type = Zotero.ItemTypes.getName(this.itemTypeID);
|
|
var Type = type[0].toUpperCase() + type.substr(1);
|
|
|
|
if (this._sourceItem) {
|
|
if (typeof this._sourceItem == 'number') {
|
|
var newSourceItem = Zotero.Items.get(this._sourceItem);
|
|
}
|
|
else {
|
|
var newSourceItem = Zotero.Items.getByLibraryAndKey(this.libraryID, this._sourceItem);
|
|
}
|
|
|
|
if (!newSourceItem) {
|
|
// TODO: clear caches
|
|
var msg = "Cannot set source to invalid item " + this._sourceItem;
|
|
var e = new Zotero.Error(msg, "MISSING_OBJECT");
|
|
throw (e);
|
|
}
|
|
|
|
var newSourceItemNotifierData = {};
|
|
newSourceItemNotifierData[newSourceItem.id] = {
|
|
old: newSourceItem.serialize()
|
|
};
|
|
Zotero.Notifier.trigger('modify', 'item', newSourceItem.id, newSourceItemNotifierData);
|
|
}
|
|
|
|
if (this._previousData) {
|
|
var oldSourceItemKey = this._previousData.sourceItemKey;
|
|
if (oldSourceItemKey) {
|
|
var oldSourceItem = Zotero.Items.getByLibraryAndKey(this.libraryID, oldSourceItemKey);
|
|
}
|
|
if (oldSourceItem) {
|
|
var oldSourceItemNotifierData = {};
|
|
oldSourceItemNotifierData[oldSourceItem.id] = {
|
|
old: oldSourceItem.serialize()
|
|
};
|
|
Zotero.Notifier.trigger('modify', 'item', oldSourceItem.id, oldSourceItemNotifierData);
|
|
}
|
|
else if (oldSourceItemKey) {
|
|
var oldSourceItemNotifierData = null;
|
|
Zotero.debug("Old source item " + oldSourceItemKey
|
|
+ " didn't exist in Zotero.Item.save()", 2);
|
|
}
|
|
}
|
|
|
|
|
|
// If this was an independent item, remove from any collections
|
|
// where it existed previously and add source instead if
|
|
// there is one
|
|
if (!oldSourceItemKey) {
|
|
var sql = "SELECT collectionID FROM collectionItems "
|
|
+ "WHERE itemID=?";
|
|
var changedCollections = Zotero.DB.columnQuery(sql, this.id);
|
|
if (changedCollections) {
|
|
sql = "UPDATE collections SET dateModified=CURRENT_TIMESTAMP, clientDateModified=CURRENT_TIMESTAMP "
|
|
+ "WHERE collectionID IN (SELECT collectionID FROM collectionItems WHERE itemID=?)";
|
|
Zotero.DB.query(sql, this.id);
|
|
|
|
if (newSourceItem) {
|
|
sql = "UPDATE OR REPLACE collectionItems "
|
|
+ "SET itemID=? WHERE itemID=?";
|
|
Zotero.DB.query(sql, [newSourceItem.id, this.id]);
|
|
}
|
|
else {
|
|
sql = "DELETE FROM collectionItems WHERE itemID=?";
|
|
Zotero.DB.query(sql, this.id);
|
|
}
|
|
|
|
for each(var c in changedCollections) {
|
|
Zotero.Notifier.trigger('remove', 'collection-item', c + '-' + this.id);
|
|
}
|
|
|
|
Zotero.Collections.reload(changedCollections);
|
|
}
|
|
}
|
|
|
|
// Update DB, if not a note or attachment we already changed above
|
|
if (!this._changedAttachmentData &&
|
|
(!this._changedNote || !this.isNote())) {
|
|
var sql = "UPDATE item" + Type + "s SET sourceItemID=? "
|
|
+ "WHERE itemID=?";
|
|
var bindParams = [
|
|
newSourceItem ? newSourceItem.id : null, this.id
|
|
];
|
|
Zotero.DB.query(sql, bindParams);
|
|
}
|
|
|
|
// Update the counts of the previous and new sources
|
|
if (oldSourceItem) {
|
|
switch (type) {
|
|
case 'note':
|
|
oldSourceItem.decrementNoteCount();
|
|
break;
|
|
case 'attachment':
|
|
oldSourceItem.decrementAttachmentCount();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (newSourceItem) {
|
|
switch (type) {
|
|
case 'note':
|
|
newSourceItem.incrementNoteCount();
|
|
break;
|
|
case 'attachment':
|
|
newSourceItem.incrementAttachmentCount();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Related items
|
|
if (this._changed.relatedItems) {
|
|
var removed = [];
|
|
var newids = [];
|
|
var currentIDs = this._getRelatedItems(true);
|
|
if (!currentIDs) {
|
|
currentIDs = [];
|
|
}
|
|
|
|
if (this._previousData && this._previousData.related) {
|
|
for each(var id in this._previousData.related) {
|
|
if (currentIDs.indexOf(id) == -1) {
|
|
removed.push(id);
|
|
}
|
|
}
|
|
}
|
|
for each(var id in currentIDs) {
|
|
if (this._previousData && this._previousData.related &&
|
|
this._previousData.related.indexOf(id) != -1) {
|
|
continue;
|
|
}
|
|
newids.push(id);
|
|
}
|
|
|
|
if (removed.length) {
|
|
var sql = "DELETE FROM itemSeeAlso WHERE itemID=? "
|
|
+ "AND linkedItemID IN ("
|
|
+ removed.map(function () '?').join()
|
|
+ ")";
|
|
Zotero.DB.query(sql, [this.id].concat(removed));
|
|
}
|
|
|
|
if (newids.length) {
|
|
var sql = "INSERT INTO itemSeeAlso (itemID, linkedItemID) VALUES (?,?)";
|
|
var insertStatement = Zotero.DB.getStatement(sql);
|
|
|
|
for each(var linkedItemID in newids) {
|
|
insertStatement.bindInt32Parameter(0, this.id);
|
|
insertStatement.bindInt32Parameter(1, linkedItemID);
|
|
|
|
try {
|
|
insertStatement.execute();
|
|
}
|
|
catch (e) {
|
|
throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']');
|
|
}
|
|
}
|
|
}
|
|
|
|
Zotero.Notifier.trigger('modify', 'item', removed.concat(newids));
|
|
}
|
|
}
|
|
|
|
//Zotero.History.commit();
|
|
Zotero.DB.commitTransaction();
|
|
}
|
|
|
|
catch (e) {
|
|
//Zotero.History.cancel();
|
|
Zotero.DB.rollbackTransaction();
|
|
Zotero.debug(e);
|
|
throw(e);
|
|
}
|
|
|
|
if (!this.id) {
|
|
this._id = itemID;
|
|
}
|
|
|
|
if (!this.key) {
|
|
this._key = key;
|
|
}
|
|
|
|
if (this._changedDeleted) {
|
|
// Update child item counts on parent
|
|
var sourceItemID = this.getSource();
|
|
if (sourceItemID) {
|
|
var sourceItem = Zotero.Items.get(sourceItemID);
|
|
if (this._deleted) {
|
|
if (this.isAttachment()) {
|
|
sourceItem.decrementAttachmentCount();
|
|
}
|
|
else {
|
|
sourceItem.decrementNoteCount();
|
|
}
|
|
}
|
|
else {
|
|
if (this.isAttachment()) {
|
|
sourceItem.incrementAttachmentCount();
|
|
}
|
|
else {
|
|
sourceItem.incrementNoteCount();
|
|
}
|
|
}
|
|
}
|
|
// Refresh trash
|
|
Zotero.Notifier.trigger('refresh', 'collection', 0);
|
|
if (this._deleted) {
|
|
Zotero.Notifier.trigger('trash', 'item', this.id);
|
|
}
|
|
}
|
|
|
|
Zotero.Items.reload(this.id);
|
|
|
|
if (isNew) {
|
|
Zotero.Notifier.trigger('add', 'item', this.id);
|
|
}
|
|
else {
|
|
Zotero.Notifier.trigger('modify', 'item', this.id, { old: this._previousData });
|
|
}
|
|
|
|
if (isNew) {
|
|
var id = this.id;
|
|
this._disabled = true;
|
|
return id;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Used by sync code
|
|
*/
|
|
Zotero.Item.prototype.updateClientDateModified = function () {
|
|
if (!this.id) {
|
|
throw ("Cannot update clientDateModified of unsaved item in Zotero.Item.updateClientDateModified()");
|
|
}
|
|
var sql = "UPDATE items SET clientDateModified=? WHERE itemID=?";
|
|
Zotero.DB.query(sql, [Zotero.DB.transactionDateTime, this.id]);
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.isRegularItem = function() {
|
|
return !(this.isNote() || this.isAttachment());
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.isTopLevelItem = function () {
|
|
return this.isRegularItem() || !this.getSourceKey();
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.numChildren = function(includeTrashed) {
|
|
return this.numNotes(includeTrashed) + this.numAttachments(includeTrashed);
|
|
}
|
|
|
|
|
|
/**
|
|
* @return {Integer|FALSE} itemID of the parent item for an attachment or note, or FALSE if none
|
|
*/
|
|
Zotero.Item.prototype.getSource = function() {
|
|
if (this._sourceItem === false) {
|
|
return false;
|
|
}
|
|
|
|
if (this._sourceItem !== null) {
|
|
if (typeof this._sourceItem == 'number') {
|
|
return this._sourceItem;
|
|
}
|
|
var sourceItem = Zotero.Items.getByLibraryAndKey(this.libraryID, this._sourceItem);
|
|
if (!sourceItem) {
|
|
var msg = "Source item for keyed source doesn't exist in Zotero.Item.getSource() " + "(" + this._sourceItem + ")";
|
|
var e = new Zotero.Error(msg, "MISSING_OBJECT");
|
|
throw (e);
|
|
}
|
|
// Replace stored key with id
|
|
this._sourceItem = sourceItem.id;
|
|
return sourceItem.id;
|
|
}
|
|
|
|
if (!this.id) {
|
|
return false;
|
|
}
|
|
|
|
if (this.isNote()) {
|
|
var Type = 'Note';
|
|
}
|
|
else if (this.isAttachment()) {
|
|
var Type = 'Attachment';
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
|
|
var sql = "SELECT sourceItemID FROM item" + Type + "s WHERE itemID=?";
|
|
var sourceItemID = Zotero.DB.valueQuery(sql, this.id);
|
|
if (!sourceItemID) {
|
|
sourceItemID = null;
|
|
}
|
|
this._sourceItem = sourceItemID;
|
|
return sourceItemID;
|
|
}
|
|
|
|
|
|
/**
|
|
* @return {String|FALSE} Key of the parent item for an attachment or note, or FALSE if none
|
|
*/
|
|
Zotero.Item.prototype.getSourceKey = function() {
|
|
if (this._sourceItem === false) {
|
|
return false;
|
|
}
|
|
|
|
if (this._sourceItem !== null) {
|
|
if (typeof this._sourceItem == 'string') {
|
|
return this._sourceItem;
|
|
}
|
|
var sourceItem = Zotero.Items.get(this._sourceItem);
|
|
return sourceItem.key;
|
|
}
|
|
|
|
if (!this.id) {
|
|
return false;
|
|
}
|
|
|
|
if (this.isNote()) {
|
|
var Type = 'Note';
|
|
}
|
|
else if (this.isAttachment()) {
|
|
var Type = 'Attachment';
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
|
|
var sql = "SELECT key FROM item" + Type + "s A JOIN items B "
|
|
+ "ON (A.sourceItemID=B.itemID) WHERE A.itemID=?";
|
|
var key = Zotero.DB.valueQuery(sql, this.id);
|
|
if (!key) {
|
|
key = null;
|
|
}
|
|
this._sourceItem = key;
|
|
return key;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.setSource = function(sourceItemID) {
|
|
if (this.isNote()) {
|
|
var type = 'note';
|
|
var Type = 'Note';
|
|
}
|
|
else if (this.isAttachment()) {
|
|
var type = 'attachment';
|
|
var Type = 'Attachment';
|
|
}
|
|
else {
|
|
throw ("setSource() can only be called on items of type 'note' or 'attachment'");
|
|
}
|
|
|
|
var oldSourceItemID = this.getSource();
|
|
if (oldSourceItemID == sourceItemID) {
|
|
Zotero.debug("Source item has not changed for item " + this.id);
|
|
return false;
|
|
}
|
|
|
|
if (this.id && this.exists() && !this._previousData) {
|
|
this._previousData = this.serialize();
|
|
}
|
|
|
|
this._sourceItem = sourceItemID ? parseInt(sourceItemID) : false;
|
|
this._changedSource = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.setSourceKey = function(sourceItemKey) {
|
|
if (this.isNote()) {
|
|
var type = 'note';
|
|
var Type = 'Note';
|
|
}
|
|
else if (this.isAttachment()) {
|
|
var type = 'attachment';
|
|
var Type = 'Attachment';
|
|
}
|
|
else {
|
|
throw ("setSourceKey() can only be called on items of type 'note' or 'attachment'");
|
|
}
|
|
|
|
var oldSourceItemID = this.getSource();
|
|
if (oldSourceItemID) {
|
|
var sourceItem = Zotero.Items.get(oldSourceItemID);
|
|
var oldSourceItemKey = sourceItem.key;
|
|
}
|
|
else {
|
|
var oldSourceItemKey = false;
|
|
}
|
|
if (oldSourceItemKey == sourceItemKey) {
|
|
Zotero.debug("Source item has not changed in Zotero.Item.setSourceKey()");
|
|
return false;
|
|
}
|
|
|
|
if (this.id && this.exists() && !this._previousData) {
|
|
this._previousData = this.serialize();
|
|
}
|
|
|
|
this._sourceItem = sourceItemKey ? sourceItemKey : false;
|
|
this._changedSource = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////
|
|
//
|
|
// Methods dealing with note items
|
|
//
|
|
////////////////////////////////////////////////////////
|
|
Zotero.Item.prototype.incrementNoteCount = function() {
|
|
this._numNotes++;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.decrementNoteCount = function() {
|
|
this._numNotes--;
|
|
}
|
|
|
|
|
|
/**
|
|
* 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
|
|
* @return {Integer}
|
|
*/
|
|
Zotero.Item.prototype.numNotes = function(includeTrashed) {
|
|
if (this.isNote()) {
|
|
throw ("numNotes() cannot be called on items of type 'note'");
|
|
}
|
|
|
|
if (!this.id) {
|
|
return 0;
|
|
}
|
|
|
|
var deleted = 0;
|
|
if (includeTrashed) {
|
|
var sql = "SELECT COUNT(*) FROM itemNotes WHERE sourceItemID=? AND "
|
|
+ "itemID IN (SELECT itemID FROM deletedItems)";
|
|
deleted = parseInt(Zotero.DB.valueQuery(sql, this.id));
|
|
}
|
|
|
|
return this._numNotes + deleted;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the first line of the note for display in the items list
|
|
*
|
|
* Note: Note titles can also come from Zotero.Items.cacheFields()!
|
|
*
|
|
* @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;
|
|
}
|
|
|
|
if (!this.id) {
|
|
return '';
|
|
}
|
|
|
|
var sql = "SELECT title FROM itemNotes WHERE itemID=?";
|
|
var title = Zotero.DB.valueQuery(sql, this.id);
|
|
|
|
this._noteTitle = title ? title : '';
|
|
|
|
return title ? title : '';
|
|
}
|
|
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
if (!this.id) {
|
|
return '';
|
|
}
|
|
|
|
var sql = "SELECT note FROM itemNotes WHERE itemID=?";
|
|
var note = Zotero.DB.valueQuery(sql, this.id);
|
|
|
|
// Convert non-HTML notes on-the-fly
|
|
if (note) {
|
|
if (!note.match(/^<div class="zotero-note znv[0-9]+">[\s\S]*<\/div>$/)) {
|
|
note = Zotero.Utilities.prototype.htmlSpecialChars(note);
|
|
note = '<p>'
|
|
+ note.replace(/\n/g, '</p><p>')
|
|
.replace(/\t/g, ' ')
|
|
.replace(/ /g, ' ')
|
|
+ '</p>';
|
|
note = note.replace(/<p>\s*<\/p>/g, '<p> </p>');
|
|
var sql = "UPDATE itemNotes SET note=? WHERE itemID=?";
|
|
Zotero.DB.query(sql, [note, this.id]);
|
|
}
|
|
|
|
// Don't include <div> wrapper when returning value
|
|
note = note.replace(/^<div class="zotero-note znv[0-9]+">([\s\S]*)<\/div>$/, '$1');
|
|
}
|
|
|
|
this._noteText = note ? note : '';
|
|
|
|
return this._noteText;
|
|
}
|
|
|
|
|
|
/**
|
|
* 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 = Zotero.Utilities.prototype.trim(text);
|
|
|
|
var oldText = this.getNote();
|
|
if (text == oldText) {
|
|
Zotero.debug("Note has not changed in Zotero.Item.setNote()");
|
|
return false;
|
|
}
|
|
|
|
if (this.id && this.exists() && !this._previousData) {
|
|
this._previousData = this.serialize();
|
|
}
|
|
|
|
this._noteText = text;
|
|
this._noteTitle = Zotero.Notes.noteToTitle(text);
|
|
this._changedNote = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns child notes of this item
|
|
*
|
|
* @param {Boolean} includeTrashed Include trashed child items
|
|
* @return {Integer[]} Array of itemIDs, or FALSE if none
|
|
*/
|
|
Zotero.Item.prototype.getNotes = function(includeTrashed) {
|
|
if (this.isNote()) {
|
|
throw ("getNotes() cannot be called on items of type 'note'");
|
|
}
|
|
|
|
if (!this.id) {
|
|
return [];
|
|
}
|
|
|
|
var sql = "SELECT N.itemID, title FROM itemNotes N NATURAL JOIN items "
|
|
+ "WHERE sourceItemID=?";
|
|
if (!includeTrashed) {
|
|
sql += " AND N.itemID NOT IN (SELECT itemID FROM deletedItems)";
|
|
}
|
|
|
|
if (Zotero.Prefs.get('sortNotesChronologically')) {
|
|
sql += " ORDER BY dateAdded";
|
|
return Zotero.DB.columnQuery(sql, this.id);
|
|
}
|
|
|
|
var notes = Zotero.DB.query(sql, this.id);
|
|
if (!notes) {
|
|
return false;
|
|
}
|
|
|
|
// Sort by title
|
|
var collation = Zotero.getLocaleCollation();
|
|
var f = function (a, b) {
|
|
var aTitle = Zotero.Items.getSortTitle(a.title);
|
|
var bTitle = Zotero.Items.getSortTitle(b.title);
|
|
return collation.compareString(1, aTitle, bTitle);
|
|
}
|
|
|
|
var noteIDs = [];
|
|
notes.sort(f);
|
|
for each(var note in notes) {
|
|
noteIDs.push(note.itemID);
|
|
}
|
|
return noteIDs;
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////
|
|
//
|
|
// Methods dealing with attachments
|
|
//
|
|
// save() is not required for attachment functions
|
|
//
|
|
///////////////////////////////////////////////////////
|
|
Zotero.Item.prototype.incrementAttachmentCount = function() {
|
|
this._numAttachments++;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.decrementAttachmentCount = function() {
|
|
this._numAttachments--;
|
|
}
|
|
|
|
|
|
/**
|
|
* Determine if an item is an attachment
|
|
**/
|
|
Zotero.Item.prototype.isAttachment = function() {
|
|
return Zotero.ItemTypes.getName(this.itemTypeID) == 'attachment';
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
/**
|
|
* 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");
|
|
}
|
|
|
|
if (!this.id) {
|
|
return 0;
|
|
}
|
|
|
|
var deleted = 0;
|
|
if (includeTrashed) {
|
|
var sql = "SELECT COUNT(*) FROM itemAttachments WHERE sourceItemID=? AND "
|
|
+ "itemID IN (SELECT itemID FROM deletedItems)";
|
|
deleted = parseInt(Zotero.DB.valueQuery(sql, this.id));
|
|
}
|
|
|
|
return this._numAttachments + deleted;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get an nsILocalFile for the attachment, or false if the associated file
|
|
* doesn't exist
|
|
*
|
|
* _row_ is optional itemAttachments row if available to skip queries
|
|
*
|
|
* Note: Always returns false for items with LINK_MODE_LINKED_URL,
|
|
* since they have no files -- use getField('url') instead
|
|
**/
|
|
Zotero.Item.prototype.getFile = function(row, skipExistsCheck) {
|
|
if (!this.isAttachment()) {
|
|
throw ("getFile() can only be called on attachment items");
|
|
}
|
|
|
|
if (!row) {
|
|
var row = {
|
|
linkMode: this.attachmentLinkMode,
|
|
path: this.attachmentPath
|
|
};
|
|
}
|
|
|
|
// No associated files for linked URLs
|
|
if (row.linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
|
return false;
|
|
}
|
|
|
|
if (!row.path) {
|
|
Zotero.debug("Attachment path is empty", 2);
|
|
return false;
|
|
}
|
|
|
|
if (row.linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL ||
|
|
row.linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE) {
|
|
try {
|
|
if (row.path.indexOf("storage:") == -1) {
|
|
Zotero.debug("Invalid attachment path '" + row.path + "'", 2);
|
|
throw ('Invalid path');
|
|
}
|
|
// Strip "storage:"
|
|
var path = row.path.substr(8);
|
|
// setRelativeDescriptor() silently uses the parent directory on Windows
|
|
// if the filename contains certain characters, so strip them —
|
|
// but don't skip characters outside of XML range, since they may be
|
|
// correct in the opaque relative descriptor string
|
|
//
|
|
// This is a bad place for this, since the change doesn't make it
|
|
// back up to the sync server, but we do it to make sure we don't
|
|
// accidentally use the parent dir. Syncing to OS X, which doesn't
|
|
// exhibit this bug, will properly correct such filenames in
|
|
// storage.js and propagate the change
|
|
if (Zotero.isWin) {
|
|
path = Zotero.File.getValidFileName(path, true);
|
|
}
|
|
var file = Zotero.Attachments.getStorageDirectory(this.id);
|
|
file.QueryInterface(Components.interfaces.nsILocalFile);
|
|
file.setRelativeDescriptor(file, path);
|
|
}
|
|
catch (e) {
|
|
// See if this is a persistent path
|
|
// (deprecated for imported attachments)
|
|
Zotero.debug('Trying as persistent descriptor');
|
|
|
|
try {
|
|
var file = Components.classes["@mozilla.org/file/local;1"].
|
|
createInstance(Components.interfaces.nsILocalFile);
|
|
file.persistentDescriptor = row.path;
|
|
|
|
// If valid, convert this to a relative descriptor
|
|
if (file.exists()) {
|
|
Zotero.DB.query("UPDATE itemAttachments SET path=? WHERE itemID=?",
|
|
["storage:" + file.leafName, this.id]);
|
|
}
|
|
}
|
|
catch (e) {
|
|
Zotero.debug('Invalid persistent descriptor', 2);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
var file = Components.classes["@mozilla.org/file/local;1"].
|
|
createInstance(Components.interfaces.nsILocalFile);
|
|
|
|
try {
|
|
file.persistentDescriptor = row.path;
|
|
}
|
|
catch (e) {
|
|
// See if this is an old relative path (deprecated)
|
|
Zotero.debug('Invalid persistent descriptor -- trying relative');
|
|
try {
|
|
var refDir = (row.linkMode == this.LINK_MODE_LINKED_FILE)
|
|
? Zotero.getZoteroDirectory() : Zotero.getStorageDirectory();
|
|
file.setRelativeDescriptor(refDir, row.path);
|
|
// If valid, convert this to a persistent descriptor
|
|
if (file.exists()) {
|
|
Zotero.DB.query("UPDATE itemAttachments SET path=? WHERE itemID=?",
|
|
[file.persistentDescriptor, this.id]);
|
|
}
|
|
}
|
|
catch (e) {
|
|
Zotero.debug('Invalid relative descriptor', 2);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!skipExistsCheck && !file.exists()) {
|
|
Zotero.debug("Attachment file '" + file.path + "' not found", 2);
|
|
return false;
|
|
}
|
|
|
|
return file;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.getFilename = function () {
|
|
if (!this.isAttachment()) {
|
|
throw ("getFileName() can only be called on attachment items in Zotero.Item.getFilename()");
|
|
}
|
|
|
|
if (this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
|
throw ("getFilename() cannot be called on link attachments in Zotero.Item.getFilename()");
|
|
}
|
|
|
|
var file = this.getFile(null, true);
|
|
if (!file) {
|
|
return false;
|
|
}
|
|
|
|
return file.leafName;
|
|
}
|
|
|
|
|
|
/*
|
|
* 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 = function(newName, overwrite) {
|
|
var file = this.getFile();
|
|
if (!file) {
|
|
Zotero.debug("Attachment file not found in renameAttachmentFile()", 2);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
newName = Zotero.File.getValidFileName(newName);
|
|
|
|
var dest = file.parent;
|
|
dest.append(newName);
|
|
|
|
// Ignore if no change
|
|
//
|
|
// Note: Just comparing file.leafName to newName isn't reliable
|
|
if (file.leafName == dest.leafName) {
|
|
return true;
|
|
}
|
|
|
|
if (overwrite) {
|
|
dest.remove(false);
|
|
}
|
|
else if (dest.exists()) {
|
|
return -1;
|
|
}
|
|
|
|
file.moveTo(null, newName);
|
|
// Update mod time and clear hash so the file syncs
|
|
// TODO: use an integer counter instead of mod time for change detection
|
|
dest.lastModifiedTime = new Date();
|
|
this.relinkAttachmentFile(dest);
|
|
|
|
Zotero.DB.beginTransaction();
|
|
|
|
Zotero.Sync.Storage.setSyncedHash(this.id, null, false);
|
|
Zotero.Sync.Storage.setSyncState(this.id, Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD);
|
|
|
|
Zotero.DB.commitTransaction();
|
|
|
|
return true;
|
|
}
|
|
catch (e) {
|
|
Zotero.debug(e);
|
|
Components.utils.reportError(e);
|
|
return -2;
|
|
}
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.relinkAttachmentFile = function(file) {
|
|
var linkMode = this.attachmentLinkMode;
|
|
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
|
throw('Cannot relink linked URL in Zotero.Items.relinkAttachmentFile()');
|
|
}
|
|
|
|
var newName = Zotero.File.getValidFileName(file.leafName);
|
|
if (!newName) {
|
|
throw ("No valid characters in filename after filtering in Zotero.Item.relinkAttachmentFile()");
|
|
}
|
|
|
|
// Rename file to filtered name if necessary
|
|
if (file.leafName != newName) {
|
|
Zotero.debug("Renaming file '" + file.leafName + "' to '" + newName + "'");
|
|
file.moveTo(null, newName);
|
|
}
|
|
|
|
var path = Zotero.Attachments.getPath(file, linkMode);
|
|
this.attachmentPath = path;
|
|
this.save();
|
|
|
|
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.Item.prototype.__defineGetter__('attachmentLinkMode', function () {
|
|
if (!this.isAttachment()) {
|
|
return undefined;
|
|
}
|
|
|
|
if (this._attachmentLinkMode !== null) {
|
|
return this._attachmentLinkMode;
|
|
}
|
|
|
|
if (!this.id) {
|
|
return null;
|
|
}
|
|
|
|
var sql = "SELECT linkMode FROM itemAttachments WHERE itemID=?";
|
|
var linkMode = Zotero.DB.valueQuery(sql, this.id);
|
|
this._attachmentLinkMode = linkMode;
|
|
return linkMode;
|
|
});
|
|
|
|
|
|
Zotero.Item.prototype.__defineSetter__('attachmentLinkMode', function (val) {
|
|
if (!this.isAttachment()) {
|
|
throw (".attachmentLinkMode can only be set for attachment items");
|
|
}
|
|
|
|
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._changedAttachmentData) {
|
|
this._changedAttachmentData = {};
|
|
}
|
|
this._changedAttachmentData.linkMode = true;
|
|
this._attachmentLinkMode = val;
|
|
});
|
|
|
|
|
|
Zotero.Item.prototype.getAttachmentMIMEType = function() {
|
|
Zotero.debug("getAttachmentMIMEType() deprecated -- use .attachmentMIMEType");
|
|
return this.attachmentMIMEType;
|
|
}
|
|
|
|
/**
|
|
* MIME type of an attachment (e.g. 'text/plain')
|
|
*/
|
|
Zotero.Item.prototype.__defineGetter__('attachmentMIMEType', function () {
|
|
if (!this.isAttachment()) {
|
|
return undefined;
|
|
}
|
|
|
|
if (this._attachmentMIMEType !== null) {
|
|
return this._attachmentMIMEType;
|
|
}
|
|
|
|
if (!this.id) {
|
|
return '';
|
|
}
|
|
|
|
var sql = "SELECT mimeType FROM itemAttachments WHERE itemID=?";
|
|
var mimeType = Zotero.DB.valueQuery(sql, this.id);
|
|
if (!mimeType) {
|
|
mimeType = '';
|
|
}
|
|
this._attachmentMIMEType = mimeType;
|
|
return mimeType;
|
|
});
|
|
|
|
|
|
Zotero.Item.prototype.__defineSetter__('attachmentMIMEType', function (val) {
|
|
if (!this.isAttachment()) {
|
|
throw (".attachmentMIMEType can only be set for attachment items");
|
|
}
|
|
|
|
if (!val) {
|
|
val = '';
|
|
}
|
|
|
|
if (val == this._attachmentMIMEType) {
|
|
return;
|
|
}
|
|
|
|
if (!this._changedAttachmentData) {
|
|
this._changedAttachmentData = {};
|
|
}
|
|
this._changedAttachmentData.mimeType = true;
|
|
this._attachmentMIMEType = val;
|
|
});
|
|
|
|
|
|
Zotero.Item.prototype.getAttachmentCharset = function() {
|
|
Zotero.debug("getAttachmentCharset() deprecated -- use .attachmentCharset");
|
|
return this.attachmentCharset;
|
|
}
|
|
|
|
|
|
/**
|
|
* Character set of an attachment
|
|
*/
|
|
Zotero.Item.prototype.__defineGetter__('attachmentCharset', function () {
|
|
if (!this.isAttachment()) {
|
|
return undefined;
|
|
}
|
|
|
|
if (this._attachmentCharset != undefined) {
|
|
return this._attachmentCharset;
|
|
}
|
|
|
|
if (!this.id) {
|
|
return null;
|
|
}
|
|
|
|
var sql = "SELECT charsetID FROM itemAttachments WHERE itemID=?";
|
|
var charset = Zotero.DB.valueQuery(sql, this.id);
|
|
if (!charset) {
|
|
charset = null;
|
|
}
|
|
this._attachmentCharset = charset;
|
|
return charset;
|
|
});
|
|
|
|
|
|
Zotero.Item.prototype.__defineSetter__('attachmentCharset', function (val) {
|
|
if (!this.isAttachment()) {
|
|
throw (".attachmentCharset can only be set for attachment items");
|
|
}
|
|
|
|
val = Zotero.CharacterSets.getID(val);
|
|
|
|
if (!val) {
|
|
val = null;
|
|
}
|
|
|
|
if (val == this._attachmentCharset) {
|
|
return;
|
|
}
|
|
|
|
if (!this._changedAttachmentData) {
|
|
this._changedAttachmentData = {};
|
|
}
|
|
this._changedAttachmentData.charset = true;
|
|
this._attachmentCharset = val;
|
|
});
|
|
|
|
|
|
Zotero.Item.prototype.__defineGetter__('attachmentPath', function () {
|
|
if (!this.isAttachment()) {
|
|
return undefined;
|
|
}
|
|
|
|
if (this._attachmentPath !== null) {
|
|
return this._attachmentPath;
|
|
}
|
|
|
|
if (!this.id) {
|
|
return '';
|
|
}
|
|
|
|
var sql = "SELECT path FROM itemAttachments WHERE itemID=?";
|
|
var path = Zotero.DB.valueQuery(sql, this.id);
|
|
if (!path) {
|
|
path = '';
|
|
}
|
|
this._attachmentPath = path;
|
|
return path;
|
|
});
|
|
|
|
|
|
Zotero.Item.prototype.__defineSetter__('attachmentPath', function (val) {
|
|
if (!this.isAttachment()) {
|
|
throw (".attachmentPath can only be set for attachment items");
|
|
}
|
|
|
|
if (this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
|
throw ('attachmentPath cannot be set for link attachments');
|
|
}
|
|
|
|
if (!val) {
|
|
val = '';
|
|
}
|
|
|
|
if (val == this._attachmentPath) {
|
|
return;
|
|
}
|
|
|
|
if (!this._changedAttachmentData) {
|
|
this._changedAttachmentData = {};
|
|
}
|
|
this._changedAttachmentData.path = true;
|
|
this._attachmentPath = val;
|
|
});
|
|
|
|
|
|
Zotero.Item.prototype.__defineGetter__('attachmentSyncState', function () {
|
|
if (!this.isAttachment()) {
|
|
return undefined;
|
|
}
|
|
|
|
if (this._attachmentSyncState != undefined) {
|
|
return this._attachmentSyncState;
|
|
}
|
|
|
|
if (!this.id) {
|
|
return undefined;
|
|
}
|
|
|
|
var sql = "SELECT syncState FROM itemAttachments WHERE itemID=?";
|
|
var syncState = Zotero.DB.valueQuery(sql, this.id);
|
|
this._attachmentSyncState = syncState;
|
|
return syncState;
|
|
});
|
|
|
|
|
|
Zotero.Item.prototype.__defineSetter__('attachmentSyncState', function (val) {
|
|
if (!this.isAttachment()) {
|
|
throw ("attachmentSyncState 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 ("attachmentSyncState can only be set for snapshots and "
|
|
+ "imported files");
|
|
}
|
|
|
|
switch (val) {
|
|
case Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD:
|
|
case Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD:
|
|
case Zotero.Sync.Storage.SYNC_STATE_IN_SYNC:
|
|
break;
|
|
|
|
default:
|
|
throw ("Invalid sync state '" + val
|
|
+ "' in Zotero.Item.attachmentSyncState setter");
|
|
}
|
|
|
|
if (val == this._attachmentSyncState) {
|
|
return;
|
|
}
|
|
|
|
if (!this._changedAttachmentData) {
|
|
this._changedAttachmentData = {};
|
|
}
|
|
this._changedAttachmentData.syncState = true;
|
|
this._attachmentSyncState = 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 {Number} File modification time as UNIX timestamp
|
|
*/
|
|
Zotero.Item.prototype.__defineGetter__('attachmentModificationTime', function () {
|
|
if (!this.isAttachment()) {
|
|
return undefined;
|
|
}
|
|
|
|
if (!this.id) {
|
|
return undefined;
|
|
}
|
|
|
|
var file = this.getFile();
|
|
if (!file) {
|
|
return undefined;
|
|
}
|
|
|
|
return Math.round(file.lastModifiedTime / 1000);
|
|
});
|
|
|
|
|
|
/**
|
|
* 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 {String} MD5 hash of file as hex string
|
|
*/
|
|
Zotero.Item.prototype.__defineGetter__('attachmentHash', function () {
|
|
if (!this.isAttachment()) {
|
|
return undefined;
|
|
}
|
|
|
|
if (!this.id) {
|
|
return undefined;
|
|
}
|
|
|
|
var file = this.getFile();
|
|
if (!file) {
|
|
return undefined;
|
|
}
|
|
|
|
return Zotero.Utilities.prototype.md5(file);
|
|
});
|
|
|
|
|
|
/**
|
|
* 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 {String} Attachment text, or empty string if unavailable
|
|
*/
|
|
Zotero.Item.prototype.__defineGetter__('attachmentText', function () {
|
|
if (!this.isAttachment()) {
|
|
return undefined;
|
|
}
|
|
|
|
if (!this.id) {
|
|
return null;
|
|
}
|
|
|
|
var file = this.getFile();
|
|
var cacheFile = Zotero.Fulltext.getItemCacheFile(this.id);
|
|
if (!file) {
|
|
if (cacheFile.exists()) {
|
|
var str = Zotero.File.getContents(cacheFile);
|
|
|
|
// TODO: remove post-Fx3.0
|
|
if (!str.trim) {
|
|
return Zotero.Utilities.prototype.trim(str);
|
|
}
|
|
|
|
return str.trim();
|
|
}
|
|
return '';
|
|
}
|
|
|
|
var mimeType = this.attachmentMIMEType;
|
|
if (!mimeType) {
|
|
mimeType = Zotero.MIME.getMIMETypeFromFile(file);
|
|
if (mimeType) {
|
|
this.attachmentMIMEType = mimeType;
|
|
this.save();
|
|
}
|
|
}
|
|
|
|
var str;
|
|
if (Zotero.Fulltext.isCachedMIMEType(mimeType)) {
|
|
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 (!Zotero.Fulltext.isFullyIndexed(this.id)) {
|
|
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 '';
|
|
}
|
|
Zotero.Fulltext.indexItems(this.id, false);
|
|
}
|
|
|
|
if (!cacheFile.exists()) {
|
|
Zotero.debug("Cache file doesn't exist after indexing -- returning empty .attachmentText");
|
|
return '';
|
|
}
|
|
str = Zotero.File.getContents(cacheFile);
|
|
}
|
|
|
|
else if (mimeType == 'text/html') {
|
|
str = Zotero.File.getContents(file);
|
|
str = Zotero.Utilities.prototype.unescapeHTML(str);
|
|
}
|
|
|
|
else if (mimeType == 'text/plain') {
|
|
str = Zotero.File.getContents(file);
|
|
}
|
|
|
|
else {
|
|
return '';
|
|
}
|
|
|
|
// TODO: remove post-Fx3.0
|
|
if (!str.trim) {
|
|
return Zotero.Utilities.prototype.trim(str);
|
|
}
|
|
|
|
return str.trim();
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
* Returns child attachments of this item
|
|
*
|
|
* @param {Boolean} includeTrashed Include trashed child items
|
|
* @return {Integer[]} Array of itemIDs, or FALSE if none
|
|
*/
|
|
Zotero.Item.prototype.getAttachments = function(includeTrashed) {
|
|
if (this.isAttachment()) {
|
|
throw ("getAttachments() cannot be called on attachment items");
|
|
}
|
|
|
|
if (!this.id) {
|
|
return [];
|
|
}
|
|
|
|
var sql = "SELECT A.itemID, value AS title FROM itemAttachments A "
|
|
+ "NATURAL JOIN items I LEFT JOIN itemData ID "
|
|
+ "ON (fieldID=110 AND A.itemID=ID.itemID) "
|
|
+ "LEFT JOIN itemDataValues IDV "
|
|
+ "ON (ID.valueID=IDV.valueID) "
|
|
+ "WHERE sourceItemID=?";
|
|
if (!includeTrashed) {
|
|
sql += " AND A.itemID NOT IN (SELECT itemID FROM deletedItems)";
|
|
}
|
|
|
|
if (Zotero.Prefs.get('sortAttachmentsChronologically')) {
|
|
sql += " ORDER BY dateAdded";
|
|
return Zotero.DB.columnQuery(sql, this.id);
|
|
}
|
|
|
|
var attachments = Zotero.DB.query(sql, this.id);
|
|
if (!attachments) {
|
|
return false;
|
|
}
|
|
|
|
// Sort by title
|
|
var collation = Zotero.getLocaleCollation();
|
|
var f = function (a, b) {
|
|
return collation.compareString(1, a.title, b.title);
|
|
}
|
|
|
|
var attachmentIDs = [];
|
|
attachments.sort(f);
|
|
for each(var attachment in attachments) {
|
|
attachmentIDs.push(attachment.itemID);
|
|
}
|
|
return attachmentIDs;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.getBestSnapshot = function () {
|
|
var msg = "Zotero.Item.getBestSnapshot() is deprecated -- use getBestAttachment";
|
|
Zotero.debug(msg, 2);
|
|
Components.utils.reportError(msg);
|
|
return this.getBestAttachment();
|
|
}
|
|
|
|
|
|
/*
|
|
* 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 {Integer} itemID for attachment
|
|
*/
|
|
Zotero.Item.prototype.getBestAttachment = function() {
|
|
if (!this.isRegularItem()) {
|
|
throw ("getBestAttachment() 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 sourceItemID=? AND linkMode NOT IN (?) "
|
|
+ "AND IA.itemID NOT IN (SELECT itemID FROM deletedItems) "
|
|
+ "ORDER BY value=? DESC, mimeType='application/pdf' DESC, dateAdded ASC";
|
|
return Zotero.DB.valueQuery(sql, [this.id, Zotero.Attachments.LINK_MODE_LINKED_URL, url]);
|
|
}
|
|
|
|
|
|
//
|
|
// Methods dealing with item tags
|
|
//
|
|
// save() is not required for tag functions
|
|
//
|
|
Zotero.Item.prototype.addTag = function(name, type) {
|
|
if (!this.id) {
|
|
throw ('Cannot add tag to unsaved item in Item.addTag()');
|
|
}
|
|
|
|
name = Zotero.Utilities.prototype.trim(name);
|
|
|
|
if (!name) {
|
|
Zotero.debug('Not saving empty tag in Item.addTag()', 2);
|
|
return false;
|
|
}
|
|
|
|
if (!type) {
|
|
type = 0;
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
try {
|
|
|
|
var matchingTags = Zotero.Tags.getIDs(name, this.libraryID);
|
|
var itemTags = this.getTags();
|
|
if (matchingTags && itemTags) {
|
|
for each(var id in matchingTags) {
|
|
if (itemTags.indexOf(id) != -1) {
|
|
var tag = Zotero.Tags.get(id);
|
|
// If existing automatic and adding identical user,
|
|
// remove automatic
|
|
if (type == 0 && tag.type == 1) {
|
|
this.removeTag(id);
|
|
break;
|
|
}
|
|
// If existing user and adding automatic, skip
|
|
else if (type == 1 && tag.type == 0) {
|
|
Zotero.debug("Identical user tag '" + name
|
|
+ "' already exists -- skipping automatic tag");
|
|
Zotero.DB.commitTransaction();
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var tagID = Zotero.Tags.getID(name, type, this.libraryID);
|
|
if (!tagID) {
|
|
var tag = new Zotero.Tag;
|
|
tag.libraryID = this.libraryID ? this.libraryID : null;
|
|
tag.name = name;
|
|
tag.type = type;
|
|
var tagID = tag.save();
|
|
}
|
|
|
|
var added = this.addTagByID(tagID);
|
|
Zotero.DB.commitTransaction();
|
|
return added ? tagID : false;
|
|
|
|
}
|
|
catch (e) {
|
|
Zotero.debug(e);
|
|
Zotero.DB.rollbackTransaction();
|
|
throw (e);
|
|
}
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.addTags = function (tags, type) {
|
|
Zotero.DB.beginTransaction();
|
|
try {
|
|
for each(var tag in tags) {
|
|
this.addTag(tag, type);
|
|
}
|
|
Zotero.DB.commitTransaction();
|
|
}
|
|
catch (e) {
|
|
Zotero.DB.rollbackTransaction();
|
|
throw (e);
|
|
}
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.addTagByID = function(tagID) {
|
|
if (!this.id) {
|
|
throw ('Cannot add tag to unsaved item in Zotero.Item.addTagByID()');
|
|
}
|
|
|
|
if (!tagID) {
|
|
throw ('tagID not provided in Zotero.Item.addTagByID()');
|
|
}
|
|
|
|
var tag = Zotero.Tags.get(tagID);
|
|
if (!tag) {
|
|
throw ('Cannot add invalid tag ' + tagID + ' in Zotero.Item.addTagByID()');
|
|
}
|
|
|
|
var added = tag.addItem(this.id);
|
|
if (!added) {
|
|
return false;
|
|
}
|
|
tag.save();
|
|
return true;
|
|
}
|
|
|
|
Zotero.Item.prototype.hasTag = function(tagID) {
|
|
return this.hasTags(tagID);
|
|
}
|
|
|
|
/*
|
|
* Returns true if the item has one or more of |tagIDs|
|
|
*
|
|
* |tagIDs| can be an int or array of ints
|
|
*/
|
|
Zotero.Item.prototype.hasTags = function(tagIDs) {
|
|
var tagIDs = Zotero.flattenArguments(tagIDs);
|
|
|
|
var sql = "SELECT COUNT(*) FROM itemTags WHERE itemID=? AND tagID IN ("
|
|
+ tagIDs.map(function () '?').join() + ")";
|
|
return !!Zotero.DB.valueQuery(sql, [this.id].concat(tagIDs));
|
|
}
|
|
|
|
/**
|
|
* Returns all tags assigned to an item
|
|
*
|
|
* @return array Array of Zotero.Tag objects
|
|
*/
|
|
Zotero.Item.prototype.getTags = function() {
|
|
if (!this.id) {
|
|
return false;
|
|
}
|
|
var sql = "SELECT tagID, name FROM tags WHERE tagID IN "
|
|
+ "(SELECT tagID FROM itemTags WHERE itemID=?)";
|
|
var tags = Zotero.DB.query(sql, this.id);
|
|
if (!tags) {
|
|
return false;
|
|
}
|
|
|
|
var collation = Zotero.getLocaleCollation();
|
|
tags.sort(function(a, b) {
|
|
return collation.compareString(1, a.name, b.name);
|
|
});
|
|
|
|
var tagObjs = [];
|
|
for (var i=0; i<tags.length; i++) {
|
|
var tag = Zotero.Tags.get(tags[i].tagID);
|
|
tagObjs.push(tag);
|
|
}
|
|
return tagObjs;
|
|
}
|
|
|
|
Zotero.Item.prototype.getTagIDs = function() {
|
|
var sql = "SELECT tagID FROM itemTags WHERE itemID=?";
|
|
return Zotero.DB.columnQuery(sql, this.id);
|
|
}
|
|
|
|
Zotero.Item.prototype.replaceTag = function(oldTagID, newTag) {
|
|
if (!this.id) {
|
|
throw ('Cannot replace tag on unsaved item');
|
|
}
|
|
|
|
newTag = Zotero.Utilities.prototype.trim(newTag);
|
|
|
|
if (!newTag) {
|
|
Zotero.debug('Not replacing with empty tag', 2);
|
|
return false;
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
|
|
var oldTag = Zotero.Tags.getName(oldTagID);
|
|
if (oldTag==newTag) {
|
|
Zotero.DB.commitTransaction();
|
|
return false;
|
|
}
|
|
|
|
this.removeTag(oldTagID);
|
|
var id = this.addTag(newTag);
|
|
Zotero.DB.commitTransaction();
|
|
Zotero.Notifier.trigger('modify', 'item', this.id);
|
|
Zotero.Notifier.trigger('remove', 'item-tag', this.id + '-' + oldTagID);
|
|
Zotero.Notifier.trigger('add', 'item-tag', this.id + '-' + id);
|
|
return id;
|
|
}
|
|
|
|
Zotero.Item.prototype.removeTag = function(tagID) {
|
|
if (!this.id) {
|
|
throw ('Cannot remove tag on unsaved item in Zotero.Item.removeTag()');
|
|
}
|
|
|
|
if (!tagID) {
|
|
throw ('tagID not provided in Zotero.Item.removeTag()');
|
|
}
|
|
|
|
var tag = Zotero.Tags.get(tagID);
|
|
if (!tag) {
|
|
throw ('Cannot remove invalid tag ' + tagID + ' in Zotero.Item.removeTag()');
|
|
}
|
|
|
|
tag.removeItem(this.id);
|
|
tag.save();
|
|
|
|
if (!tag.countLinkedItems()) {
|
|
Zotero.Prefs.set('purge.tags', true);
|
|
}
|
|
}
|
|
|
|
Zotero.Item.prototype.removeAllTags = function() {
|
|
if (!this.id) {
|
|
throw ('Cannot remove tags on unsaved item');
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
var tagIDs = this.getTagIDs();
|
|
if (!tagIDs) {
|
|
Zotero.DB.commitTransaction();
|
|
return;
|
|
}
|
|
|
|
Zotero.DB.query("DELETE FROM itemTags WHERE itemID=?", this.id);
|
|
Zotero.Tags.purge();
|
|
Zotero.DB.commitTransaction();
|
|
Zotero.Notifier.trigger('modify', 'item', this.id);
|
|
|
|
for (var i in tagIDs) {
|
|
tagIDs[i] = this.id + '-' + tagIDs[i];
|
|
}
|
|
Zotero.Notifier.trigger('remove', 'item-tag', tagIDs);
|
|
}
|
|
|
|
|
|
/**
|
|
* Return an item in the specified library equivalent to this item
|
|
*/
|
|
Zotero.Item.prototype.getLinkedItem = function (libraryID) {
|
|
if (libraryID == this.libraryID) {
|
|
throw ("Item is already in library " + libraryID + " in Zotero.Item.getLinkedItem()");
|
|
}
|
|
|
|
var predicate = Zotero.Items.linkedItemPredicate;
|
|
var itemURI = Zotero.URI.getItemURI(this);
|
|
var links = Zotero.Relations.getObject(itemURI, predicate, false).concat(
|
|
Zotero.Relations.getSubject(false, predicate, itemURI)
|
|
);
|
|
Zotero.debug(links);
|
|
if (!links.length) {
|
|
return false;
|
|
}
|
|
|
|
if (libraryID) {
|
|
var libraryItemPrefix = Zotero.URI.getLibraryURI(libraryID) + "/items/";
|
|
}
|
|
else {
|
|
var libraryItemPrefix = Zotero.URI.getCurrentUserURI() + "/items/";
|
|
}
|
|
for each(var link in links) {
|
|
if (link.indexOf(libraryItemPrefix) == 0) {
|
|
var item = Zotero.URI.getURIItem(link);
|
|
if (!item) {
|
|
Zotero.debug("Referenced linked item '" + link + "' not found in Zotero.Item.getLinkedItem()", 2);
|
|
continue;
|
|
}
|
|
return item;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.addLinkedItem = function (item) {
|
|
var url1 = Zotero.URI.getItemURI(this);
|
|
var url2 = Zotero.URI.getItemURI(item);
|
|
var predicate = Zotero.Items.linkedItemPredicate;
|
|
if (Zotero.Relations.getByURIs(url1, predicate, url2).length
|
|
|| Zotero.Relations.getByURIs(url2, predicate, url1).length) {
|
|
Zotero.debug("Items " + this.key + " and " + item.key + " are already linked");
|
|
return false;
|
|
}
|
|
Zotero.Relations.add(null, url1, predicate, url2);
|
|
}
|
|
|
|
|
|
|
|
|
|
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.attachmentMIMEType == '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);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* 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 = Zotero.Items.diff(thisData, otherData, diff, includeMatches);
|
|
|
|
diff[0].creators = [];
|
|
diff[1].creators = [];
|
|
// TODO: creators?
|
|
// TODO: tags?
|
|
// TODO: related?
|
|
// TODO: annotations
|
|
|
|
var changed = false;
|
|
|
|
changed = thisData.sourceItemKey != otherData.sourceItemKey;
|
|
if (includeMatches || changed) {
|
|
diff[0].sourceItemKey = thisData.sourceItemKey;
|
|
diff[1].sourceItemKey = otherData.sourceItemKey;
|
|
|
|
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) {
|
|
changed = thisData.note != otherData.note;
|
|
if (includeMatches || changed) {
|
|
diff[0].note = thisData.note;
|
|
diff[1].note = otherData.note;
|
|
}
|
|
|
|
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 each(var field in ignoreFields) {
|
|
if (diff[0].primary[field] != undefined) {
|
|
realDiffs--;
|
|
if (realDiffs == 0) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return diff;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns an unsaved copy of the item
|
|
*
|
|
* @param {Boolean} [includePrimary=false]
|
|
* @param {Zotero.Item} [newItem=null] Target item for clone (used to pass a saved
|
|
* item for duplicating items with tags)
|
|
* @param {Boolean} [unsaved=false] Skip properties that require a saved object (e.g., tags)
|
|
*/
|
|
Zotero.Item.prototype.clone = function(includePrimary, newItem, unsaved) {
|
|
Zotero.debug('Cloning item ' + this.id);
|
|
|
|
if (includePrimary && newItem) {
|
|
throw ("includePrimary and newItem parameters are mutually exclusive in Zotero.Item.clone()");
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
|
|
var obj = this.serialize();
|
|
|
|
var itemTypeID = this.itemTypeID;
|
|
|
|
if (newItem) {
|
|
var sameLibrary = newItem.libraryID == this.libraryID;
|
|
}
|
|
else {
|
|
var newItem = new Zotero.Item(itemTypeID);
|
|
var sameLibrary = true;
|
|
|
|
if (includePrimary) {
|
|
newItem.id = this.id;
|
|
newItem.libraryID = this.libraryID;
|
|
newItem.key = this.key;
|
|
for (var field in obj.primary) {
|
|
switch (field) {
|
|
case 'itemID':
|
|
case 'itemType':
|
|
case 'libraryID':
|
|
case 'key':
|
|
continue;
|
|
}
|
|
newItem.setField(field, obj.primary[field]);
|
|
}
|
|
}
|
|
}
|
|
|
|
var changedFields = {};
|
|
for (var field in obj.fields) {
|
|
var fieldID = Zotero.ItemFields.getID(field);
|
|
if (fieldID && Zotero.ItemFields.isValidForType(fieldID, itemTypeID)) {
|
|
newItem.setField(field, obj.fields[field]);
|
|
changedFields[field] = true;
|
|
}
|
|
}
|
|
// If modifying an existing item, clear other fields not in the cloned item
|
|
if (newItem) {
|
|
var previousFields = this.getUsedFields(true);
|
|
for each(var field in previousFields) {
|
|
if (!changedFields[field] && Zotero.ItemFields.isValidForType(field, itemTypeID)) {
|
|
newItem.setField(field, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Regular item
|
|
if (this.isRegularItem()) {
|
|
if (includePrimary) {
|
|
// newItem = loaded from db
|
|
// obj = in-memory
|
|
var max = Math.max(newItem.numCreators(), this.numCreators());
|
|
var deleteOffset = 0;
|
|
for (var i=0; i<max; i++) {
|
|
var newIndex = i - deleteOffset;
|
|
|
|
// Remove existing creators (loaded because we set the itemID
|
|
// above) not in the in-memory version
|
|
if (!obj.creators[i]) {
|
|
if (newItem.getCreator(newIndex)) {
|
|
newItem.removeCreator(newIndex);
|
|
deleteOffset++;
|
|
}
|
|
continue;
|
|
}
|
|
// Add in-memory creators
|
|
newItem.setCreator(
|
|
newIndex, this.getCreator(i).ref, obj.creators[i].creatorType
|
|
);
|
|
}
|
|
}
|
|
else {
|
|
// If overwriting an existing item, clear existing creators
|
|
if (newItem) {
|
|
for (var i=newItem.numCreators()-1; i>=0; i--) {
|
|
if (newItem.getCreator(i)) {
|
|
newItem.removeCreator(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
var i = 0;
|
|
for (var c in obj.creators) {
|
|
var creator = this.getCreator(c).ref;
|
|
var creatorTypeID = this.getCreator(c).creatorTypeID;
|
|
|
|
if (!sameLibrary) {
|
|
var creatorDataID = Zotero.Creators.getDataID(this.getCreator(c).ref);
|
|
var creatorIDs = Zotero.Creators.getCreatorsWithData(creatorDataID, newItem.libraryID);
|
|
if (creatorIDs) {
|
|
// TODO: support multiple creators?
|
|
var creator = Zotero.Creators.get(creatorIDs[0]);
|
|
}
|
|
else {
|
|
var newCreator = new Zotero.Creator;
|
|
newCreator.libraryID = newItem.libraryID;
|
|
newCreator.setFields(creator);
|
|
var creator = newCreator;
|
|
}
|
|
|
|
var creatorTypeID = this.getCreator(c).creatorTypeID;
|
|
}
|
|
|
|
newItem.setCreator(i, creator, creatorTypeID);
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
newItem.setNote(this.getNote());
|
|
if (sameLibrary) {
|
|
var parent = this.getSourceKey();
|
|
if (parent) {
|
|
newItem.setSourceKey(parent);
|
|
}
|
|
}
|
|
|
|
if (this.isAttachment()) {
|
|
newItem.attachmentLinkMode = this.attachmentLinkMode;
|
|
newItem.attachmentMIMEType = this.attachmentMIMEType;
|
|
newItem.attachmentCharset = this.attachmentCharset;
|
|
if (sameLibrary) {
|
|
if (this.attachmentPath) {
|
|
newItem.attachmentPath = this.attachmentPath;
|
|
}
|
|
if (this.attachmentSyncState) {
|
|
newItem.attachmentSyncState = this.attachmentSyncState;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!unsaved && obj.tags) {
|
|
for each(var tag in obj.tags) {
|
|
if (sameLibrary) {
|
|
newItem.addTagByID(tag.primary.tagID);
|
|
}
|
|
else {
|
|
newItem.addTag(tag.fields.name, tag.fields.type);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (obj.related && sameLibrary) {
|
|
// DEBUG: this will add reverse-only relateds too
|
|
newItem.relatedItems = obj.related;
|
|
}
|
|
|
|
Zotero.DB.commitTransaction();
|
|
|
|
return newItem;
|
|
}
|
|
|
|
|
|
/**
|
|
* Delete item from database and clear from Zotero.Items internal array
|
|
*
|
|
* Items.erase() should be used instead of this
|
|
*/
|
|
Zotero.Item.prototype.erase = function() {
|
|
if (!this.id) {
|
|
return false;
|
|
}
|
|
|
|
Zotero.debug('Deleting item ' + this.id);
|
|
|
|
var changedItems = [];
|
|
var changedItemsNotifierData = {};
|
|
|
|
Zotero.DB.beginTransaction();
|
|
|
|
var deletedItemNotifierData = {};
|
|
deletedItemNotifierData[this.id] = { old: this.serialize() };
|
|
|
|
// Remove group item metadata
|
|
if (this.libraryID) {
|
|
var sql = "DELETE FROM groupItems WHERE itemID=?";
|
|
Zotero.DB.query(sql, this.id);
|
|
}
|
|
|
|
// Remove item from parent collections
|
|
var parentCollectionIDs = this.getCollections();
|
|
if (parentCollectionIDs) {
|
|
for (var i=0; i<parentCollectionIDs.length; i++) {
|
|
Zotero.Collections.get(parentCollectionIDs[i]).removeItem(this.id);
|
|
}
|
|
}
|
|
|
|
// Note
|
|
if (this.isNote()) {
|
|
// Decrement note count of source items
|
|
var sql = "SELECT sourceItemID FROM itemNotes WHERE itemID=" + this.id;
|
|
var sourceItemID = Zotero.DB.valueQuery(sql);
|
|
if (sourceItemID) {
|
|
var sourceItem = Zotero.Items.get(sourceItemID);
|
|
changedItemsNotifierData[sourceItem.id] = { old: sourceItem.serialize() };
|
|
if (!this.deleted) {
|
|
sourceItem.decrementNoteCount();
|
|
}
|
|
changedItems.push(sourceItemID);
|
|
}
|
|
}
|
|
// Attachment
|
|
else if (this.isAttachment()) {
|
|
// Decrement file count of source items
|
|
var sql = "SELECT sourceItemID FROM itemAttachments WHERE itemID=" + this.id;
|
|
var sourceItemID = Zotero.DB.valueQuery(sql);
|
|
if (sourceItemID) {
|
|
var sourceItem = Zotero.Items.get(sourceItemID);
|
|
changedItemsNotifierData[sourceItem.id] = { old: sourceItem.serialize() };
|
|
if (!this.deleted) {
|
|
sourceItem.decrementAttachmentCount();
|
|
}
|
|
changedItems.push(sourceItemID);
|
|
}
|
|
|
|
// Delete associated files
|
|
var linkMode = this.getAttachmentLinkMode();
|
|
switch (linkMode) {
|
|
// Link only -- nothing to delete
|
|
case Zotero.Attachments.LINK_MODE_LINKED_URL:
|
|
break;
|
|
default:
|
|
try {
|
|
var file = Zotero.Attachments.getStorageDirectory(this.id);
|
|
if (file.exists()) {
|
|
file.remove(true);
|
|
}
|
|
}
|
|
catch (e) {
|
|
Components.utils.reportError(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Regular item
|
|
|
|
// If flag given, delete child notes and files
|
|
else {
|
|
var sql = "SELECT itemID FROM itemNotes WHERE sourceItemID=?1 UNION "
|
|
+ "SELECT itemID FROM itemAttachments WHERE sourceItemID=?1";
|
|
var toDelete = Zotero.DB.columnQuery(sql, [this.id]);
|
|
|
|
if (toDelete) {
|
|
for (var i in toDelete) {
|
|
var obj = Zotero.Items.get(toDelete[i]);
|
|
obj.erase();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Flag related items for notification
|
|
var relateds = this._getRelatedItemsBidirectional();
|
|
if (relateds) {
|
|
for each(var id in relateds) {
|
|
var relatedItem = Zotero.Items.get(id);
|
|
if (changedItems.indexOf(id) != -1) {
|
|
changedItemsNotifierData[id] = { old: relatedItem.serialize() };
|
|
changedItems.push(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear fulltext cache
|
|
if (this.isAttachment()) {
|
|
Zotero.Fulltext.clearItemWords(this.id);
|
|
//Zotero.Fulltext.clearItemContent(this.id);
|
|
}
|
|
|
|
|
|
Zotero.DB.query('DELETE FROM annotations WHERE itemID=?', this.id);
|
|
Zotero.DB.query('DELETE FROM highlights WHERE itemID=?', this.id);
|
|
Zotero.DB.query('DELETE FROM deletedItems WHERE itemID=?', this.id);
|
|
var hasCreators = Zotero.DB.valueQuery(
|
|
"SELECT rowid FROM itemCreators WHERE itemID=? LIMIT 1", this.id
|
|
);
|
|
if (hasCreators) {
|
|
Zotero.DB.query('DELETE FROM itemCreators WHERE itemID=?', this.id);
|
|
}
|
|
Zotero.DB.query('DELETE FROM itemNotes WHERE itemID=?', this.id);
|
|
Zotero.DB.query('DELETE FROM itemAttachments WHERE itemID=?', this.id);
|
|
Zotero.DB.query('DELETE FROM itemSeeAlso WHERE itemID=?', this.id);
|
|
Zotero.DB.query('DELETE FROM itemSeeAlso WHERE linkedItemID=?', this.id);
|
|
|
|
var tags = this.getTags();
|
|
if (tags) {
|
|
var hasTags = true;
|
|
Zotero.DB.query('DELETE FROM itemTags WHERE itemID=?', this.id);
|
|
// DEBUG: Hack to reload linked items -- replace with something better
|
|
for each(var tag in tags) {
|
|
tag._linkedItemsLoaded = false;
|
|
}
|
|
}
|
|
else {
|
|
var hasTags = false;
|
|
}
|
|
|
|
Zotero.DB.query('DELETE FROM itemData WHERE itemID=?', this.id);
|
|
Zotero.DB.query('DELETE FROM items WHERE itemID=?', this.id);
|
|
|
|
try {
|
|
Zotero.DB.commitTransaction();
|
|
}
|
|
catch (e) {
|
|
// On failure, reset count of source items
|
|
if (sourceItem) {
|
|
if (this.isNote()) {
|
|
sourceItem.incrementNoteCount();
|
|
}
|
|
else if (this.isAttachment()) {
|
|
sourceItem.incrementAttachmentCount();
|
|
}
|
|
}
|
|
Zotero.DB.rollbackTransaction();
|
|
throw (e);
|
|
}
|
|
|
|
Zotero.Items.unload(this.id);
|
|
|
|
// Send notification of changed items
|
|
if (changedItems.length) {
|
|
Zotero.Notifier.trigger('modify', 'item', changedItems, changedItemsNotifierData);
|
|
}
|
|
|
|
Zotero.Notifier.trigger('delete', 'item', this.id, deletedItemNotifierData);
|
|
|
|
Zotero.Prefs.set('purge.items', true);
|
|
if (hasCreators) {
|
|
Zotero.Prefs.set('purge.creators', true);
|
|
}
|
|
if (hasTags) {
|
|
Zotero.Prefs.set('purge.tags', true);
|
|
}
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.isCollection = function() {
|
|
return false;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.toArray = function (mode) {
|
|
Zotero.debug('Zotero.Item.toArray() is deprecated -- use Zotero.Item.serialize()');
|
|
|
|
if (this.id || this.key) {
|
|
if (!this._primaryDataLoaded) {
|
|
this.loadPrimaryData(true);
|
|
}
|
|
if (!this._itemDataLoaded) {
|
|
this._loadItemData();
|
|
}
|
|
}
|
|
|
|
var arr = {};
|
|
|
|
// Primary fields
|
|
for each(var i in Zotero.Items.primaryFields) {
|
|
switch (i) {
|
|
case 'itemID':
|
|
arr.itemID = this._id;
|
|
continue;
|
|
|
|
case 'itemTypeID':
|
|
arr.itemType = Zotero.ItemTypes.getName(this.itemTypeID);
|
|
continue;
|
|
|
|
// Skip virtual fields
|
|
case 'firstCreator':
|
|
case 'numNotes':
|
|
case 'numAttachments':
|
|
continue;
|
|
|
|
// For the rest, just copy over
|
|
default:
|
|
arr[i] = this['_' + i];
|
|
}
|
|
}
|
|
|
|
// Item metadata
|
|
for (var i in this._itemData) {
|
|
arr[Zotero.ItemFields.getName(i)] = this._itemData[i] ? this._itemData[i] + '': '';
|
|
}
|
|
|
|
if (!arr.title) {
|
|
var titleFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(this.itemTypeID, 'title');
|
|
var titleFieldName = Zotero.ItemFields.getName(titleFieldID);
|
|
if (arr[titleFieldName]) {
|
|
arr.title = titleFieldName;
|
|
delete arr[titleFieldName];
|
|
}
|
|
|
|
switch (this.typeID) {
|
|
case Zotero.ItemTypes.getID('note'):
|
|
break;
|
|
|
|
default:
|
|
arr.title = this.getDisplayTitle(mode == 2) + '';
|
|
}
|
|
}
|
|
|
|
if (!this.isNote() && !this.isAttachment()) {
|
|
// Creators
|
|
arr.creators = [];
|
|
var creators = this.getCreators();
|
|
for (var i in creators) {
|
|
var creator = {};
|
|
// Convert creatorTypeIDs to text
|
|
creator.creatorType =
|
|
Zotero.CreatorTypes.getName(creators[i].creatorTypeID);
|
|
creator.creatorID = creators[i].ref.id;
|
|
creator.firstName = creators[i].ref.firstName;
|
|
creator.lastName = creators[i].ref.lastName;
|
|
creator.fieldMode = creators[i].ref.fieldMode;
|
|
arr.creators.push(creator);
|
|
}
|
|
}
|
|
|
|
// Notes
|
|
if (this.isNote()) {
|
|
arr.note = this.getNote();
|
|
var parent = this.getSourceKey();
|
|
if (parent) {
|
|
arr.sourceItemKey = parent;
|
|
}
|
|
}
|
|
|
|
// Attachments
|
|
if (this.isAttachment()) {
|
|
// Attachments can have embedded notes
|
|
arr.note = this.getNote();
|
|
|
|
var parent = this.getSourceKey();
|
|
if (parent) {
|
|
arr.sourceItemKey = parent;
|
|
}
|
|
}
|
|
|
|
// Attach children of regular items
|
|
if (this.isRegularItem()) {
|
|
// Append attached notes
|
|
arr.notes = [];
|
|
var notes = this.getNotes();
|
|
for (var i in notes) {
|
|
var note = Zotero.Items.get(notes[i]);
|
|
arr.notes.push(note.toArray());
|
|
}
|
|
|
|
arr.attachments = [];
|
|
var attachments = this.getAttachments();
|
|
for (var i in attachments) {
|
|
var attachment = Zotero.Items.get(attachments[i]);
|
|
arr.attachments.push(attachment.toArray());
|
|
}
|
|
}
|
|
|
|
arr.tags = [];
|
|
var tags = this.getTags();
|
|
if (tags) {
|
|
for (var i=0; i<tags.length; i++) {
|
|
var tag = tags[i].serialize();
|
|
tag.tag = tag.fields.name;
|
|
tag.type = tag.fields.type;
|
|
arr.tags.push(tag);
|
|
}
|
|
}
|
|
|
|
arr.related = this._getRelatedItemsBidirectional();
|
|
if (!arr.related) {
|
|
arr.related = [];
|
|
}
|
|
|
|
return arr;
|
|
}
|
|
|
|
/*
|
|
* Convert the item object into a persistent form
|
|
* for use by the export functions
|
|
*
|
|
* Modes:
|
|
*
|
|
* 1 == e.g. [Letter to Valee]
|
|
* 2 == e.g. [Stothard; Letter to Valee; May 8, 1928]
|
|
*/
|
|
Zotero.Item.prototype.serialize = function(mode) {
|
|
if (this.id || this.key) {
|
|
if (!this._primaryDataLoaded) {
|
|
this.loadPrimaryData(true);
|
|
}
|
|
if (!this._itemDataLoaded) {
|
|
this._loadItemData();
|
|
}
|
|
}
|
|
|
|
var arr = {};
|
|
arr.primary = {};
|
|
arr.virtual = {};
|
|
arr.fields = {};
|
|
|
|
// Primary and virtual fields
|
|
for each(var i in Zotero.Items.primaryFields) {
|
|
switch (i) {
|
|
case 'itemID':
|
|
arr.primary.itemID = this._id;
|
|
continue;
|
|
|
|
case 'itemTypeID':
|
|
arr.primary.itemType = Zotero.ItemTypes.getName(this.itemTypeID);
|
|
continue;
|
|
|
|
case 'firstCreator':
|
|
arr.virtual[i] = this['_' + i] + '';
|
|
continue;
|
|
|
|
case 'numNotes':
|
|
case 'numAttachments':
|
|
arr.virtual[i] = this['_' + i];
|
|
continue;
|
|
|
|
// For the rest, just copy over
|
|
default:
|
|
arr.primary[i] = this['_' + i];
|
|
}
|
|
}
|
|
|
|
// Item metadata
|
|
for (var i in this._itemData) {
|
|
arr.fields[Zotero.ItemFields.getName(i)] = this._itemData[i] ? this._itemData[i] + '' : '';
|
|
}
|
|
|
|
if (mode == 1 || mode == 2) {
|
|
if (!arr.fields.title &&
|
|
(this.itemTypeID == Zotero.ItemTypes.getID('letter') ||
|
|
this.itemTypeID == Zotero.ItemTypes.getID('interview'))) {
|
|
arr.fields.title = this.getDisplayTitle(mode == 2) + '';
|
|
}
|
|
}
|
|
|
|
// Deleted items flag
|
|
if (this.deleted) {
|
|
arr.deleted = true;
|
|
}
|
|
|
|
if (this.isRegularItem()) {
|
|
// Creators
|
|
arr.creators = [];
|
|
var creators = this.getCreators();
|
|
for (var i in creators) {
|
|
var creator = {};
|
|
// Convert creatorTypeIDs to text
|
|
creator.creatorType = Zotero.CreatorTypes.getName(creators[i].creatorTypeID);
|
|
creator.creatorID = creators[i].ref.id;
|
|
creator.firstName = creators[i].ref.firstName;
|
|
creator.lastName = creators[i].ref.lastName;
|
|
creator.fieldMode = creators[i].ref.fieldMode;
|
|
creator.libraryID = creators[i].ref.libraryID;
|
|
creator.key = creators[i].ref.key;
|
|
arr.creators.push(creator);
|
|
}
|
|
|
|
// Attach children of regular items
|
|
|
|
// Append attached notes
|
|
arr.notes = [];
|
|
var notes = this.getNotes();
|
|
for (var i in notes) {
|
|
var note = Zotero.Items.get(notes[i]);
|
|
arr.notes.push(note.serialize());
|
|
}
|
|
|
|
// Append attachments
|
|
arr.attachments = [];
|
|
var attachments = this.getAttachments();
|
|
for (var i in attachments) {
|
|
var attachment = Zotero.Items.get(attachments[i]);
|
|
arr.attachments.push(attachment.serialize());
|
|
}
|
|
}
|
|
// Notes and embedded attachment notes
|
|
else {
|
|
if (this.isAttachment()) {
|
|
arr.attachment = {};
|
|
arr.attachment.linkMode = this.attachmentLinkMode;
|
|
arr.attachment.mimeType = this.attachmentMIMEType;
|
|
var charsetID = this.attachmentCharset;
|
|
arr.attachment.charset = Zotero.CharacterSets.getName(charsetID);
|
|
arr.attachment.path = this.attachmentPath;
|
|
}
|
|
|
|
arr.note = this.getNote();
|
|
var parent = this.getSourceKey();
|
|
if (parent) {
|
|
arr.sourceItemKey = parent;
|
|
}
|
|
}
|
|
|
|
arr.tags = [];
|
|
var tags = this.getTags();
|
|
if (tags) {
|
|
for (var i=0; i<tags.length; i++) {
|
|
arr.tags.push(tags[i].serialize());
|
|
}
|
|
}
|
|
|
|
var related = this._getRelatedItems(true);
|
|
var reverse = this._getRelatedItemsReverse();
|
|
arr.related = related ? related : [];
|
|
arr.relatedReverse = reverse ? reverse : [];
|
|
|
|
return arr;
|
|
}
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Private Zotero.Item methods
|
|
//
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
* Load in the creators from the database
|
|
*/
|
|
Zotero.Item.prototype._loadCreators = function() {
|
|
if (!this.id) {
|
|
throw ('ItemID not set for item before attempting to load creators');
|
|
}
|
|
|
|
var sql = 'SELECT creatorID, creatorTypeID, orderIndex FROM itemCreators '
|
|
+ 'WHERE itemID=? ORDER BY orderIndex';
|
|
var creators = Zotero.DB.query(sql, this.id);
|
|
|
|
this._creators = [];
|
|
this._creatorsLoaded = true;
|
|
|
|
if (!creators) {
|
|
return true;
|
|
}
|
|
|
|
for (var i=0; i<creators.length; i++) {
|
|
var creatorObj = Zotero.Creators.get(creators[i].creatorID);
|
|
if (!creatorObj) {
|
|
creatorObj = new Zotero.Creator();
|
|
creatorObj.id = creators[i].creatorID;
|
|
}
|
|
this._creators[creators[i].orderIndex] = {
|
|
ref: creatorObj,
|
|
creatorTypeID: creators[i].creatorTypeID
|
|
};
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Load in the field data from the database
|
|
*/
|
|
Zotero.Item.prototype._loadItemData = function() {
|
|
if (!this.id) {
|
|
// Log backtrace and data
|
|
try {
|
|
asfasfsa();
|
|
}
|
|
catch (e) {
|
|
Zotero.debug(e);
|
|
Zotero.debug(this._itemTypeID);
|
|
Zotero.debug(this._libraryID);
|
|
Zotero.debug(this._key);
|
|
Zotero.debug(this._dateAdded);
|
|
Zotero.debug(this._dateModified);
|
|
}
|
|
throw ('ItemID not set for object before attempting to load data');
|
|
}
|
|
|
|
var sql = "SELECT fieldID, value FROM itemData NATURAL JOIN itemDataValues "
|
|
+ "WHERE itemID=?";
|
|
var fields = Zotero.DB.query(sql, this.id);
|
|
|
|
var itemTypeFields = Zotero.ItemFields.getItemTypeFields(this.itemTypeID);
|
|
|
|
for each(var field in fields) {
|
|
this.setField(field.fieldID, field.value, true);
|
|
}
|
|
|
|
// Mark nonexistent fields as loaded
|
|
for each(var fieldID in itemTypeFields) {
|
|
if (this._itemData[fieldID] === null) {
|
|
this._itemData[fieldID] = false;
|
|
}
|
|
}
|
|
|
|
this._itemDataLoaded = true;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype._loadRelatedItems = function() {
|
|
if (!this.id) {
|
|
return;
|
|
}
|
|
|
|
if (!this._primaryDataLoaded) {
|
|
this.loadPrimaryData(true);
|
|
}
|
|
|
|
var sql = "SELECT linkedItemID FROM itemSeeAlso WHERE itemID=?";
|
|
var ids = Zotero.DB.columnQuery(sql, this.id);
|
|
|
|
this._relatedItems = [];
|
|
|
|
if (ids) {
|
|
for each(var id in ids) {
|
|
this._relatedItems.push(Zotero.Items.get(id));
|
|
}
|
|
}
|
|
|
|
this._relatedItemsLoaded = true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns related items this item point to
|
|
*
|
|
* @param bool asIDs Return as itemIDs
|
|
* @return array Array of itemIDs, or FALSE if none
|
|
*/
|
|
Zotero.Item.prototype._getRelatedItems = function (asIDs) {
|
|
if (!this._relatedItemsLoaded) {
|
|
this._loadRelatedItems();
|
|
}
|
|
|
|
if (this._relatedItems.length == 0) {
|
|
return false;
|
|
}
|
|
|
|
// Return itemIDs
|
|
if (asIDs) {
|
|
var ids = [];
|
|
for each(var item in this._relatedItems) {
|
|
ids.push(item.id);
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
// Return Zotero.Item objects
|
|
var objs = [];
|
|
for each(var item in this._relatedItems) {
|
|
objs.push(item);
|
|
}
|
|
return objs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns related items that point to this item
|
|
*
|
|
* @return array Array of itemIDs, or FALSE if none
|
|
*/
|
|
Zotero.Item.prototype._getRelatedItemsReverse = function () {
|
|
if (!this.id) {
|
|
return false;
|
|
}
|
|
|
|
var sql = "SELECT itemID FROM itemSeeAlso WHERE linkedItemID=?";
|
|
return Zotero.DB.columnQuery(sql, this.id);
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns related items this item points to and that point to this item
|
|
*
|
|
* @return array|bool Array of itemIDs, or false if none
|
|
*/
|
|
Zotero.Item.prototype._getRelatedItemsBidirectional = function () {
|
|
var related = this._getRelatedItems(true);
|
|
var reverse = this._getRelatedItemsReverse();
|
|
if (reverse) {
|
|
if (!related) {
|
|
return reverse;
|
|
}
|
|
|
|
for each(var id in reverse) {
|
|
if (related.indexOf(id) == -1) {
|
|
related.push(id);
|
|
}
|
|
}
|
|
}
|
|
else if (!related) {
|
|
return false;
|
|
}
|
|
return related;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype._setRelatedItems = function (itemIDs) {
|
|
if (!this._relatedItemsLoaded) {
|
|
this._loadRelatedItems();
|
|
}
|
|
|
|
if (itemIDs.constructor.name != 'Array') {
|
|
throw ('ids must be an array in Zotero.Items._setRelatedItems()');
|
|
}
|
|
|
|
var currentIDs = this._getRelatedItems(true);
|
|
if (!currentIDs) {
|
|
currentIDs = [];
|
|
}
|
|
var oldIDs = []; // children being kept
|
|
var newIDs = []; // new children
|
|
|
|
if (itemIDs.length == 0) {
|
|
if (currentIDs.length == 0) {
|
|
Zotero.debug('No related items added', 4);
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
for (var i in itemIDs) {
|
|
var id = itemIDs[i];
|
|
var parsedInt = parseInt(id);
|
|
if (parsedInt != id) {
|
|
throw ("itemID '" + id + "' not an integer in Zotero.Item.addRelatedItem()");
|
|
}
|
|
id = parsedInt;
|
|
|
|
if (id == this.id) {
|
|
Zotero.debug("Can't relate item to itself in Zotero.Item._setRelatedItems()", 2);
|
|
continue;
|
|
}
|
|
|
|
if (currentIDs.indexOf(id) != -1) {
|
|
Zotero.debug("Item " + this.id + " is already related to item " + id);
|
|
oldIDs.push(id);
|
|
continue;
|
|
}
|
|
|
|
var item = Zotero.Items.get(id);
|
|
if (!item) {
|
|
throw ("Can't relate item to invalid item " + id
|
|
+ " in Zotero.Item._setRelatedItems()");
|
|
}
|
|
/*
|
|
var otherCurrent = item.relatedItems;
|
|
if (otherCurrent.length && otherCurrent.indexOf(this.id) != -1) {
|
|
Zotero.debug("Other item " + id + " already related to item "
|
|
+ this.id + " in Zotero.Item._setRelatedItems()");
|
|
return false;
|
|
}
|
|
*/
|
|
|
|
newIDs.push(id);
|
|
}
|
|
}
|
|
|
|
// Mark as changed if new or removed ids
|
|
if (newIDs.length > 0 || oldIDs.length != currentIDs.length) {
|
|
this._prepFieldChange('relatedItems');
|
|
}
|
|
else {
|
|
Zotero.debug('Related items not changed in Zotero.Item._setRelatedItems()', 4);
|
|
return false;
|
|
}
|
|
|
|
newIDs = oldIDs.concat(newIDs);
|
|
this._relatedItems = [];
|
|
for each(var itemID in newIDs) {
|
|
this._relatedItems.push(Zotero.Items.get(itemID));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// TODO: use for stuff other than related items
|
|
Zotero.Item.prototype._prepFieldChange = function (field) {
|
|
if (!this._changed) {
|
|
this._changed = {};
|
|
}
|
|
this._changed[field] = true;
|
|
|
|
// Save a copy of the data before changing
|
|
if (this.id && this.exists() && !this._previousData) {
|
|
this._previousData = this.serialize();
|
|
}
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype._generateKey = function () {
|
|
return Zotero.ID.getKey();
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype._disabledCheck = function () {
|
|
if (this._disabled) {
|
|
var msg = "New Zotero.Item objects shouldn't be accessed after save -- use Zotero.Items.get()";
|
|
Zotero.debug(msg, 2);
|
|
Components.utils.reportError(msg);
|
|
}
|
|
}
|