Remove some obsolete sync code

This commit is contained in:
Dan Stillman 2015-08-06 04:19:53 -04:00
parent 0aecaad761
commit b5b99672db

View file

@ -1483,22 +1483,6 @@ Zotero.Sync.Server = new function () {
_checkTimer = setTimeout(function () { callback(mode); }, wait);
return true;
}
function _invalidSession(xmlhttp) {
if (xmlhttp.responseXML.childNodes[0].firstChild.tagName != 'error') {
return false;
}
var code = xmlhttp.responseXML.childNodes[0].firstChild.getAttribute('code');
return (code == 'INVALID_SESSION_ID') || (code == 'SESSION_TIMED_OUT');
}
}
@ -1541,98 +1525,6 @@ Zotero.BufferedInputListener.prototype = {
}
/**
* Stores information about a sync session
*
* @class
* @property {Object} uploadKeys Keys to upload to server
* @property {Zotero.Sync.ObjectKeySet} uploadKeys.updated
* @property {Zotero.Sync.ObjectKeySet} uploadKeys.deleted
*/
Zotero.Sync.Server.Session = function (defaultLibraryID) {
this.uploadKeys = {};
this.uploadKeys.updated = new Zotero.Sync.ObjectKeySet;
this.uploadKeys.deleted = new Zotero.Sync.ObjectKeySet;
this.suppressWarnings = false;
}
Zotero.Sync.Server.Session.prototype.addToUpdated = function (objs) {
this._addToKeySet('updated', objs);
}
Zotero.Sync.Server.Session.prototype.addToDeleted = function (objs) {
this._addToKeySet('deleted', objs);
}
Zotero.Sync.Server.Session.prototype.objectInUpdated = function (obj) {
var type = obj.objectType;
return this.uploadKeys.updated.hasLibraryKey(type, obj.libraryID, obj.key);
}
Zotero.Sync.Server.Session.prototype.objectInDeleted = function (obj) {
var type = obj.objectType;
return this.uploadKeys.deleted.hasLibraryKey(type, obj.libraryID, obj.key);
}
/**
* Returns array of keys of deleted objects in specified library
*/
Zotero.Sync.Server.Session.prototype.getDeleted = function (type, libraryID) {
return this.uploadKeys.deleted.getKeys(type, libraryID);
}
Zotero.Sync.Server.Session.prototype.removeFromUpdated = function (objs) {
this._removeFromKeySet('updated', objs);
}
Zotero.Sync.Server.Session.prototype.removeFromDeleted = function (objs) {
this._removeFromKeySet('deleted', objs);
}
Zotero.Sync.Server.Session.prototype._addToKeySet = function (keySet, objs) {
objs = Zotero.flattenArguments(objs);
var type = objs[0].objectType;
var keyPairs = [];
for each(var obj in objs) {
keyPairs.push({
libraryID: obj.libraryID,
key: obj.key
});
}
this.uploadKeys[keySet].addLibraryKeyPairs(type, keyPairs)
}
Zotero.Sync.Server.Session.prototype._removeFromKeySet = function (keySet, objs) {
if (!objs) {
throw ("No objects provided in Zotero.Sync.Server.Session._removeFromKeySet()");
}
objs = Zotero.flattenArguments(objs);
var type = objs[0].objectType;
var keyPairs = [];
for each(var obj in objs) {
keyPairs.push({
libraryID: obj.libraryID,
key: obj.key
});
}
this.uploadKeys[keySet].removeLibraryKeyPairs(type, keyPairs)
}
Zotero.Sync.Server.Data = new function() {
var _noMergeTypes = ['search'];
@ -3004,42 +2896,6 @@ Zotero.Sync.Server.Data = new function() {
}
/**
* Open a conflict resolution window and return the results
*
* @param {String} type 'item', 'collection', etc.
* @param {Array[]} objectPairs Array of arrays of pairs of Item, Collection, etc.
*/
function _reconcile(type, objectPairs, changedCreators) {
var io = {
dataIn: {
type: type,
captions: [
Zotero.getString('sync.localObject'),
Zotero.getString('sync.remoteObject'),
Zotero.getString('sync.mergedObject')
],
objects: objectPairs
}
};
if (type == 'item') {
if (!Zotero.Utilities.isEmpty(changedCreators)) {
io.dataIn.changedCreators = changedCreators;
}
}
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var lastWin = wm.getMostRecentWindow("navigator:browser");
lastWin.openDialog('chrome://zotero/content/merge.xul', '', 'chrome,modal,centerscreen', io);
if (io.error) {
throw (io.error);
}
return io.dataOut;
}
/**
* Process the results of conflict resolution
*/
@ -3081,299 +2937,6 @@ Zotero.Sync.Server.Data = new function() {
}
/**
* Converts a Zotero.Item object to a DOM <item> node
*
* @param {Zotero.Item} item
* @param {DOMDocument} doc
* @param {Zotero.Sync.Server.Session} [syncSession]
*/
this.itemToXML = function (item, doc, syncSession) {
var item = item.serialize();
var itemNode = doc.createElement('item');
itemNode.setAttribute('libraryID', item.primary.libraryID ? item.primary.libraryID : Zotero.libraryID);
itemNode.setAttribute('key', item.primary.key);
// Primary fields
for (var field in item.primary) {
switch (field) {
case 'itemID':
case 'libraryID':
case 'key':
continue;
default:
var attr = field;
}
itemNode.setAttribute(attr, item.primary[field]);
}
// Item data
for (var field in item.fields) {
if (!item.fields[field]) {
continue;
}
var fieldElem = doc.createElement('field');
fieldElem.setAttribute('name', field);
fieldElem.appendChild(doc.createTextNode(_xmlize(item.fields[field])));
itemNode.appendChild(fieldElem);
}
// Deleted item flag
if (item.deleted) {
itemNode.setAttribute('deleted', '1');
}
if (item.primary.itemType == 'note' || item.primary.itemType == 'attachment') {
if (item.sourceItemKey) {
itemNode.setAttribute('sourceItem', item.sourceItemKey);
}
}
// Note
if (item.primary.itemType == 'note') {
var noteElem = doc.createElement('note');
noteElem.appendChild(doc.createTextNode(_xmlize(item.note)));
itemNode.appendChild(noteElem);
}
// Attachment
if (item.primary.itemType == 'attachment') {
itemNode.setAttribute('linkMode', item.attachment.linkMode);
itemNode.setAttribute('mimeType', item.attachment.mimeType);
var charset = item.attachment.charset;
if (charset) {
itemNode.setAttribute('charset', charset);
}
if (item.attachment.linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
// Include paths for non-links
var path = item.attachment.path;
if (path != _xmlize(path)) {
var filename = item.attachment.path.substr(8);
var msg = Zotero.getString('sync.error.invalidCharsFilename', filename);
var e = new Zotero.Error(msg, 0, { dialogButtonText: null });
throw (e);
}
if (item.attachment.linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
path = Zotero.Attachments.getBaseDirectoryRelativePath(path);
}
var pathElem = doc.createElement('path');
pathElem.appendChild(doc.createTextNode(path));
itemNode.appendChild(pathElem);
// Include storage sync time and hash for imported files
if (item.attachment.linkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE) {
var mtime = Zotero.Sync.Storage.getSyncedModificationTime(item.primary.itemID);
if (mtime) {
itemNode.setAttribute('storageModTime', mtime);
}
var hash = Zotero.Sync.Storage.getSyncedHash(item.primary.itemID);
if (hash) {
itemNode.setAttribute('storageHash', hash);
}
}
}
if (item.note) {
var noteElem = doc.createElement('note');
noteElem.appendChild(doc.createTextNode(_xmlize(item.note)));
itemNode.appendChild(noteElem);
}
}
// Creators
var defaultLibraryID = Zotero.libraryID;
for (var index in item.creators) {
var creatorElem = doc.createElement('creator');
var libraryID = item.creators[index].libraryID ? item.creators[index].libraryID : defaultLibraryID;
var key = item.creators[index].key;
if (!key) {
Zotero.debug('==========');
Zotero.debug(index);
Zotero.debug(item);
throw ("Creator key not set for item in Zotero.Sync.Server.sync()");
}
creatorElem.setAttribute('libraryID', libraryID);
creatorElem.setAttribute('key', key);
creatorElem.setAttribute('creatorType', item.creators[index].creatorType);
creatorElem.setAttribute('index', index);
// Add creator XML as glue if not already included in sync session
var fakeObj = {
objectType: 'creator',
libraryID: libraryID,
key: key
};
if (syncSession && syncSession.objectInUpdated(fakeObj)) {
var creator = Zotero.Creators.getByLibraryAndKey(libraryID, key);
var subCreatorElem = Zotero.Sync.Server.Data.creatorToXML(creator, doc);
creatorElem.appendChild(subCreatorElem);
}
itemNode.appendChild(creatorElem);
}
// Related items
var related = item.related;
if (related.length) {
related = Zotero.Items.get(related);
var keys = [];
for each(var item in related) {
keys.push(item.key);
}
if (keys.length) {
var relatedElem = doc.createElement('related');
relatedElem.appendChild(doc.createTextNode(keys.join(' ')));
itemNode.appendChild(relatedElem);
}
}
return itemNode;
}
/**
* Convert DOM <item> node into an unsaved Zotero.Item
*
* @param object itemNode DOM XML node with item data
* @param object item (Optional) Existing Zotero.Item to update
* @param bool skipPrimary (Optional) Ignore passed primary fields (except itemTypeID)
*/
this.xmlToItem = function (itemNode, item, skipPrimary, defaultLibraryID) {
if (!item) {
item = new Zotero.Item;
}
else if (skipPrimary) {
throw ("Cannot use skipPrimary with existing item in "
+ "Zotero.Sync.Server.Data.xmlToItem()");
}
// TODO: add custom item types
var data = {};
if (!skipPrimary) {
data.libraryID = _getLibraryID(itemNode.getAttribute('libraryID'), defaultLibraryID);
data.key = itemNode.getAttribute('key');
data.dateAdded = itemNode.getAttribute('dateAdded');
data.dateModified = itemNode.getAttribute('dateModified');
}
data.itemTypeID = Zotero.ItemTypes.getID(itemNode.getAttribute('itemType'));
// TEMP - NSF
if (!data.itemTypeID) {
var msg = "Invalid item type '" + itemNode.getAttribute('itemType') + "' in Zotero.Sync.Server.Data.xmlToItem()";
var e = new Zotero.Error(msg, "INVALID_ITEM_TYPE");
throw (e);
}
var changedFields = {};
// Primary data
for (var field in data) {
item.setField(field, data[field]);
changedFields[field] = true;
}
// Item data
var fields = itemNode.getElementsByTagName('field');
for (var i=0, len=fields.length; i<len; i++) {
var field = fields[i];
var fieldName = field.getAttribute('name');
item.setField(fieldName, field.textContent);
changedFields[fieldName] = true;
}
var previousFields = item.getUsedFields(true);
for each(var field in previousFields) {
if (!changedFields[field] &&
// If not valid, it'll already have been cleared by the
// type change
Zotero.ItemFields.isValidForType(
Zotero.ItemFields.getID(field), data.itemTypeID
)) {
item.setField(field, false);
}
}
// Deleted item flag
var deleted = itemNode.getAttribute('deleted');
item.deleted = (deleted == 'true' || deleted == "1");
// Item creators
var i = 0;
var creators = Zotero.Utilities.xpath(itemNode, "creator");
for each(var creator in creators) {
var pos = parseInt(creator.getAttribute('index'));
if (pos != i) {
throw ('No creator in position ' + i);
}
var libraryID = data.libraryID;
var key = creator.getAttribute('key');
var creatorObj = Zotero.Creators.getByLibraryAndKey(libraryID, key);
if (!creatorObj) {
var msg = "Data for missing local creator " + libraryID + "/" + key
+ " not provided in Zotero.Sync.Server.Data.xmlToItem()";
var e = new Zotero.Error(msg, "MISSING_OBJECT");
throw (e);
}
item.setCreator(
pos,
creatorObj,
creator.getAttribute('creatorType')
);
i++;
}
// Remove item's remaining creators not in XML
var numCreators = item.numCreators();
var rem = numCreators - i;
for (var j=0; j<rem; j++) {
// Keep removing last creator
item.removeCreator(i);
}
// Both notes and attachments might have parents and notes
if (item.isNote() || item.isAttachment()) {
var sourceItemKey = itemNode.getAttribute('sourceItem');
item.setSourceKey(sourceItemKey ? sourceItemKey : false);
item.setNote(_getFirstChildContent(itemNode, 'note'));
}
// Attachment metadata
if (item.isAttachment()) {
item.attachmentLinkMode = parseInt(itemNode.getAttribute('linkMode'));
item.attachmentMIMEType = itemNode.getAttribute('mimeType');
item.attachmentCharset = itemNode.getAttribute('charset');
if (item.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
item.attachmentPath = _getFirstChildContent(itemNode, 'path');
}
}
// Related items
var related = _getFirstChildContent(itemNode, 'related');
var relatedIDs = [];
if (related) {
related = related.split(' ');
for each(var key in related) {
var relItem = Zotero.Items.getByLibraryAndKey(item.libraryID, key);
if (!relItem) {
var msg = "Related item " + item.libraryID + "/" + key
+ " doesn't exist in Zotero.Sync.Server.Data.xmlToItem()";
var e = new Zotero.Error(msg, "MISSING_OBJECT");
throw (e);
}
relatedIDs.push(relItem.id);
}
}
item.relatedItems = relatedIDs;
return item;
}
this.removeMissingRelatedItems = function (itemNode) {
var relatedNode = Zotero.Utilities.xpath(itemNode, "related");
if (!relatedNode.length) {
@ -3398,330 +2961,6 @@ Zotero.Sync.Server.Data = new function() {
}
this.collectionToXML = function (collection, doc) {
var colElem = doc.createElement('collection');
colElem.setAttribute('libraryID', collection.libraryID ? collection.libraryID : Zotero.libraryID);
colElem.setAttribute('key', collection.key);
colElem.setAttribute('name', _xmlize(collection.name));
colElem.setAttribute('dateAdded', collection.dateAdded);
colElem.setAttribute('dateModified', collection.dateModified);
if (collection.parent) {
var parentCol = Zotero.Collections.get(collection.parent);
colElem.setAttribute('parent', parentCol.key);
}
var children = collection.getChildren();
if (children) {
var itemKeys = [];
for each(var child in children) {
if (child.type == 'item') {
itemKeys.push(child.key);
}
}
if (itemKeys.length) {
var itemsElem = doc.createElement('items');
itemsElem.appendChild(doc.createTextNode(itemKeys.join(' ')));
colElem.appendChild(itemsElem);
}
}
return colElem;
}
/**
* Convert DOM <collection> node into an unsaved Zotero.Collection
*
* @param object collectionNode DOM XML node with collection data
* @param object item (Optional) Existing Zotero.Collection to update
* @param bool skipPrimary (Optional) Ignore passed primary fields (except itemTypeID)
* @param integer defaultLibraryID (Optional)
* @param array deletedItems (Optional) An array of keys that have been deleted in this sync session
*/
this.xmlToCollection = function (collectionNode, collection, skipPrimary, defaultLibraryID, deletedItemKeys) {
if (!collection) {
collection = new Zotero.Collection;
}
else if (skipPrimary) {
throw ("Cannot use skipPrimary with existing collection in "
+ "Zotero.Sync.Server.Data.xmlToCollection()");
}
if (!skipPrimary) {
collection.libraryID = _getLibraryID(collectionNode.getAttribute('libraryID'), defaultLibraryID);
collection.key = collectionNode.getAttribute('key');
var parentKey = collectionNode.getAttribute('parent');
if (parentKey) {
collection.parentKey = parentKey;
}
else {
collection.parent = false;
}
collection.dateAdded = collectionNode.getAttribute('dateAdded');
collection.dateModified = collectionNode.getAttribute('dateModified');
}
collection.name = collectionNode.getAttribute('name');
/*
// Subcollections
var str = xmlCollection.collections.toString();
collection.childCollections = str == '' ? [] : str.split(' ');
*/
// Child items
var childItems = _getFirstChildContent(collectionNode, 'items');
childItems = childItems ? childItems.split(' ') : []
var childItemIDs = [];
for each(var key in childItems) {
var childItem = Zotero.Items.getByLibraryAndKey(collection.libraryID, key);
if (!childItem) {
// Ignore items that were deleted in this sync session
//
// This can happen if a collection and its items are deleted
// locally but are in conflict with the server, and the local
// item deletes are selected in CR. Then, when the deleted
// collection is automatically restored, the items no
// longer exist.
if (deletedItemKeys && deletedItemKeys.indexOf(key) != -1) {
Zotero.debug("Ignoring deleted collection item '" + key + "'");
continue;
}
var msg = "Missing child item " + key + " for collection "
+ collection.libraryID + "/" + collection.key
+ " in Zotero.Sync.Server.Data.xmlToCollection()";
var e = new Zotero.Error(msg, "MISSING_OBJECT");
throw (e);
}
childItemIDs.push(childItem.id);
}
collection.childItems = childItemIDs;
return collection;
}
/**
* Recursively save collections from the top down
*/
function _saveCollections(collections) {
var originalLength = collections.length;
var unsaved = [];
var parentKey, parentCollection;
for each(var collection in collections) {
parentKey = collection.parentKey;
// Top-level collection, so save
if (!parentKey) {
collection.save();
continue;
}
parentCollection = Zotero.Collections.getByLibraryAndKey(
collection.libraryID, parentKey
);
// Parent collection exists, so save
if (parentCollection) {
collection.save();
continue;
}
// Add to unsaved list
unsaved.push(collection);
continue;
}
if (unsaved.length) {
if (unsaved.length == originalLength) {
var msg = "Incomplete collection hierarchy cannot be saved in Zotero.Sync.Server.Data._saveCollections()";
var e = new Zotero.Error(msg, "MISSING_OBJECT");
throw (e);
}
_saveCollections(unsaved);
}
}
/**
* Converts a Zotero.Creator object to a DOM <creator> node
*/
this.creatorToXML = function (creator, doc) {
var creatorElem = doc.createElement('creator');
creatorElem.setAttribute('libraryID', creator.libraryID ? creator.libraryID : Zotero.libraryID);
creatorElem.setAttribute('key', creator.key);
creatorElem.setAttribute('dateAdded', creator.dateAdded);
creatorElem.setAttribute('dateModified', creator.dateModified);
var allowEmpty = ['firstName', 'lastName', 'name'];
var creator = creator.serialize();
for (var field in creator.fields) {
if (!creator.fields[field] && allowEmpty.indexOf(field) == -1) {
continue;
}
var fieldElem = doc.createElement(field);
switch (field) {
case 'firstName':
case 'lastName':
case 'name':
var val = _xmlize(creator.fields[field]);
break;
default:
var val = creator.fields[field];
}
fieldElem.appendChild(doc.createTextNode(val));
creatorElem.appendChild(fieldElem);
}
return creatorElem;
}
/**
* Convert DOM <creator> node into an unsaved Zotero.Creator
*
* @param object creatorNode DOM XML node with creator data
* @param object item (Optional) Existing Zotero.Creator to update
* @param bool skipPrimary (Optional) Ignore passed primary fields (except itemTypeID)
*/
this.xmlToCreator = function (creatorNode, creator, skipPrimary, defaultLibraryID) {
if (!creator) {
creator = new Zotero.Creator;
}
else if (skipPrimary) {
throw ("Cannot use skipPrimary with existing creator in "
+ "Zotero.Sync.Server.Data.xmlToCreator()");
}
if (!skipPrimary) {
creator.libraryID = _getLibraryID(creatorNode.getAttribute('libraryID'), defaultLibraryID);
creator.key = creatorNode.getAttribute('key');
creator.dateAdded = creatorNode.getAttribute('dateAdded');
creator.dateModified = creatorNode.getAttribute('dateModified');
}
if (_getFirstChildContent(creatorNode, 'fieldMode') == 1) {
creator.firstName = '';
creator.lastName = _getFirstChildContent(creatorNode, 'name');
creator.fieldMode = 1;
}
else {
creator.firstName = _getFirstChildContent(creatorNode, 'firstName');
creator.lastName = _getFirstChildContent(creatorNode, 'lastName');
creator.fieldMode = 0;
}
creator.birthYear = _getFirstChildContent(creatorNode, 'birthYear');
return creator;
}
this.searchToXML = function (search, doc) {
var searchElem = doc.createElement('search');
searchElem.setAttribute('libraryID', search.libraryID ? search.libraryID : Zotero.libraryID);
searchElem.setAttribute('key', search.key);
searchElem.setAttribute('name', _xmlize(search.name));
searchElem.setAttribute('dateAdded', search.dateAdded);
searchElem.setAttribute('dateModified', search.dateModified);
var conditions = search.getConditions();
if (conditions) {
for each(var condition in conditions) {
var conditionElem = doc.createElement('condition');
conditionElem.setAttribute('id', condition.id);
conditionElem.setAttribute('condition', condition.condition);
if (condition.mode) {
conditionElem.setAttribute('mode', condition.mode);
}
conditionElem.setAttribute('operator', condition.operator);
conditionElem.setAttribute('value', _xmlize(condition.value ? condition.value : ''));
if (condition.required) {
conditionElem.setAttribute('required', 1);
}
searchElem.appendChild(conditionElem);
}
}
return searchElem;
}
/**
* Convert DOM <search> node into an unsaved Zotero.Search
*
* @param object searchNode DOM XML node with search data
* @param object item (Optional) Existing Zotero.Search to update
* @param bool skipPrimary (Optional) Ignore passed primary fields (except itemTypeID)
*/
this.xmlToSearch = function (searchNode, search, skipPrimary, defaultLibraryID) {
if (!search) {
search = new Zotero.Search;
}
else if (skipPrimary) {
throw ("Cannot use new id with existing search in "
+ "Zotero.Sync.Server.Data.xmlToSearch()");
}
if (!skipPrimary) {
search.libraryID = _getLibraryID(searchNode.getAttribute('libraryID'), defaultLibraryID);
search.key = searchNode.getAttribute('key');
search.dateAdded = searchNode.getAttribute('dateAdded');
search.dateModified = searchNode.getAttribute('dateModified');
}
search.name = searchNode.getAttribute('name');
var conditionID = -1;
// Search conditions
var conditions = searchNode.getElementsByTagName('condition');
for (var i=0, len=conditions.length; i<len; i++) {
var condition = conditions[i];
conditionID = parseInt(condition.getAttribute('id'));
var name = condition.getAttribute('condition');
var mode = condition.getAttribute('mode');
if (mode) {
name = name + '/' + mode;
}
if (search.getCondition(conditionID)) {
search.updateCondition(
conditionID,
name,
condition.getAttribute('operator'),
condition.getAttribute('value'),
!!condition.getAttribute('required')
);
}
else {
var newID = search.addCondition(
name,
condition.getAttribute('operator'),
condition.getAttribute('value'),
!!condition.getAttribute('required')
);
if (newID != conditionID) {
throw ("Search condition ids not contiguous in Zotero.Sync.Server.xmlToSearch()");
}
}
}
conditionID++;
while (search.getCondition(conditionID)) {
search.removeCondition(conditionID);
conditionID++;
}
return search;
}
this.tagToXML = function (tag, doc) {
var tagElem = doc.createElement('tag');
tagElem.setAttribute('libraryID', tag.libraryID ? tag.libraryID : Zotero.libraryID);
@ -3835,55 +3074,6 @@ Zotero.Sync.Server.Data = new function() {
}
/**
* Convert DOM <group> node into an unsaved Zotero.Group
*
* @param object groupNode DOM XML node with group data
* @param object group (Optional) Existing Zotero.Group to update
*/
this.xmlToGroup = function (groupNode, group) {
if (!group) {
group = new Zotero.Group;
}
group.id = parseInt(groupNode.getAttribute('id'));
group.libraryID = parseInt(groupNode.getAttribute('libraryID'));
group.name = groupNode.getAttribute('name');
group.editable = !!parseInt(groupNode.getAttribute('editable'));
group.filesEditable = !!parseInt(groupNode.getAttribute('filesEditable'));
group.description = _getFirstChildContent(groupNode, 'description');
/*
var keys = xmlGroup.items.toString() ? xmlGroup.items.toString().split(' ') : false;
if (keys) {
var ids = [];
for each(var key in keys) {
var item = Zotero.Items.getByLibraryAndKey(group.libraryID, key);
if (!item) {
throw ("Linked item " + key + " doesn't exist in Zotero.Sync.Server.Data.xmlToGroup()");
}
ids.push(item.id);
}
}
else {
var ids = [];
}
group.linkedItems = ids;
*/
return group;
}
this.relationToXML = function (relation, doc) {
return relation.toXML(doc);
}
this.xmlToRelation = function (relationNode) {
return Zotero.Relations.xmlToRelation(relationNode);
}
function _getFirstChildContent(node, childName) {
var elems = Zotero.Utilities.xpath(node, childName);
return elems.length ? elems[0].textContent : "";