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