commit 00c2b14d6c1b9b3af8c086a0421d393684fe5992 parent 4d03dd8d43caee6ba29e202be7edbcaefadac4ce Author: Dan Stillman <dstillman@zotero.org> Date: Tue, 3 Jun 2008 05:26:30 +0000 Adds rudimentary Zeroconf support to Zotero (a.k.a. "Z(ot)eroconf") - Inspired by Dan Chudnov's Python/MODS-based Zeroconf demo at THATcamp - Enabled by extensions.zotero.zeroconf.enabled (off by default) - Currently supports only OS X (tested on Leopard, not sure about earlier versions) - Uses Apple's dns-sd and mDNS command-client clients, but should be able to be extended to other clients, though a native library would be far superior - Discovery is on-demand for now via Actions menu ("Search for Shared Libraries") - Includes rudimentary web server (code copied from integration.js) that serves items as sync XML -- no authentication yet! - Only supports top-level items - Remote libraries show up in left pane (under remote computer name, for now) - Items can be dragged into collections (but not the library yet, for some reason) - On first run, might cause a long pause and the "This file was downloaded from the Internet" message on Leopard -- can't manage to get around the quarantine for the script file that we need to access stdout from Firefox - Needs a lot of work, and without a real JS (or otherwise Mozilla-native) Zeroconf library we can't do proper discovery without intermittent polling - But it works, at least for me Also includes some data/sync-layer changes that I needed along the way (and that we'll need for shared collections of any type) Diffstat:
16 files changed, 859 insertions(+), 56 deletions(-)
diff --git a/chrome/content/zotero/itemPane.js b/chrome/content/zotero/itemPane.js @@ -70,7 +70,7 @@ var ZoteroItemPane = new function() { /* * Loads an item */ - function viewItem(thisItem) { + function viewItem(thisItem, mode) { //Zotero.debug('Viewing item'); // Force blur() when clicking off a textbox to another item in middle @@ -100,11 +100,11 @@ var ZoteroItemPane = new function() { _itemBeingEdited = thisItem; _loaded = {}; - loadPane(_tabs.selectedIndex); + loadPane(_tabs.selectedIndex, mode); } - function loadPane(index) { + function loadPane(index, mode) { //Zotero.debug('Loading item pane ' + index); // Clear the tab index when switching panes @@ -121,7 +121,14 @@ var ZoteroItemPane = new function() { // Info pane if (index == 0) { var itembox = document.getElementById('zotero-editpane-item-box'); - itembox.mode = 'edit'; + // Hack to allow read-only mode in right pane -- probably a better + // way to allow access to this + if (mode) { + itembox.mode = mode; + } + else { + itembox.mode = 'edit'; + } itembox.item = _itemBeingEdited; } diff --git a/chrome/content/zotero/overlay.js b/chrome/content/zotero/overlay.js @@ -788,7 +788,12 @@ var ZoteroPane = new function() if(item.ref.isNote()) { var noteEditor = document.getElementById('zotero-note-editor'); - noteEditor.mode = 'edit'; + if (this.itemsView.readOnly) { + noteEditor.mode = 'view'; + } + else { + noteEditor.mode = 'edit'; + } // If loading new or different note, disable undo while we repopulate the text field // so Undo doesn't end up clearing the field. This also ensures that Undo doesn't @@ -953,7 +958,7 @@ var ZoteroPane = new function() } else { - ZoteroItemPane.viewItem(item.ref); + ZoteroItemPane.viewItem(item.ref, this.itemsView.readOnly ? 'view' : false); document.getElementById('zotero-item-pane-content').selectedIndex = 1; } } @@ -1056,7 +1061,8 @@ var ZoteroPane = new function() var noPrompt = true; } // Do nothing in search view - else if (this.itemsView._itemGroup.isSearch()) { + else if (this.itemsView._itemGroup.isSearch() || + this.itemsView._itemGroup.isShare()) { return; } } @@ -1465,7 +1471,14 @@ var ZoteroPane = new function() var enable = [], disable = [], show = [], hide = [], multiple = ''; - if (this.itemsView && this.itemsView.selection.count > 0) { + // TODO: implement menu for remote items + if (this.itemsView.readOnly) { + for each(var pos in m) { + disable.push(pos); + } + } + + else if (this.itemsView && this.itemsView.selection.count > 0) { enable.push(m.showInLibrary, m.addNote, m.attachSnapshot, m.attachLink, m.sep2, m.duplicateItem, m.deleteItem, m.deleteFromLibrary, m.exportItems, m.createBib, m.loadReport); @@ -1607,6 +1620,10 @@ var ZoteroPane = new function() } } else if (tree.id == 'zotero-items-tree') { + if (this.itemsView.readOnly) { + return; + } + if (this.itemsView && this.itemsView.selection.currentIndex > -1) { var item = this.getSelectedItems()[0]; if (item && item.isNote()) { diff --git a/chrome/content/zotero/overlay.xul b/chrome/content/zotero/overlay.xul @@ -124,6 +124,8 @@ <menuitem id="zotero-tb-actions-import" label="&zotero.toolbar.import.label;" oncommand="Zotero_File_Interface.importFile();"/> <menuitem id="zotero-tb-actions-export" label="&zotero.toolbar.export.label;" oncommand="Zotero_File_Interface.exportFile();"/> <menuseparator id="zotero-tb-actions-utilities-separator"/> + <menuitem id="zotero-tb-actions-zeroconf-update" label="Search for Shared Libraries" + oncommand="Zotero.Zeroconf.findInstances()"/> <menuitem id="zotero-tb-actions-timeline" label="&zotero.toolbar.timeline.label;" oncommand="Zotero_Timeline_Interface.loadTimeline()"/> <menuseparator id="zotero-tb-actions-sync-separator"/> <menuitem label="Clear Server Data" oncommand="Zotero.Sync.Server.clear()"/> diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js @@ -36,7 +36,7 @@ Zotero.CollectionTreeView = function() this._treebox = null; this.itemToSelect = null; this._highlightedRows = {}; - this._unregisterID = Zotero.Notifier.registerObserver(this, ['collection', 'search']); + this._unregisterID = Zotero.Notifier.registerObserver(this, ['collection', 'search', 'share']); } /* @@ -107,6 +107,13 @@ Zotero.CollectionTreeView.prototype.refresh = function() } } + var shares = Zotero.Zeroconf.instances; + if (shares) { + for each(var share in shares) { + this._showItem(new Zotero.ItemGroup('share', share), 0, this._dataItems.length); //itemgroup ref, level, beforeRow + } + } + this._refreshHashMap(); // Update the treebox's row count @@ -162,8 +169,16 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids) var madeChanges = false; - if(action == 'delete') - { + if (action == 'refresh') { + switch (type) { + case 'share': + this.reload(); + this.rememberSelection(savedSelection); + break; + } + } + + else if(action == 'delete') { //Since a delete involves shifting of rows, we have to do it in order //sort the ids by row @@ -672,6 +687,16 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient) } return false; } + else if (dataType == 'zotero/item-xml') { + var xml = new XML(data.data); + for each(var xmlNode in xml.items.item) { + var item = Zotero.Sync.Server.Data.xmlToItem(xmlNode); + if (item.isRegularItem() || !item.getSource()) { + return true; + } + } + return false; + } else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') { if (this._getItemAtRow(row).isSearch()) { @@ -733,6 +758,25 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient) this._getItemAtRow(row).ref.addItems(toAdd); } } + else if (dataType == 'zotero/item-xml') { + Zotero.DB.beginTransaction(); + var xml = new XML(data.data); + var toAdd = []; + for each(var xmlNode in xml.items.item) { + var item = Zotero.Sync.Server.Data.xmlToItem(xmlNode, false, true); + if (item.isRegularItem() || !item.getSource()) { + var id = item.save(); + toAdd.push(id); + } + } + if (toAdd.length > 0) { + this._getItemAtRow(row).ref.addItems(toAdd); + } + + Zotero.DB.commitTransaction(); + + return; + } else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') { if (this._getItemAtRow(row).isCollection()) { var parentCollectionID = this._getItemAtRow(row).ref.id; @@ -820,6 +864,7 @@ Zotero.CollectionTreeView.prototype.getSupportedFlavours = function () var flavors = new FlavourSet(); flavors.appendFlavour("zotero/collection"); flavors.appendFlavour("zotero/item"); + flavors.appendFlavour("zotero/item-xml"); flavors.appendFlavour("text/x-moz-url"); flavors.appendFlavour("application/x-moz-file", "nsIFile"); return flavors; @@ -884,6 +929,11 @@ Zotero.ItemGroup.prototype.isSearch = function() return this.type == 'search'; } +Zotero.ItemGroup.prototype.isShare = function() +{ + return this.type == 'share'; +} + Zotero.ItemGroup.prototype.getName = function() { if (this.isCollection()) { @@ -895,6 +945,9 @@ Zotero.ItemGroup.prototype.getName = function() else if (this.isSearch()) { return this.ref.name; } + else if (this.isShare()) { + return this.ref.name; + } else { return ""; } @@ -902,6 +955,11 @@ Zotero.ItemGroup.prototype.getName = function() Zotero.ItemGroup.prototype.getChildItems = function() { + // Fake results if this is a shared library + if (this.isShare()) { + return this.ref.getAll(); + } + var s = this.getSearchObject(); try { var ids = s.search(); @@ -970,6 +1028,11 @@ Zotero.ItemGroup.prototype.getSearchObject = function() { * Returns all the tags used by items in the current view */ Zotero.ItemGroup.prototype.getChildTags = function() { + // TODO: implement? + if (this.isShare()) { + return false; + } + var s = this.getSearchObject(); return Zotero.Tags.getAllWithinSearch(s); } diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js @@ -1765,6 +1765,10 @@ Zotero.Item.prototype.getNoteTitle = function() { return this._noteTitle; } + if (!this.id) { + return ''; + } + var sql = "SELECT title FROM itemNotes WHERE itemID=?"; var title = Zotero.DB.valueQuery(sql, this.id); @@ -1782,10 +1786,6 @@ Zotero.Item.prototype.getNote = function() { throw ("getNote() can only be called on notes and attachments"); } - if (!this.id) { - return ''; - } - // Store access time for later garbage collection this._noteAccessTime = new Date(); @@ -1793,6 +1793,10 @@ Zotero.Item.prototype.getNote = function() { return this._noteText; } + if (!this.id) { + return ''; + } + var sql = "SELECT note FROM itemNotes WHERE itemID=" + this.id; var note = Zotero.DB.valueQuery(sql); diff --git a/chrome/content/zotero/xpcom/data/tags.js b/chrome/content/zotero/xpcom/data/tags.js @@ -163,7 +163,8 @@ Zotero.Tags = new function() { var tmpTable = search.search(true); } catch (e) { - if (e.match(/Saved search [0-9]+ does not exist/)) { + if (typeof e == 'string' + && e.match(/Saved search [0-9]+ does not exist/)) { Zotero.DB.rollbackTransaction(); Zotero.debug(e, 2); } diff --git a/chrome/content/zotero/xpcom/dataServer.js b/chrome/content/zotero/xpcom/dataServer.js @@ -0,0 +1,271 @@ +Zotero.DataServer = new function () { + this.init = init; + this.handleHeader = handleHeader; + + // TODO: assign dynamically + this.__defineGetter__('port', function () { + return 22030; + }); + + var _onlineObserverRegistered; + + + /* + * initializes a very rudimentary web server used for SOAP RPC + */ + function init() { + // Use Zeroconf pref for now + if (!Zotero.Prefs.get("zeroconf.server.enabled")) { + Zotero.debug("Not initializing data HTTP server"); + return; + } + + if (Zotero.Utilities.HTTP.browserIsOffline()) { + Zotero.debug('Browser is offline -- not initializing data HTTP server'); + _registerOnlineObserver() + return; + } + + // start listening on socket + var serv = Components.classes["@mozilla.org/network/server-socket;1"] + .createInstance(Components.interfaces.nsIServerSocket); + try { + serv.init(this.port, false, -1); + serv.asyncListen(Zotero.DataServer.SocketListener); + + Zotero.debug("Data HTTP server listening on 127.0.0.1:" + serv.port); + } + catch(e) { + Zotero.debug("Not initializing data HTTP server"); + } + + _registerOnlineObserver() + } + + /* + * handles an HTTP request + */ + function handleHeader(header) { + // get first line of request (all we care about for now) + var method = header.substr(0, header.indexOf(" ")); + + if (!method) { + return _generateResponse("400 Bad Request"); + } + + if (method != "POST") { + return _generateResponse("501 Method Not Implemented"); + } + + // Parse request URI + var matches = header.match("^[A-Z]+ (\/.*) HTTP/1.[01]"); + if (!matches) { + return _generateResponse("400 Bad Request"); + } + + var response = _handleRequest(matches[1]); + + // return OK + return _generateResponse("200 OK", 'text/xml; charset="UTF-8"', response); + } + + + function _handleRequest(uri) { + var s = new Zotero.Search(); + s.addCondition('noChildren', 'true'); + var ids = s.search(); + + if (!ids) { + ids = []; + } + + var uploadIDs = { + updated: { + items: ids + }, + /* TODO: fix buildUploadXML to ignore missing */ + deleted: {} + }; + return Zotero.Sync.Server.Data.buildUploadXML(uploadIDs); + } + + + /* + * generates the response to an HTTP request + */ + function _generateResponse(status, contentType, body) { + var response = "HTTP/1.0 "+status+"\r\n"; + + if(body) { + if(contentType) { + response += "Content-Type: "+contentType+"\r\n"; + } + response += "\r\n"+body; + } else { + response += "Content-Length: 0\r\n\r\n" + } + + return response; + } + + + function _registerOnlineObserver() { + if (_onlineObserverRegistered) { + return; + } + + // Observer to enable the integration when we go online + var observer = { + observe: function(subject, topic, data) { + if (data == 'online') { + Zotero.Integration.init(); + } + } + }; + + var observerService = + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + observerService.addObserver(observer, "network:offline-status-changed", false); + + _onlineObserverRegistered = true; + } +} + + +Zotero.DataServer.SocketListener = new function() { + this.onSocketAccepted = onSocketAccepted; + + /* + * called when a socket is opened + */ + function onSocketAccepted(socket, transport) { + // get an input stream + var iStream = transport.openInputStream(0, 0, 0); + var oStream = transport.openOutputStream(0, 0, 0); + + var dataListener = new Zotero.DataServer.DataListener(iStream, oStream); + var pump = Components.classes["@mozilla.org/network/input-stream-pump;1"] + .createInstance(Components.interfaces.nsIInputStreamPump); + pump.init(iStream, -1, -1, 0, 0, false); + pump.asyncRead(dataListener, null); + } +} + +/* + * handles the actual acquisition of data + */ +Zotero.DataServer.DataListener = function(iStream, oStream) { + this.header = ""; + this.headerFinished = false; + + this.body = ""; + this.bodyLength = 0; + + this.iStream = iStream; + this.oStream = oStream; + this.sStream = Components.classes["@mozilla.org/scriptableinputstream;1"] + .createInstance(Components.interfaces.nsIScriptableInputStream); + this.sStream.init(iStream); + + this.foundReturn = false; +} + +/* + * called when a request begins (although the request should have begun before + * the DataListener was generated) + */ +Zotero.DataServer.DataListener.prototype.onStartRequest = function(request, context) {} + +/* + * called when a request stops + */ +Zotero.DataServer.DataListener.prototype.onStopRequest = function(request, context, status) { + this.iStream.close(); + this.oStream.close(); +} + +/* + * called when new data is available + */ +Zotero.DataServer.DataListener.prototype.onDataAvailable = function(request, context, + inputStream, offset, count) { + var readData = this.sStream.read(count); + + // Read header + if (!this.headerFinished) { + // see if there's a magic double return + var lineBreakIndex = readData.indexOf("\r\n\r\n"); + if (lineBreakIndex != -1) { + if (lineBreakIndex != 0) { + this.header += readData.substr(0, lineBreakIndex+4); + } + + this._headerFinished(); + return; + } + + var lineBreakIndex = readData.indexOf("\n\n"); + if (lineBreakIndex != -1) { + if (lineBreakIndex != 0) { + this.header += readData.substr(0, lineBreakIndex+2); + } + + this._headerFinished(); + return; + } + + if (this.header && this.header[this.header.length-1] == "\n" && + (readData[0] == "\n" || readData[0] == "\r")) { + if (readData.length > 1 && readData[1] == "\n") { + this.header += readData.substr(0, 2); + } + else { + this.header += readData[0]; + } + + this._headerFinished(); + return; + } + + this.header += readData; + } +} + +/* + * processes an HTTP header and decides what to do + */ +Zotero.DataServer.DataListener.prototype._headerFinished = function() { + this.headerFinished = true; + var output = Zotero.DataServer.handleHeader(this.header); + this._requestFinished(output); +} + +/* + * returns HTTP data from a request + */ +Zotero.DataServer.DataListener.prototype._requestFinished = function(response) { + // close input stream + this.iStream.close(); + + // open UTF-8 converter for output stream + var intlStream = Components.classes["@mozilla.org/intl/converter-output-stream;1"] + .createInstance(Components.interfaces.nsIConverterOutputStream); + + // write + try { + intlStream.init(this.oStream, "UTF-8", 1024, "?".charCodeAt(0)); + + Zotero.debug('Writing response to stream:\n\n' + response); + + // write response + intlStream.writeString(response); + } catch(e) { + Zotero.debug("An error occurred."); + Zotero.debug(e); + } finally { + Zotero.debug('Closing stream'); + intlStream.close(); + } +} + diff --git a/chrome/content/zotero/xpcom/itemTreeView.js b/chrome/content/zotero/xpcom/itemTreeView.js @@ -47,7 +47,7 @@ Zotero.ItemTreeView = function(itemGroup, sourcesOnly) this._dataItems = []; this.rowCount = 0; - this._unregisterID = Zotero.Notifier.registerObserver(this, ['item', 'collection-item']); + this._unregisterID = Zotero.Notifier.registerObserver(this, ['item', 'collection-item', 'share-items']); } @@ -229,6 +229,13 @@ Zotero.ItemTreeView.prototype.refresh = function() } +Zotero.ItemTreeView.prototype.__defineGetter__('readOnly', function () { + if (this._itemGroup.isShare()) { + return true; + } + return false; +}); + /* * Called by Zotero.Notifier on any changes to items in the data layer */ @@ -251,7 +258,12 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData) // If refreshing a single item, just unselect and reselect it if (action == 'refresh') { - if (savedSelection.length == 1 && savedSelection[0] == ids[0]) { + if (type == 'share-items') { + if (this._itemGroup.isShare()) { + this.refresh(); + } + } + else if (savedSelection.length == 1 && savedSelection[0] == ids[0]) { this.selection.clearSelection(); this.rememberSelection(savedSelection); } @@ -259,6 +271,10 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData) return; } + if (this._itemGroup.isShare()) { + return; + } + this.selection.selectEventsSuppressed = true; // See if we're in the active window @@ -1502,7 +1518,22 @@ Zotero.ItemTreeCommandController.prototype.onEvent = function(evt) * Begin a drag */ Zotero.ItemTreeView.prototype.onDragStart = function (evt,transferData,action) -{ +{ + // Quick implementation of dragging of XML item format + if (this.readOnly) { + var items = this.getSelectedItems(); + + var xml = <data/>; + for (var i=0; i<items.length; i++) { + var xmlNode = Zotero.Sync.Server.Data.itemToXML(items[i]); + xml.items.item += xmlNode; + } + Zotero.debug(xml.toXMLString()); + transferData.data = new TransferData(); + transferData.data.addDataForFlavour("zotero/item-xml", xml.toXMLString()); + return; + } + transferData.data = new TransferData(); transferData.data.addDataForFlavour("zotero/item", this.saveSelection()); @@ -1783,6 +1814,7 @@ Zotero.ItemTreeView.prototype.getSupportedFlavours = function () { var flavors = new FlavourSet(); flavors.appendFlavour("zotero/item"); + flavors.appendFlavour("zotero/item-xml"); flavors.appendFlavour("text/x-moz-url"); flavors.appendFlavour("application/x-moz-file", "nsIFile"); return flavors; @@ -1878,8 +1910,7 @@ Zotero.ItemTreeView.prototype.canDrop = function(row, orient) // Highlight the rows correctly on drag var rowItem = this._getItemAtRow(row).ref; //the item we are dragging over - if (dataType == 'zotero/item') - { + if (dataType == 'zotero/item') { // Directly on a row if (orient == 0) { diff --git a/chrome/content/zotero/xpcom/notifier.js b/chrome/content/zotero/xpcom/notifier.js @@ -24,7 +24,7 @@ Zotero.Notifier = new function(){ var _observers = new Zotero.Hash(); var _disabled = false; var _types = [ - 'collection', 'creator', 'search', 'item', + 'collection', 'creator', 'search', 'share', 'share-items', 'item', 'collection-item', 'item-tag', 'tag' ]; var _inTransaction; diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js @@ -1073,7 +1073,6 @@ Zotero.Sync.Server.Data = new function() { default xml namespace = ''; - function processUpdatedXML(xml, lastLocalSyncDate, uploadIDs) { if (xml.children().length() == 0) { Zotero.debug('No changes received from server'); @@ -1636,11 +1635,11 @@ Zotero.Sync.Server.Data = new function() { * * @param object xmlItem E4X XML node with item data * @param object item (Optional) Existing Zotero.Item to update - * @param bool newID (Optional) Ignore passed itemID and choose new one + * @param bool skipPrimary (Optional) Ignore passed primary fields (except itemTypeID) */ - function xmlToItem(xmlItem, item, newID) { + function xmlToItem(xmlItem, item, skipPrimary) { if (!item) { - if (newID) { + if (skipPrimary) { item = new Zotero.Item(null); } else { @@ -1653,19 +1652,21 @@ Zotero.Sync.Server.Data = new function() { */ } } - else if (newID) { - _error("Cannot use new id with existing item in " + else if (skipPrimary) { + _error("Cannot use skipPrimary with existing item in " + "Zotero.Sync.Server.Data.xmlToItem()"); } // TODO: add custom item types var data = { - itemTypeID: Zotero.ItemTypes.getID(xmlItem.@itemType.toString()), - dateAdded: xmlItem.@dateAdded.toString(), - dateModified: xmlItem.@dateModified.toString(), - key: xmlItem.@key.toString() + itemTypeID: Zotero.ItemTypes.getID(xmlItem.@itemType.toString()) }; + if (!skipPrimary) { + data.dateAdded = xmlItem.@dateAdded.toString(); + data.dateModified = xmlItem.@dateModified.toString(); + data.key = xmlItem.@key.toString(); + } var changedFields = {}; @@ -1780,11 +1781,11 @@ Zotero.Sync.Server.Data = new function() { * * @param object xmlCollection E4X XML node with collection data * @param object item (Optional) Existing Zotero.Collection to update - * @param bool newID (Optional) Ignore passed collectionID and choose new one + * @param bool skipPrimary (Optional) Ignore passed primary fields (except itemTypeID) */ - function xmlToCollection(xmlCollection, collection, newID) { + function xmlToCollection(xmlCollection, collection, skipPrimary) { if (!collection) { - if (newID) { + if (skipPrimary) { collection = new Zotero.Collection(null); } else { @@ -1797,16 +1798,19 @@ Zotero.Sync.Server.Data = new function() { */ } } - else if (newID) { - _error("Cannot use new id with existing collection in " + else if (skipPrimary) { + _error("Cannot use skipPrimary with existing collection in " + "Zotero.Sync.Server.Data.xmlToCollection()"); } collection.name = xmlCollection.@name.toString(); - collection.parent = xmlCollection.@parent.toString() ? - parseInt(xmlCollection.@parent) : false; - collection.dateModified = xmlCollection.@dateModified.toString(); - collection.key = xmlCollection.@key.toString(); + if (!skipPrimary) { + collection.parent = xmlCollection.@parent.toString() ? + parseInt(xmlCollection.@parent) : false; + collection.dateAdded = xmlCollection.@dateAdded.toString(); + collection.dateModified = xmlCollection.@dateModified.toString(); + collection.key = xmlCollection.@key.toString(); + } // Subcollections var str = xmlCollection.collections.toString(); @@ -1855,11 +1859,11 @@ Zotero.Sync.Server.Data = new function() { * * @param object xmlCreator E4X XML node with creator data * @param object item (Optional) Existing Zotero.Creator to update - * @param bool newID (Optional) Ignore passed creatorID and choose new one + * @param bool skipPrimary (Optional) Ignore passed primary fields (except itemTypeID) */ - function xmlToCreator(xmlCreator, creator, newID) { + function xmlToCreator(xmlCreator, creator, skipPrimary) { if (!creator) { - if (newID) { + if (skipPrimary) { creator = new Zotero.Creator(null); } else { @@ -1872,16 +1876,19 @@ Zotero.Sync.Server.Data = new function() { */ } } - else if (newID) { - _error("Cannot use new id with existing creator in " + else if (skipPrimary) { + _error("Cannot use skipPrimary with existing creator in " + "Zotero.Sync.Server.Data.xmlToCreator()"); } var data = { - dateModified: xmlCreator.@dateModified.toString(), - key: xmlCreator.@key.toString(), birthYear: xmlCreator.birthYear.toString() }; + if (!skipPrimary) { + data.dateAdded = xmlCreator.@dateAdded.toString(); + data.dateModified = xmlCreator.@dateModified.toString(); + data.key = xmlCreator.@key.toString(); + } if (xmlCreator.fieldMode == 1) { data.firstName = ''; @@ -1935,11 +1942,11 @@ Zotero.Sync.Server.Data = new function() { * * @param object xmlSearch E4X XML node with search data * @param object item (Optional) Existing Zotero.Search to update - * @param bool newID (Optional) Ignore passed searchID and choose new one + * @param bool skipPrimary (Optional) Ignore passed primary fields (except itemTypeID) */ - function xmlToSearch(xmlSearch, search, newID) { + function xmlToSearch(xmlSearch, search, skipPrimary) { if (!search) { - if (newID) { + if (skipPrimary) { search = new Zotero.Search(null); } else { @@ -1952,14 +1959,17 @@ Zotero.Sync.Server.Data = new function() { */ } } - else if (newID) { + else if (skipPrimary) { _error("Cannot use new id with existing search in " + "Zotero.Sync.Server.Data.xmlToSearch()"); } search.name = xmlSearch.@name.toString(); - search.dateModified = xmlSearch.@dateModified.toString(); - search.key = xmlSearch.@key.toString(); + if (!skipPrimary) { + search.dateAdded = xmlSearch.@dateAdded.toString(); + search.dateModified = xmlSearch.@dateModified.toString(); + search.key = xmlSearch.@key.toString(); + } var conditionID = -1; diff --git a/chrome/content/zotero/xpcom/zeroconf.js b/chrome/content/zotero/xpcom/zeroconf.js @@ -0,0 +1,348 @@ +Zotero.Zeroconf = new function () { + this.init = init; + this.registerService = registerService; + this.findInstances = findInstances; + this.findInstancesCallback = findInstancesCallback; + this.unregisterService = unregisterService; + this.getScript = getScript; + + this.clientEnabled = true; + this.serverEnabled = true; + + this.__defineGetter__('clientPath', function () { + return '/usr/bin/dns-sd'; + }); + + this.__defineGetter__('displayName', function () { + var dnsService = Components.classes["@mozilla.org/network/dns-service;1"]. + getService(Components.interfaces.nsIDNSService); + var hostname = dnsService.myHostName; + + return hostname; + }); + + this.__defineGetter__('port', function () { + return Zotero.DataServer.port; + }); + + this.__defineGetter__('instances', function () { + var instances = {}; + for (var instance in _instances) { + instances[instance] = new Zotero.Zeroconf.RemoteLibrary(instance); + } + return instances; + }); + + var _instances = []; + var _browseCacheFile = '/tmp/zoteroconf_instances'; + var scriptsLoaded = false; + + function init() { + if (!Zotero.Prefs.get("zeroconf.server.enabled")) { + this.clientEnabled = false; + this.serverEnabled = false; + } + + // OS X only, for now + if (!Zotero.isMac) { + this.clientEnabled = false; + this.serverEnabled = false; + } + + // Make sure we have the client executable + var file = Components.classes["@mozilla.org/file/local;1"]. + createInstance(Components.interfaces.nsILocalFile); + file.initWithPath(this.clientPath); + + if (!file.exists()) { + Zotero.debug('Not enabling Z(ot)eroconf -- executable not found'); + this.clientEnabled = false; + this.serverEnabled = false; + return; + } + + if (!this.serverEnabled) { + Zotero.debug('Not enabling Z(ot)eroconf'); + return; + } + + var registered = this.registerService(); + if (!registered) { + return; + } + + var observerService = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + observerService.addObserver({ + observe: function(subject, topic, data) { + Zotero.Zeroconf.unregisterService(); + } + }, "quit-application", false); + } + + + function registerService() { + var file = Components.classes["@mozilla.org/file/local;1"]. + createInstance(Components.interfaces.nsILocalFile); + file.initWithPath(this.clientPath); + + var process = Components.classes["@mozilla.org/process/util;1"]. + createInstance(Components.interfaces.nsIProcess); + process.init(file); + + var args = ["-R", this.displayName, "_zotero._tcp", "local.", this.port]; + + Zotero.debug("Registering Z(ot)eroconf on port " + this.port); + process.run(false, args, args.length); + + return true; + } + + + function findInstances(callback) { + if (!this.clientEnabled) { + return; + } + + Zotero.debug("Browsing for Z(ot)eroconf instances"); + var file = this.getScript('find_instances'); + + var process = Components.classes["@mozilla.org/process/util;1"]. + createInstance(Components.interfaces.nsIProcess); + process.init(file); + var args = ['find_instances']; + process.run(false, args, args.length); + + // Wait half a second for browse before proceeding + setTimeout(function () { + Zotero.Zeroconf.findInstancesCallback(callback); + }, 500); + } + + + function findInstancesCallback(callback) { + var file = Zotero.Zeroconf.getScript('kill_find_instances'); + + var process = Components.classes["@mozilla.org/process/util;1"]. + createInstance(Components.interfaces.nsIProcess); + process.init(file); + var args = ['kill_find_instances']; + process.run(false, args, args.length); + + var file = Components.classes["@mozilla.org/file/local;1"]. + createInstance(Components.interfaces.nsILocalFile); + file.initWithPath(_browseCacheFile); + + if (!file.exists()) { + Zotero.debug(_browseCacheFile + " doesn't exist", 2); + _instances = {}; + return; + } + + var browseCache = Zotero.File.getContents(file); + Zotero.debug(browseCache); + file.remove(null); + + // Parse browse output + var lines = browseCache.split(/\n/); + var newInstances = {}; + for each(var line in lines) { + var matches = line.match(/([a-zA-Z\.]+) +_zotero\._tcp\. +(.+)/); + if (matches) { + var domain = matches[1]; + var name = matches[2]; + // Skip local host + if (name == this.displayName) { + continue; + } + newInstances[name] = true; + } + } + + // Remove expired instances + for (var instance in _instances) { + if (!newInstances[instance]) { + delete _instances[instance]; + } + } + + // Add new instances + for (var instance in newInstances) { + _instances[instance] = true; + } + + Zotero.Notifier.trigger('refresh', 'share', 'all'); + + if (callback) { + callback(); + } + } + + + function unregisterService() { + Zotero.debug("Unregistering Zeroconf service"); + var file = Zotero.Zeroconf.getScript('kill_service'); + + var process = Components.classes["@mozilla.org/process/util;1"]. + createInstance(Components.interfaces.nsIProcess); + process.init(file); + var args = ['kill_service']; + var ret = process.run(false, args, args.length); + + if (ret != 0) { + Zotero.debug("Zeroconf client not stopped!", 2); + } + + // Remove any zoteroconf files remaining in tmp directory + var file = Components.classes["@mozilla.org/file/local;1"]. + createInstance(Components.interfaces.nsILocalFile); + file.initWithPath('/tmp'); + if (!file.exists() || !file.isDirectory()) { + return; + } + try { + var files = file.directoryEntries; + while (files.hasMoreElements()) { + var tmpFile = files.getNext(); + tmpFile.QueryInterface(Components.interfaces.nsILocalFile); + if (tmpFile.leafName.indexOf('zoteroconf') != -1) { + tmpFile.remove(null); + } + } + } + catch (e) { + Zotero.debug(e); + } + } + + + function getScript() { + var file = Components.classes["@mozilla.org/extensions/manager;1"] + .getService(Components.interfaces.nsIExtensionManager) + .getInstallLocation(ZOTERO_CONFIG['GUID']) + .getItemLocation(ZOTERO_CONFIG['GUID']); + file.append('scripts'); + file.append('zoteroconf.sh'); + + // The first time we load the script, do some checks + if (!scriptsLoaded) { + if (!file.exists()) { + throw ('zoteroconf.sh not found in Zotero.Zeroconf.getScript()'); + } + + // Make sure the file is executable + if (file.permissions != 33261) { + try { + file.permissions = 33261; + } + catch (e) { + throw ('Cannot make zoteroconf.sh executable in Zotero.Zeroconf.getScript()'); + } + } + } + + return file; + } +} + + + +Zotero.Zeroconf.RemoteLibrary = function (name) { + default xml namespace = ''; + + this.name = name; + + this._host; + this._port; + this._items = []; + this._tmpFile = '/tmp/zoteroconf_info_' + Zotero.randomString(6); + //this.search = new Zotero.Zeroconf.RemoteLibrary.Search(this); +} + +Zotero.Zeroconf.RemoteLibrary.prototype.load = function () { + Zotero.debug("Getting service info for " + this.name); + + var file = Zotero.Zeroconf.getScript('get_info'); + + var process = Components.classes["@mozilla.org/process/util;1"]. + createInstance(Components.interfaces.nsIProcess); + process.init(file); + var args = ['get_info', this.name, this._tmpFile]; + process.run(false, args, args.length); + + var self = this; + + setTimeout(function () { + var file = Zotero.Zeroconf.getScript('kill_get_info'); + + var process = Components.classes["@mozilla.org/process/util;1"]. + createInstance(Components.interfaces.nsIProcess); + process.init(file); + var args = ['kill_get_info']; + process.run(false, args, args.length); + + var file = Components.classes["@mozilla.org/file/local;1"]. + createInstance(Components.interfaces.nsILocalFile); + file.initWithPath(self._tmpFile); + + var infoCache = Zotero.File.getContents(file); + Zotero.debug(infoCache); + file.remove(null); + + var lines = infoCache.split(/\n/); + for each(var line in lines) { + var matches = line.match(/can be reached at +([^ ]+) *:([0-9]+)/); + if (matches) { + self._host = matches[1]; + self._port = matches[2]; + break; + } + } + + if (self._host) { + self.loadItems(self); + } + }, 250); +} + +Zotero.Zeroconf.RemoteLibrary.prototype.loadItems = function (self, noNotify) { + var url = "http://" + this._host + ':' + this._port; + Zotero.Utilities.HTTP.doPost(url, '', function (xmlhttp) { + Zotero.debug(xmlhttp.responseText); + + self._items = []; + var xml = new XML(xmlhttp.responseText); + for each(var xmlNode in xml.items.item) { + var obj = Zotero.Sync.Server.Data.xmlToItem(xmlNode, false, true); + self._items.push(obj); + } + + Zotero.debug("Retrieved " + self._items.length + + " item" + (self._items.length == 1 ? '' : 's')); + + if (!noNotify) { + Zotero.Notifier.trigger('refresh', 'share-items', 'all'); + } + }); +} + +Zotero.Zeroconf.RemoteLibrary.prototype.getAll = function () { + if (!this._host) { + this.load(); + return []; + } + + this.loadItems(this, true); + + return this._items; +} + +/* +Zotero.Zeroconf.RemoteLibrary.Search = function (library) { + this.library = library; +} + +Zotero.Zeroconf.RemoteLibrary.Search.prototype = function () { + +} +*/ diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js @@ -253,6 +253,10 @@ var Zotero = new function(){ Zotero.Integration.SOAP.init(); Zotero.Integration.init(); + // Initialize data web server + Zotero.DataServer.init(); + Zotero.Zeroconf.init(); + Zotero.Sync.init(); this.initialized = true; diff --git a/chrome/skin/default/zotero/treesource-share.png b/chrome/skin/default/zotero/treesource-share.png Binary files differ. diff --git a/components/zotero-service.js b/components/zotero-service.js @@ -16,11 +16,11 @@ var ZoteroWrapped = this; var xpcomFiles = [ 'zotero', 'annotate', 'attachments', 'cite', 'cite_compat', 'collectionTreeView', - 'data_access', 'data/item', 'data/items', 'data/collection', 'data/collections', + 'dataServer', 'data_access', 'data/item', 'data/items', 'data/collection', 'data/collections', 'data/cachedTypes', 'data/creator', 'data/creators', 'data/itemFields', 'data/notes', 'data/tags', 'db', 'file', 'fulltext', 'id', 'ingester', 'integration', 'itemTreeView', 'mime', 'notifier', 'progressWindow', 'quickCopy', 'report', - 'schema', 'search', 'sync', 'timeline', 'translate', 'utilities']; + 'schema', 'search', 'sync', 'timeline', 'translate', 'utilities', 'zeroconf']; for (var i=0; i<xpcomFiles.length; i++) { Cc["@mozilla.org/moz/jssubscript-loader;1"] diff --git a/defaults/preferences/zotero.js b/defaults/preferences/zotero.js @@ -68,6 +68,9 @@ pref("extensions.zotero.export.quickCopy.setting", 'bibliography=http://www.zote // Integration settings pref("extensions.zotero.integration.autoRegenerate", -1); // -1 = ask; 0 = no; 1 = yes +// Zeroconf +pref("extensions.zotero.zeroconf.server.enabled", false); + // Annotation settings pref("extensions.zotero.annotations.warnOnClose", true); diff --git a/scripts/zoteroconf.sh b/scripts/zoteroconf.sh @@ -0,0 +1,42 @@ +#!/bin/sh +if [ ! "$1" ]; then + echo "Action not specified" + exit 1 +fi + +if [ $1 = "find_instances" ]; then + dns-sd -B _zotero._tcp local. > /tmp/zoteroconf_instances & + +elif [ $1 = "kill_find_instances" ]; then + PIDs=`ps x | grep "dns-sd -B" | grep _zotero._tcp | sed -E 's/ *([0-9]+).*/\1/' | xargs` + if [ "$PIDs" ]; then + kill $PIDs + fi + +elif [ $1 = "get_info" ]; then + if [ ! "$2" ]; then + echo "Service name not specified" + exit 1 + fi + + if [ ! "$3" ]; then + echo "Temp file path not specified" + exit 1 + fi + + #dns-sd -L "$2" _zotero._tcp local. > $3 & + mDNS -L "$2" _zotero._tcp local. > $3 & + +elif [ $1 = "kill_get_info" ]; then + #PIDs=`ps x | grep "dns-sd -L" | grep _zotero._tcp | sed -E 's/ *([0-9]+).*/\1/' | xargs` + PIDs=`ps x | grep "mDNS -L" | grep _zotero._tcp | sed -E 's/ *([0-9]+).*/\1/' | xargs` + if [ "$PIDs" ]; then + kill $PIDs + fi + +elif [ $1 = "kill_service" ]; then + PIDs=`ps x | grep dns-sd | grep '_zotero._tcp' | sed -E 's/ *([0-9]+).*/\1/' | xargs` + if [ "$PIDs" ]; then + kill $PIDs + fi +fi