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)
This commit is contained in:
Dan Stillman 2008-06-03 05:26:30 +00:00
parent 4d03dd8d43
commit 00c2b14d6c
16 changed files with 859 additions and 56 deletions

View file

@ -70,7 +70,7 @@ var ZoteroItemPane = new function() {
/* /*
* Loads an item * Loads an item
*/ */
function viewItem(thisItem) { function viewItem(thisItem, mode) {
//Zotero.debug('Viewing item'); //Zotero.debug('Viewing item');
// Force blur() when clicking off a textbox to another item in middle // Force blur() when clicking off a textbox to another item in middle
@ -100,11 +100,11 @@ var ZoteroItemPane = new function() {
_itemBeingEdited = thisItem; _itemBeingEdited = thisItem;
_loaded = {}; _loaded = {};
loadPane(_tabs.selectedIndex); loadPane(_tabs.selectedIndex, mode);
} }
function loadPane(index) { function loadPane(index, mode) {
//Zotero.debug('Loading item pane ' + index); //Zotero.debug('Loading item pane ' + index);
// Clear the tab index when switching panes // Clear the tab index when switching panes
@ -121,7 +121,14 @@ var ZoteroItemPane = new function() {
// Info pane // Info pane
if (index == 0) { if (index == 0) {
var itembox = document.getElementById('zotero-editpane-item-box'); 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; itembox.item = _itemBeingEdited;
} }

View file

@ -788,7 +788,12 @@ var ZoteroPane = new function()
if(item.ref.isNote()) if(item.ref.isNote())
{ {
var noteEditor = document.getElementById('zotero-note-editor'); 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 // 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 // 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 else
{ {
ZoteroItemPane.viewItem(item.ref); ZoteroItemPane.viewItem(item.ref, this.itemsView.readOnly ? 'view' : false);
document.getElementById('zotero-item-pane-content').selectedIndex = 1; document.getElementById('zotero-item-pane-content').selectedIndex = 1;
} }
} }
@ -1056,7 +1061,8 @@ var ZoteroPane = new function()
var noPrompt = true; var noPrompt = true;
} }
// Do nothing in search view // Do nothing in search view
else if (this.itemsView._itemGroup.isSearch()) { else if (this.itemsView._itemGroup.isSearch() ||
this.itemsView._itemGroup.isShare()) {
return; return;
} }
} }
@ -1465,7 +1471,14 @@ var ZoteroPane = new function()
var enable = [], disable = [], show = [], hide = [], multiple = ''; 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, enable.push(m.showInLibrary, m.addNote, m.attachSnapshot, m.attachLink,
m.sep2, m.duplicateItem, m.deleteItem, m.deleteFromLibrary, m.sep2, m.duplicateItem, m.deleteItem, m.deleteFromLibrary,
m.exportItems, m.createBib, m.loadReport); m.exportItems, m.createBib, m.loadReport);
@ -1607,6 +1620,10 @@ var ZoteroPane = new function()
} }
} }
else if (tree.id == 'zotero-items-tree') { else if (tree.id == 'zotero-items-tree') {
if (this.itemsView.readOnly) {
return;
}
if (this.itemsView && this.itemsView.selection.currentIndex > -1) { if (this.itemsView && this.itemsView.selection.currentIndex > -1) {
var item = this.getSelectedItems()[0]; var item = this.getSelectedItems()[0];
if (item && item.isNote()) { if (item && item.isNote()) {

View file

@ -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-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();"/> <menuitem id="zotero-tb-actions-export" label="&zotero.toolbar.export.label;" oncommand="Zotero_File_Interface.exportFile();"/>
<menuseparator id="zotero-tb-actions-utilities-separator"/> <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()"/> <menuitem id="zotero-tb-actions-timeline" label="&zotero.toolbar.timeline.label;" oncommand="Zotero_Timeline_Interface.loadTimeline()"/>
<menuseparator id="zotero-tb-actions-sync-separator"/> <menuseparator id="zotero-tb-actions-sync-separator"/>
<menuitem label="Clear Server Data" oncommand="Zotero.Sync.Server.clear()"/> <menuitem label="Clear Server Data" oncommand="Zotero.Sync.Server.clear()"/>

View file

@ -36,7 +36,7 @@ Zotero.CollectionTreeView = function()
this._treebox = null; this._treebox = null;
this.itemToSelect = null; this.itemToSelect = null;
this._highlightedRows = {}; 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(); this._refreshHashMap();
// Update the treebox's row count // Update the treebox's row count
@ -162,8 +169,16 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
var madeChanges = false; 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 //Since a delete involves shifting of rows, we have to do it in order
//sort the ids by row //sort the ids by row
@ -672,6 +687,16 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient)
} }
return false; 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' else if (dataType == 'text/x-moz-url'
|| dataType == 'application/x-moz-file') { || dataType == 'application/x-moz-file') {
if (this._getItemAtRow(row).isSearch()) { if (this._getItemAtRow(row).isSearch()) {
@ -733,6 +758,25 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
this._getItemAtRow(row).ref.addItems(toAdd); 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') { else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') {
if (this._getItemAtRow(row).isCollection()) { if (this._getItemAtRow(row).isCollection()) {
var parentCollectionID = this._getItemAtRow(row).ref.id; var parentCollectionID = this._getItemAtRow(row).ref.id;
@ -820,6 +864,7 @@ Zotero.CollectionTreeView.prototype.getSupportedFlavours = function ()
var flavors = new FlavourSet(); var flavors = new FlavourSet();
flavors.appendFlavour("zotero/collection"); flavors.appendFlavour("zotero/collection");
flavors.appendFlavour("zotero/item"); flavors.appendFlavour("zotero/item");
flavors.appendFlavour("zotero/item-xml");
flavors.appendFlavour("text/x-moz-url"); flavors.appendFlavour("text/x-moz-url");
flavors.appendFlavour("application/x-moz-file", "nsIFile"); flavors.appendFlavour("application/x-moz-file", "nsIFile");
return flavors; return flavors;
@ -884,6 +929,11 @@ Zotero.ItemGroup.prototype.isSearch = function()
return this.type == 'search'; return this.type == 'search';
} }
Zotero.ItemGroup.prototype.isShare = function()
{
return this.type == 'share';
}
Zotero.ItemGroup.prototype.getName = function() Zotero.ItemGroup.prototype.getName = function()
{ {
if (this.isCollection()) { if (this.isCollection()) {
@ -895,6 +945,9 @@ Zotero.ItemGroup.prototype.getName = function()
else if (this.isSearch()) { else if (this.isSearch()) {
return this.ref.name; return this.ref.name;
} }
else if (this.isShare()) {
return this.ref.name;
}
else { else {
return ""; return "";
} }
@ -902,6 +955,11 @@ Zotero.ItemGroup.prototype.getName = function()
Zotero.ItemGroup.prototype.getChildItems = 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(); var s = this.getSearchObject();
try { try {
var ids = s.search(); var ids = s.search();
@ -970,6 +1028,11 @@ Zotero.ItemGroup.prototype.getSearchObject = function() {
* Returns all the tags used by items in the current view * Returns all the tags used by items in the current view
*/ */
Zotero.ItemGroup.prototype.getChildTags = function() { Zotero.ItemGroup.prototype.getChildTags = function() {
// TODO: implement?
if (this.isShare()) {
return false;
}
var s = this.getSearchObject(); var s = this.getSearchObject();
return Zotero.Tags.getAllWithinSearch(s); return Zotero.Tags.getAllWithinSearch(s);
} }

View file

@ -1765,6 +1765,10 @@ Zotero.Item.prototype.getNoteTitle = function() {
return this._noteTitle; return this._noteTitle;
} }
if (!this.id) {
return '';
}
var sql = "SELECT title FROM itemNotes WHERE itemID=?"; var sql = "SELECT title FROM itemNotes WHERE itemID=?";
var title = Zotero.DB.valueQuery(sql, this.id); 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"); throw ("getNote() can only be called on notes and attachments");
} }
if (!this.id) {
return '';
}
// Store access time for later garbage collection // Store access time for later garbage collection
this._noteAccessTime = new Date(); this._noteAccessTime = new Date();
@ -1793,6 +1793,10 @@ Zotero.Item.prototype.getNote = function() {
return this._noteText; return this._noteText;
} }
if (!this.id) {
return '';
}
var sql = "SELECT note FROM itemNotes WHERE itemID=" + this.id; var sql = "SELECT note FROM itemNotes WHERE itemID=" + this.id;
var note = Zotero.DB.valueQuery(sql); var note = Zotero.DB.valueQuery(sql);

View file

@ -163,7 +163,8 @@ Zotero.Tags = new function() {
var tmpTable = search.search(true); var tmpTable = search.search(true);
} }
catch (e) { 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.DB.rollbackTransaction();
Zotero.debug(e, 2); Zotero.debug(e, 2);
} }

View file

@ -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();
}
}

View file

@ -47,7 +47,7 @@ Zotero.ItemTreeView = function(itemGroup, sourcesOnly)
this._dataItems = []; this._dataItems = [];
this.rowCount = 0; 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 * 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 refreshing a single item, just unselect and reselect it
if (action == 'refresh') { 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.selection.clearSelection();
this.rememberSelection(savedSelection); this.rememberSelection(savedSelection);
} }
@ -259,6 +271,10 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
return; return;
} }
if (this._itemGroup.isShare()) {
return;
}
this.selection.selectEventsSuppressed = true; this.selection.selectEventsSuppressed = true;
// See if we're in the active window // See if we're in the active window
@ -1502,7 +1518,22 @@ Zotero.ItemTreeCommandController.prototype.onEvent = function(evt)
* Begin a drag * Begin a drag
*/ */
Zotero.ItemTreeView.prototype.onDragStart = function (evt,transferData,action) 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 = new TransferData();
transferData.data.addDataForFlavour("zotero/item", this.saveSelection()); transferData.data.addDataForFlavour("zotero/item", this.saveSelection());
@ -1783,6 +1814,7 @@ Zotero.ItemTreeView.prototype.getSupportedFlavours = function ()
{ {
var flavors = new FlavourSet(); var flavors = new FlavourSet();
flavors.appendFlavour("zotero/item"); flavors.appendFlavour("zotero/item");
flavors.appendFlavour("zotero/item-xml");
flavors.appendFlavour("text/x-moz-url"); flavors.appendFlavour("text/x-moz-url");
flavors.appendFlavour("application/x-moz-file", "nsIFile"); flavors.appendFlavour("application/x-moz-file", "nsIFile");
return flavors; return flavors;
@ -1878,8 +1910,7 @@ Zotero.ItemTreeView.prototype.canDrop = function(row, orient)
// Highlight the rows correctly on drag // Highlight the rows correctly on drag
var rowItem = this._getItemAtRow(row).ref; //the item we are dragging over var rowItem = this._getItemAtRow(row).ref; //the item we are dragging over
if (dataType == 'zotero/item') if (dataType == 'zotero/item') {
{
// Directly on a row // Directly on a row
if (orient == 0) if (orient == 0)
{ {

View file

@ -24,7 +24,7 @@ Zotero.Notifier = new function(){
var _observers = new Zotero.Hash(); var _observers = new Zotero.Hash();
var _disabled = false; var _disabled = false;
var _types = [ var _types = [
'collection', 'creator', 'search', 'item', 'collection', 'creator', 'search', 'share', 'share-items', 'item',
'collection-item', 'item-tag', 'tag' 'collection-item', 'item-tag', 'tag'
]; ];
var _inTransaction; var _inTransaction;

View file

@ -1073,7 +1073,6 @@ Zotero.Sync.Server.Data = new function() {
default xml namespace = ''; default xml namespace = '';
function processUpdatedXML(xml, lastLocalSyncDate, uploadIDs) { function processUpdatedXML(xml, lastLocalSyncDate, uploadIDs) {
if (xml.children().length() == 0) { if (xml.children().length() == 0) {
Zotero.debug('No changes received from server'); 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 xmlItem E4X XML node with item data
* @param object item (Optional) Existing Zotero.Item to update * @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 (!item) {
if (newID) { if (skipPrimary) {
item = new Zotero.Item(null); item = new Zotero.Item(null);
} }
else { else {
@ -1653,19 +1652,21 @@ Zotero.Sync.Server.Data = new function() {
*/ */
} }
} }
else if (newID) { else if (skipPrimary) {
_error("Cannot use new id with existing item in " _error("Cannot use skipPrimary with existing item in "
+ "Zotero.Sync.Server.Data.xmlToItem()"); + "Zotero.Sync.Server.Data.xmlToItem()");
} }
// TODO: add custom item types // TODO: add custom item types
var data = { var data = {
itemTypeID: Zotero.ItemTypes.getID(xmlItem.@itemType.toString()), itemTypeID: Zotero.ItemTypes.getID(xmlItem.@itemType.toString())
dateAdded: xmlItem.@dateAdded.toString(),
dateModified: xmlItem.@dateModified.toString(),
key: xmlItem.@key.toString()
}; };
if (!skipPrimary) {
data.dateAdded = xmlItem.@dateAdded.toString();
data.dateModified = xmlItem.@dateModified.toString();
data.key = xmlItem.@key.toString();
}
var changedFields = {}; var changedFields = {};
@ -1780,11 +1781,11 @@ Zotero.Sync.Server.Data = new function() {
* *
* @param object xmlCollection E4X XML node with collection data * @param object xmlCollection E4X XML node with collection data
* @param object item (Optional) Existing Zotero.Collection to update * @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 (!collection) {
if (newID) { if (skipPrimary) {
collection = new Zotero.Collection(null); collection = new Zotero.Collection(null);
} }
else { else {
@ -1797,16 +1798,19 @@ Zotero.Sync.Server.Data = new function() {
*/ */
} }
} }
else if (newID) { else if (skipPrimary) {
_error("Cannot use new id with existing collection in " _error("Cannot use skipPrimary with existing collection in "
+ "Zotero.Sync.Server.Data.xmlToCollection()"); + "Zotero.Sync.Server.Data.xmlToCollection()");
} }
collection.name = xmlCollection.@name.toString(); collection.name = xmlCollection.@name.toString();
collection.parent = xmlCollection.@parent.toString() ? if (!skipPrimary) {
parseInt(xmlCollection.@parent) : false; collection.parent = xmlCollection.@parent.toString() ?
collection.dateModified = xmlCollection.@dateModified.toString(); parseInt(xmlCollection.@parent) : false;
collection.key = xmlCollection.@key.toString(); collection.dateAdded = xmlCollection.@dateAdded.toString();
collection.dateModified = xmlCollection.@dateModified.toString();
collection.key = xmlCollection.@key.toString();
}
// Subcollections // Subcollections
var str = xmlCollection.collections.toString(); 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 xmlCreator E4X XML node with creator data
* @param object item (Optional) Existing Zotero.Creator to update * @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 (!creator) {
if (newID) { if (skipPrimary) {
creator = new Zotero.Creator(null); creator = new Zotero.Creator(null);
} }
else { else {
@ -1872,16 +1876,19 @@ Zotero.Sync.Server.Data = new function() {
*/ */
} }
} }
else if (newID) { else if (skipPrimary) {
_error("Cannot use new id with existing creator in " _error("Cannot use skipPrimary with existing creator in "
+ "Zotero.Sync.Server.Data.xmlToCreator()"); + "Zotero.Sync.Server.Data.xmlToCreator()");
} }
var data = { var data = {
dateModified: xmlCreator.@dateModified.toString(),
key: xmlCreator.@key.toString(),
birthYear: xmlCreator.birthYear.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) { if (xmlCreator.fieldMode == 1) {
data.firstName = ''; data.firstName = '';
@ -1935,11 +1942,11 @@ Zotero.Sync.Server.Data = new function() {
* *
* @param object xmlSearch E4X XML node with search data * @param object xmlSearch E4X XML node with search data
* @param object item (Optional) Existing Zotero.Search to update * @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 (!search) {
if (newID) { if (skipPrimary) {
search = new Zotero.Search(null); search = new Zotero.Search(null);
} }
else { 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 " _error("Cannot use new id with existing search in "
+ "Zotero.Sync.Server.Data.xmlToSearch()"); + "Zotero.Sync.Server.Data.xmlToSearch()");
} }
search.name = xmlSearch.@name.toString(); search.name = xmlSearch.@name.toString();
search.dateModified = xmlSearch.@dateModified.toString(); if (!skipPrimary) {
search.key = xmlSearch.@key.toString(); search.dateAdded = xmlSearch.@dateAdded.toString();
search.dateModified = xmlSearch.@dateModified.toString();
search.key = xmlSearch.@key.toString();
}
var conditionID = -1; var conditionID = -1;

View file

@ -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 () {
}
*/

View file

@ -253,6 +253,10 @@ var Zotero = new function(){
Zotero.Integration.SOAP.init(); Zotero.Integration.SOAP.init();
Zotero.Integration.init(); Zotero.Integration.init();
// Initialize data web server
Zotero.DataServer.init();
Zotero.Zeroconf.init();
Zotero.Sync.init(); Zotero.Sync.init();
this.initialized = true; this.initialized = true;

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

View file

@ -16,11 +16,11 @@ var ZoteroWrapped = this;
var xpcomFiles = [ 'zotero', var xpcomFiles = [ 'zotero',
'annotate', 'attachments', 'cite', 'cite_compat', 'collectionTreeView', '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/cachedTypes', 'data/creator', 'data/creators', 'data/itemFields',
'data/notes', 'data/tags', 'db', 'file', 'fulltext', 'id', 'ingester', 'integration', 'data/notes', 'data/tags', 'db', 'file', 'fulltext', 'id', 'ingester', 'integration',
'itemTreeView', 'mime', 'notifier', 'progressWindow', 'quickCopy', 'report', '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++) { for (var i=0; i<xpcomFiles.length; i++) {
Cc["@mozilla.org/moz/jssubscript-loader;1"] Cc["@mozilla.org/moz/jssubscript-loader;1"]

View file

@ -68,6 +68,9 @@ pref("extensions.zotero.export.quickCopy.setting", 'bibliography=http://www.zote
// Integration settings // Integration settings
pref("extensions.zotero.integration.autoRegenerate", -1); // -1 = ask; 0 = no; 1 = yes pref("extensions.zotero.integration.autoRegenerate", -1); // -1 = ask; 0 = no; 1 = yes
// Zeroconf
pref("extensions.zotero.zeroconf.server.enabled", false);
// Annotation settings // Annotation settings
pref("extensions.zotero.annotations.warnOnClose", true); pref("extensions.zotero.annotations.warnOnClose", true);

42
scripts/zoteroconf.sh Executable file
View file

@ -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