Skip to content

Commit

Permalink
Add initial SSE from Zotero client support
Browse files Browse the repository at this point in the history
  • Loading branch information
adomasven committed Aug 9, 2017
1 parent 9feeb25 commit f7119cf
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 114 deletions.
227 changes: 119 additions & 108 deletions src/common/connector.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,137 +24,83 @@
*/

// TODO: refactor this class
Zotero.Connector = new function() {
const CONNECTOR_API_VERSION = 2;
Zotero.Connector = {
_CONNECTOR_API_VERSION: 2,

var _ieStandaloneIframeTarget, _ieConnectorCallbacks;
// As of Chrome 38 (and corresponding Opera version 24?) pages loaded over
// https (i.e. the zotero bookmarklet iframe) can not send requests over
// http, so pinging Standalone at http://127.0.0.1 fails.
// Disable for all browsers, except IE, which may be used frequently with ZSA
this.isOnline = Zotero.isBookmarklet && !Zotero.isIE ? false : null;
this.shouldReportActiveURL = true;
isOnline: Zotero.isBookmarklet && !Zotero.isIE ? false : null,
_shouldReportActiveURL: true,
_selected: {collection: null, library: null, item: null},

init: function() {
this.addEventListener('init', {notify: function(data) {
this._selected = data.selected;
}.bind(this)});
this.addEventListener('select', {notify: function(data) {
Object.assign(this._selected, data);
}.bind(this)});

Zotero.Connector.SSE.init();
Zotero.Connector_Types.init();
},

/**
* Checks if Zotero is online and passes current status to callback
* @param {Function} callback
*/
this.checkIsOnline = Zotero.Promise.method(function() {
checkIsOnline: Zotero.Promise.method(function() {
// Only check once in bookmarklet
if(Zotero.isBookmarklet && this.isOnline !== null) {
return this.isOnline;
}

var deferred = Zotero.Promise.defer();

if (Zotero.isIE) {
if (window.location.protocol !== "http:") {
this.isOnline = false;
// If SSE is available then we can return current status too
if (Zotero.Connector.SSE.available) {
return this.isOnline;
}

return Zotero.Connector.ping("ping", {}).catch(function(e) {
if (e.status == 0) {
return false;
}

Zotero.debug("Connector: Looking for Zotero Standalone");
var fail = function() {
if (this.isOnline !== null) return;
Zotero.debug("Connector: Zotero Standalone is not online or cannot be contacted");
this.isOnline = false;
deferred.resolve(false);
}.bind(this);

window.setTimeout(fail, 1000);
try {
var xdr = new XDomainRequest();
xdr.timeout = 700;
xdr.open("POST", `${Zotero.Prefs.get('connector.url')}connector/ping`, true);
xdr.onerror = function() {
Zotero.debug("Connector: XDomainRequest to Zotero Standalone experienced an error");
fail();
};
xdr.ontimeout = function() {
Zotero.debug("Connector: XDomainRequest to Zotero Standalone timed out");
fail();
};
xdr.onload = function() {
if(me.isOnline !== null) return;
me.isOnline = true;
Zotero.debug("Connector: Standalone found; trying IE hack");

_ieConnectorCallbacks = [];
var listener = function(event) {
if(!Zotero.Prefs.get('connector.url').includes(event.origin)
|| event.source !== iframe.contentWindow) return;
if(event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}

// If this is the first time the target was loaded, then this is a loaded
// event
if(!_ieStandaloneIframeTarget) {
Zotero.debug("Connector: Standalone loaded");
_ieStandaloneIframeTarget = iframe.contentWindow;
deferred.resolve(true);
}

// Otherwise, this is a response event
try {
var data = JSON.parse(event.data);
} catch(e) {
Zotero.debug("Invalid JSON received: "+event.data);
return;
}
var xhrSurrogate = {
"status":data[1],
"responseText":data[2],
"getResponseHeader":function(x) { return data[3][x.toLowerCase()] }
};
_ieConnectorCallbacks[data[0]](xhrSurrogate);
delete _ieConnectorCallbacks[data[0]];
};

if(window.addEventListener) {
window.addEventListener("message", listener, false);
} else {
window.attachEvent("onmessage", function() { listener(event); });
}

var iframe = document.createElement("iframe");
iframe.src = `${Zotero.Prefs.get('connector.url')}connector/ieHack`;
document.documentElement.appendChild(iframe);
};
xdr.send("");
} catch(e) {
Zotero.logError(e);
fail();
}
} else {
return this.ping({}).catch(function(e) {
if (e.status == 0) {
return false;
}
throw e
});
}
return deferred.promise;
});
throw e
});
}),

this.reportActiveURL = function(url) {
if (!this.isOnline || !this.shouldReportActiveURL) return;
reportActiveURL: function(url) {
if (!this.isOnline || !this._shouldReportActiveURL) return;

let payload = { activeURL: url };
this.ping(payload);
}
},

this.ping = function(payload={}) {
ping: function(payload={}) {
return Zotero.Connector.callMethod("ping", payload).then(function(response) {
if (response && 'prefs' in response) {
Zotero.Connector.shouldReportActiveURL = !!response.prefs.reportActiveURL;
Zotero.Connector._shouldReportActiveURL = !!response.prefs.reportActiveURL;
Zotero.Connector.automaticSnapshots = !!response.prefs.automaticSnapshots;
}
return response || {};
});
}
},

getSelectedCollection: Zotero.Promise.method(function() {
if (!Zotero.Connector.isOnline) {
throw new this.CommunicationError('Zotero is Offline');
} else if (Zotero.Connector.SSE.available) {
return this._selected;
} else {
return this.callMethod('getSelectedCollection', {}).then(function(response) {
let selected = {library: {editable: response.libraryEditable}};
selected.library.id = response.id;
selected.collection = {name: response.name};
return selected;
});
}
}),

/**
* Sends the XHR to execute an RPC call.
Expand All @@ -167,7 +113,9 @@ Zotero.Connector = new function() {
* @param {Object} data - RPC data to POST. If null or undefined, a GET request is sent.
* @param {Function} callback - Function to be called when requests complete.
*/
this.callMethod = Zotero.Promise.method(function(options, data, cb, tab) {
callMethod: Zotero.Promise.method(function(options, data, cb, tab) {
// TODO: make this default behaviour once people switch to SSE enabled Zotero
// and add communication if Zotero.isOnline but SSE unavailable - i.e. fairly old version
// Don't bother trying if not online in bookmarklet
if (Zotero.isBookmarklet && this.isOnline === false) {
throw new Zotero.CommunicationError("Zotero Offline", 0);
Expand All @@ -179,18 +127,17 @@ Zotero.Connector = new function() {
var headers = Object.assign({
"Content-Type":"application/json",
"X-Zotero-Version":Zotero.version,
"X-Zotero-Connector-API-Version":CONNECTOR_API_VERSION
"X-Zotero-Connector-API-Version":Zotero.Connector._CONNECTOR_API_VERSION
}, options.headers || {});
var queryString = options.queryString ? ("?" + options.queryString) : "";

var deferred = Zotero.Promise.defer();
var newCallback = function(req) {
try {
var isOnline = req.status !== 0 && req.status !== 403 && req.status !== 412;

if(Zotero.Connector.isOnline !== isOnline) {
Zotero.Connector.isOnline = isOnline;
if(Zotero.Connector_Browser && Zotero.Connector_Browser.onStateChange) {
if (Zotero.Connector_Browser && Zotero.Connector_Browser.onStateChange) {
Zotero.Connector_Browser.onStateChange(isOnline && req.getResponseHeader('X-Zotero-Version'));
}
}
Expand Down Expand Up @@ -254,7 +201,7 @@ Zotero.Connector = new function() {
* @param {String|Object} options. See documentation above
* @param {Object} data RPC data. See documentation above.
*/
this.callMethodWithCookies = function(options, data, cb, tab) {
callMethodWithCookies: function(options, data, cb, tab) {
if (Zotero.isBrowserExt && !Zotero.isBookmarklet) {
return new Zotero.Promise(function(resolve) {
chrome.cookies.getAll({url: tab.url}, resolve);
Expand Down Expand Up @@ -292,6 +239,70 @@ Zotero.Connector.CommunicationError = function (message, status=0, value='') {
}
Zotero.Connector.CommunicationError.prototype = new Error;


Zotero.Connector.SSE = {
_listeners: {},
available: false,

init: function() {
this._evtSrc = new EventSource(ZOTERO_CONFIG.CONNECTOR_SERVER_URL + 'connector/sse');
this._evtSrc.onerror = this._onError.bind(this);
this._evtSrc.onmessage = this._onMessage.bind(this);
this._evtSrc.onopen = this._onOpen.bind(this);
},

_onError: function(e) {
this._evtSrc.close();
delete this._evtSrc;

if (Zotero.Connector.isOnline) {
Zotero.Connector.isOnline = false;
Zotero.Connector_Browser.onStateChange(false);
Zotero.debug('Zotero client went offline');
}

if (e.target.readyState != 1) {
// Attempt to reconnect every 10 secs
return setTimeout(this.init.bind(this), 10000);
}
// Immediately attempt to reconnect in case of a simple HTTP timeout
this.init();
},

_onMessage: function(e) {
var data = JSON.parse(e.data);
Zotero.debug(`SSE event '${data.event}':${JSON.stringify(data.data).substr(0, 100)}`);
if (data.event in this._listeners) {
this._listeners[data.event].forEach((l) => l.notify(data.data));
}
},

_onOpen: function() {
this.available = true;
Zotero.Connector.ping();
Zotero.debug('Zotero client is online');
},

_addEventListener: function(event, fn) {
if (event in this._listeners) {
this._listeners[event].push(fn);
} else {
this._listeners[event] = [fn];
}
return fn;
},

_removeEventListener: function(event, fn) {
if (event in this._listeners) {
this._listeners[event] = this._listeners[event].filter((l) => l !== listener);
}
}
};
Zotero.Connector.addEventListener = Zotero.Connector.SSE._addEventListener.bind(Zotero.Connector.SSE);
Zotero.Connector.removeEventListener = Zotero.Connector.SSE._removeEventListener.bind(Zotero.Connector.SSE);


// TODO: this does not belong here in the slightest
Zotero.Connector_Debug = new function() {
/**
* Call a callback depending upon whether debug output is being stored
Expand Down
8 changes: 4 additions & 4 deletions src/common/inject/inject.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ if(isTopWindow) {
if (headline) {
return Zotero.ProgressWindow.changeHeadline(headline);
}
return Zotero.Connector.callMethod("getSelectedCollection", {}).then(function(response) {
return Zotero.Connector.getSelectedCollection().then(function(response) {
Zotero.ProgressWindow.changeHeadline("Saving to ",
response.id ? "treesource-collection.png" : "treesource-library.png",
response.name+"\u2026");
if (response.libraryEditable === false) {
response.collection.name ? "treesource-collection.png" : "treesource-library.png",
response.collection.name || response.library.name +"\u2026");
if (response.library.editable === false) {
new Zotero.ProgressWindow.ErrorMessage("collectionNotEditable");
Zotero.ProgressWindow.startCloseTimer(8000);
}
Expand Down
3 changes: 2 additions & 1 deletion src/common/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ var MESSAGES = {
Connector: {
checkIsOnline: true,
callMethod: true,
callMethodWithCookies: true
callMethodWithCookies: true,
getSelectedCollection: true,
},
Connector_Browser: {
onSelect: true,
Expand Down
1 change: 1 addition & 0 deletions src/common/test/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@
<script src="tests/preferencesTest.js"></script>
<script src="tests/translationTest.js"></script>
<script src="tests/httpTest.js"></script>
<script src="tests/connectorTest.js"></script>
</body>
</html>
39 changes: 39 additions & 0 deletions src/common/test/tests/connectorTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,43 @@ describe('Connector', function() {
throw new Error('Expected error not thrown');
}));
});

describe('#getSelectedCollection()', function() {
it('throws if Zotero is offline', Promise.coroutine(function* () {
try {
yield background(function() {
Zotero.Connector.isOnline = false;
return Zotero.Connector.getSelectedCollection()
});
} catch (e) {
assert.equal(e.status, 0);
return;
}
throw new Error('Error not thrown');
}));

it('gets an SSE result if SSE available', Promise.coroutine(function*() {
let s = yield background(function() {
Zotero.Connector.isOnline = true;
Zotero.Connector.SSE.available = true;
Zotero.Connector._selected = {collection: 'selected'};
return Zotero.Connector.getSelectedCollection()
});
assert.equal(s, 'selected');
}));
it('calls Zotero if SSE unavailable', Promise.coroutine(function*() {
let call = yield background(function() {
Zotero.Connector.isOnline = true;
Zotero.Connector.SSE.available = false;
Zotero.Connector._selected = {collection: 'selected'};
sinon.stub(Zotero.Connector, 'callMethod');
return Zotero.Connector.getSelectedCollection().then(function() {
let call = Zotero.Connector.callMethod.lastCall;
Zotero.Connector.callMethod.restore();
return call
});
});
assert.equal(call.args[0], 'getSelectedCollection');
}));
});
});
2 changes: 1 addition & 1 deletion src/common/zotero.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ var Zotero = new function() {
return Zotero.Prefs.init().then(function() {
Zotero.Debug.init();
Zotero.Messaging.init();
Zotero.Connector_Types.init();
Zotero.Repo.init();
Zotero.Connector.init();
if (Zotero.isBrowserExt) {
Zotero.WebRequestIntercept.init();
Zotero.Proxies.init();
Expand Down

0 comments on commit f7119cf

Please sign in to comment.