diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js
index 6a5a09c9b..66977ff85 100644
--- a/chrome/content/zotero/xpcom/collectionTreeView.js
+++ b/chrome/content/zotero/xpcom/collectionTreeView.js
@@ -182,13 +182,13 @@ Zotero.CollectionTreeView.prototype.refresh = function()
}
}
- /*var shares = Zotero.Zeroconf.instances;
+ var shares = Zotero.Zeroconf.instances;
if (shares.length) {
this._showRow(new Zotero.ItemGroup('separator', false));
for each(var share in shares) {
this._showRow(new Zotero.ItemGroup('share', share));
}
- }*/
+ }
if (this.hideSources.indexOf('commons') == -1 && Zotero.Commons.enabled) {
this._showRow(new Zotero.ItemGroup('separator', false));
diff --git a/chrome/content/zotero/xpcom/dataServer.js b/chrome/content/zotero/xpcom/dataServer.js
new file mode 100644
index 000000000..6f8e1033f
--- /dev/null
+++ b/chrome/content/zotero/xpcom/dataServer.js
@@ -0,0 +1,297 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2009 Center for History and New Media
+ George Mason University, Fairfax, Virginia, USA
+ http://zotero.org
+
+ This file is part of Zotero.
+
+ Zotero is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Zotero is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Zotero. If not, see .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+
+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.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/zeroconf.js b/chrome/content/zotero/xpcom/zeroconf.js
new file mode 100644
index 000000000..9f837c911
--- /dev/null
+++ b/chrome/content/zotero/xpcom/zeroconf.js
@@ -0,0 +1,377 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2009 Center for History and New Media
+ George Mason University, Fairfax, Virginia, USA
+ http://zotero.org
+
+ This file is part of Zotero.
+
+ Zotero is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Zotero is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Zotero. If not, see .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+
+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;
+
+ // TODO: Why is Windows breaking without this?
+ return;
+ }
+
+ // 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.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
index 5718ee95b..19f29ce1e 100644
--- a/chrome/content/zotero/xpcom/zotero.js
+++ b/chrome/content/zotero/xpcom/zotero.js
@@ -635,6 +635,8 @@ const ZOTERO_CONFIG = {
Zotero.Server.init();
}
+ Zotero.Zeroconf.init();
+
Zotero.Sync.init();
Zotero.Sync.Runner.init();
diff --git a/components/zotero-service.js b/components/zotero-service.js
index 557004daa..4bf06bf98 100644
--- a/components/zotero-service.js
+++ b/components/zotero-service.js
@@ -100,6 +100,7 @@ const xpcomFilesLocal = [
'storage/webdav',
'timeline',
'uri',
+ 'zeroconf',
'translation/translate_item',
'translation/translator',
'server_connector'
diff --git a/scripts/zoteroconf.sh b/scripts/zoteroconf.sh
new file mode 100755
index 000000000..3d87d6bc1
--- /dev/null
+++ 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