// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
* This class provides an interface between the HostController and either the
* NativeMessaging Host or the Host NPAPI plugin, depending on whether or not
* NativeMessaging is supported. Since the test for NativeMessaging support is
* asynchronous, this class stores any requests on a queue, pending the result
* of the test.
* Once the test is complete, the pending requests are performed on either the
* NativeMessaging host, or the NPAPI plugin.
*
* If necessary, the HostController is instructed (via a callback) to
* instantiate the NPAPI plugin, and return a reference to it here.
*/
'use strict';
/** @suppress {duplicate} */
var remoting = remoting || {};
/**
* @constructor
* @param {function():remoting.HostPlugin} createPluginCallback Callback to
* instantiate the NPAPI plugin when NativeMessaging is determined to be
* unsupported.
*/
remoting.HostDispatcher = function(createPluginCallback) {
/** @type {remoting.HostDispatcher} */
var that = this;
/** @type {remoting.HostNativeMessaging} @private */
this.nativeMessagingHost_ = new remoting.HostNativeMessaging();
/** @type {remoting.HostPlugin} @private */
this.npapiHost_ = null;
/** @type {remoting.HostDispatcher.State} */
this.state_ = remoting.HostDispatcher.State.UNKNOWN;
/** @type {Array.<function()>} */
this.pendingRequests_ = [];
function sendPendingRequests() {
for (var i = 0; i < that.pendingRequests_.length; i++) {
that.pendingRequests_[i]();
}
that.pendingRequests_ = null;
}
function onNativeMessagingInit() {
console.log('Native Messaging supported.');
that.state_ = remoting.HostDispatcher.State.NATIVE_MESSAGING;
sendPendingRequests();
}
function onNativeMessagingFailed(error) {
console.log('Native Messaging unsupported, falling back to NPAPI.');
that.npapiHost_ = createPluginCallback();
that.state_ = remoting.HostDispatcher.State.NPAPI;
sendPendingRequests();
}
this.nativeMessagingHost_.initialize(onNativeMessagingInit,
onNativeMessagingFailed);
};
/** @enum {number} */
remoting.HostDispatcher.State = {
UNKNOWN: 0,
NATIVE_MESSAGING: 1,
NPAPI: 2
};
/**
* @param {remoting.HostController.Feature} feature The feature to test for.
* @param {function(boolean):void} onDone
* @return {void}
*/
remoting.HostDispatcher.prototype.hasFeature = function(
feature, onDone) {
switch (this.state_) {
case remoting.HostDispatcher.State.UNKNOWN:
this.pendingRequests_.push(
this.hasFeature.bind(this, feature, onDone));
break;
case remoting.HostDispatcher.State.NATIVE_MESSAGING:
onDone(this.nativeMessagingHost_.hasFeature(feature));
break;
case remoting.HostDispatcher.State.NPAPI:
// If this is an old NPAPI plugin that doesn't list supportedFeatures,
// assume it is an old plugin that doesn't support any new feature.
var supportedFeatures = [];
if (typeof(this.npapiHost_.supportedFeatures) == 'string') {
supportedFeatures = this.npapiHost_.supportedFeatures.split(' ');
}
onDone(supportedFeatures.indexOf(feature) >= 0);
break;
}
};
/**
* @param {function(string):void} onDone
* @param {function(remoting.Error):void} onError
* @return {void}
*/
remoting.HostDispatcher.prototype.getHostName = function(onDone, onError) {
switch (this.state_) {
case remoting.HostDispatcher.State.UNKNOWN:
this.pendingRequests_.push(
this.getHostName.bind(this, onDone, onError));
break;
case remoting.HostDispatcher.State.NATIVE_MESSAGING:
this.nativeMessagingHost_.getHostName(onDone, onError);
break;
case remoting.HostDispatcher.State.NPAPI:
try {
this.npapiHost_.getHostName(onDone);
} catch (err) {
onError(remoting.Error.MISSING_PLUGIN);
}
break;
}
};
/**
* @param {string} hostId
* @param {string} pin
* @param {function(string):void} onDone
* @param {function(remoting.Error):void} onError
* @return {void}
*/
remoting.HostDispatcher.prototype.getPinHash =
function(hostId, pin, onDone, onError) {
switch (this.state_) {
case remoting.HostDispatcher.State.UNKNOWN:
this.pendingRequests_.push(
this.getPinHash.bind(this, hostId, pin, onDone, onError));
break;
case remoting.HostDispatcher.State.NATIVE_MESSAGING:
this.nativeMessagingHost_.getPinHash(hostId, pin, onDone, onError);
break;
case remoting.HostDispatcher.State.NPAPI:
try {
this.npapiHost_.getPinHash(hostId, pin, onDone);
} catch (err) {
onError(remoting.Error.MISSING_PLUGIN);
}
break;
}
};
/**
* @param {function(string, string):void} onDone
* @param {function(remoting.Error):void} onError
* @return {void}
*/
remoting.HostDispatcher.prototype.generateKeyPair = function(onDone, onError) {
switch (this.state_) {
case remoting.HostDispatcher.State.UNKNOWN:
this.pendingRequests_.push(
this.generateKeyPair.bind(this, onDone, onError));
break;
case remoting.HostDispatcher.State.NATIVE_MESSAGING:
this.nativeMessagingHost_.generateKeyPair(onDone, onError);
break;
case remoting.HostDispatcher.State.NPAPI:
try {
this.npapiHost_.generateKeyPair(onDone);
} catch (err) {
onError(remoting.Error.MISSING_PLUGIN);
}
break;
}
};
/**
* @param {Object} config
* @param {function(remoting.HostController.AsyncResult):void} onDone
* @param {function(remoting.Error):void} onError
* @return {void}
*/
remoting.HostDispatcher.prototype.updateDaemonConfig =
function(config, onDone, onError) {
switch (this.state_) {
case remoting.HostDispatcher.State.UNKNOWN:
this.pendingRequests_.push(
this.updateDaemonConfig.bind(this, config, onDone, onError));
break;
case remoting.HostDispatcher.State.NATIVE_MESSAGING:
this.nativeMessagingHost_.updateDaemonConfig(config, onDone, onError);
break;
case remoting.HostDispatcher.State.NPAPI:
try {
this.npapiHost_.updateDaemonConfig(JSON.stringify(config), onDone);
} catch (err) {
onError(remoting.Error.MISSING_PLUGIN);
}
break;
}
};
/**
* @param {function(Object):void} onDone
* @param {function(remoting.Error):void} onError
* @return {void}
*/
remoting.HostDispatcher.prototype.getDaemonConfig = function(onDone, onError) {
/**
* Converts the config string from the NPAPI plugin to an Object, to pass to
* |onDone|.
* @param {string} configStr
* @return {void}
*/
function callbackForNpapi(configStr) {
var config = jsonParseSafe(configStr);
if (typeof(config) != 'object') {
onError(remoting.Error.UNEXPECTED);
} else {
onDone(/** @type {Object} */ (config));
}
}
switch (this.state_) {
case remoting.HostDispatcher.State.UNKNOWN:
this.pendingRequests_.push(
this.getDaemonConfig.bind(this, onDone, onError));
break;
case remoting.HostDispatcher.State.NATIVE_MESSAGING:
this.nativeMessagingHost_.getDaemonConfig(onDone, onError);
break;
case remoting.HostDispatcher.State.NPAPI:
try {
this.npapiHost_.getDaemonConfig(callbackForNpapi);
} catch (err) {
onError(remoting.Error.MISSING_PLUGIN);
}
break;
}
};
/**
* @param {function(string):void} onDone
* @param {function(remoting.Error):void} onError
* @return {void}
*/
remoting.HostDispatcher.prototype.getDaemonVersion = function(onDone, onError) {
switch (this.state_) {
case remoting.HostDispatcher.State.UNKNOWN:
this.pendingRequests_.push(
this.getDaemonVersion.bind(this, onDone, onError));
break;
case remoting.HostDispatcher.State.NATIVE_MESSAGING:
onDone(this.nativeMessagingHost_.getDaemonVersion());
break;
case remoting.HostDispatcher.State.NPAPI:
try {
this.npapiHost_.getDaemonVersion(onDone);
} catch (err) {
onError(remoting.Error.MISSING_PLUGIN);
}
break;
}
};
/**
* @param {function(boolean, boolean, boolean):void} onDone
* @param {function(remoting.Error):void} onError
* @return {void}
*/
remoting.HostDispatcher.prototype.getUsageStatsConsent =
function(onDone, onError) {
switch (this.state_) {
case remoting.HostDispatcher.State.UNKNOWN:
this.pendingRequests_.push(
this.getUsageStatsConsent.bind(this, onDone, onError));
break;
case remoting.HostDispatcher.State.NATIVE_MESSAGING:
this.nativeMessagingHost_.getUsageStatsConsent(onDone, onError);
break;
case remoting.HostDispatcher.State.NPAPI:
try {
this.npapiHost_.getUsageStatsConsent(onDone);
} catch (err) {
onError(remoting.Error.MISSING_PLUGIN);
}
break;
}
};
/**
* @param {Object} config
* @param {boolean} consent
* @param {function(remoting.HostController.AsyncResult):void} onDone
* @param {function(remoting.Error):void} onError
* @return {void}
*/
remoting.HostDispatcher.prototype.startDaemon =
function(config, consent, onDone, onError) {
switch (this.state_) {
case remoting.HostDispatcher.State.UNKNOWN:
this.pendingRequests_.push(
this.startDaemon.bind(this, config, consent, onDone, onError));
break;
case remoting.HostDispatcher.State.NATIVE_MESSAGING:
this.nativeMessagingHost_.startDaemon(config, consent, onDone, onError);
break;
case remoting.HostDispatcher.State.NPAPI:
try {
this.npapiHost_.startDaemon(JSON.stringify(config), consent, onDone);
} catch (err) {
onError(remoting.Error.MISSING_PLUGIN);
}
break;
}
};
/**
* @param {function(remoting.HostController.AsyncResult):void} onDone
* @param {function(remoting.Error):void} onError
* @return {void}
*/
remoting.HostDispatcher.prototype.stopDaemon = function(onDone, onError) {
switch (this.state_) {
case remoting.HostDispatcher.State.UNKNOWN:
this.pendingRequests_.push(this.stopDaemon.bind(this, onDone, onError));
break;
case remoting.HostDispatcher.State.NATIVE_MESSAGING:
this.nativeMessagingHost_.stopDaemon(onDone, onError);
break;
case remoting.HostDispatcher.State.NPAPI:
try {
this.npapiHost_.stopDaemon(onDone);
} catch (err) {
onError(remoting.Error.MISSING_PLUGIN);
}
break;
}
};
/**
* @param {function(remoting.HostController.State):void} onDone
* @param {function(remoting.Error):void} onError
* @return {void}
*/
remoting.HostDispatcher.prototype.getDaemonState = function(onDone, onError) {
switch (this.state_) {
case remoting.HostDispatcher.State.UNKNOWN:
this.pendingRequests_.push(
this.getDaemonState.bind(this, onDone, onError));
break;
case remoting.HostDispatcher.State.NATIVE_MESSAGING:
this.nativeMessagingHost_.getDaemonState(onDone, onError);
break;
case remoting.HostDispatcher.State.NPAPI:
// Call the callback directly, since NPAPI exposes the state directly as
// a field member, rather than an asynchronous method.
var state = this.npapiHost_.daemonState;
if (state === undefined) {
onError(remoting.Error.MISSING_PLUGIN);
} else {
onDone(state);
}
break;
}
};
/**
* @param {function(Array.<remoting.PairedClient>):void} onDone
* @param {function(remoting.Error):void} onError
* @return {void}
*/
remoting.HostDispatcher.prototype.getPairedClients = function(onDone, onError) {
/**
* Converts the JSON string from the NPAPI plugin to Array.<PairedClient>, to
* pass to |onDone|.
* @param {string} pairedClientsJson
* @return {void}
*/
function callbackForNpapi(pairedClientsJson) {
var pairedClients = remoting.PairedClient.convertToPairedClientArray(
jsonParseSafe(pairedClientsJson));
if (pairedClients != null) {
onDone(pairedClients);
} else {
onError(remoting.Error.UNEXPECTED);
}
}
switch (this.state_) {
case remoting.HostDispatcher.State.UNKNOWN:
this.pendingRequests_.push(
this.getPairedClients.bind(this, onDone, onError));
break;
case remoting.HostDispatcher.State.NATIVE_MESSAGING:
this.nativeMessagingHost_.getPairedClients(onDone, onError);
break;
case remoting.HostDispatcher.State.NPAPI:
try {
this.npapiHost_.getPairedClients(callbackForNpapi);
} catch (err) {
onError(remoting.Error.MISSING_PLUGIN);
}
break;
}
};
/**
* The pairing API returns a boolean to indicate success or failure, but
* the JS API is defined in terms of onDone and onError callbacks. This
* function converts one to the other.
*
* @param {function():void} onDone Success callback.
* @param {function(remoting.Error):void} onError Error callback.
* @param {boolean} success True if the operation succeeded; false otherwise.
* @private
*/
remoting.HostDispatcher.runCallback_ = function(onDone, onError, success) {
if (success) {
onDone();
} else {
onError(remoting.Error.UNEXPECTED);
}
};
/**
* @param {function():void} onDone
* @param {function(remoting.Error):void} onError
* @return {void}
*/
remoting.HostDispatcher.prototype.clearPairedClients =
function(onDone, onError) {
var callback =
remoting.HostDispatcher.runCallback_.bind(null, onDone, onError);
switch (this.state_) {
case remoting.HostDispatcher.State.UNKNOWN:
this.pendingRequests_.push(
this.clearPairedClients.bind(this, onDone, onError));
break;
case remoting.HostDispatcher.State.NATIVE_MESSAGING:
this.nativeMessagingHost_.clearPairedClients(callback, onError);
break;
case remoting.HostDispatcher.State.NPAPI:
try {
this.npapiHost_.clearPairedClients(callback);
} catch (err) {
onError(remoting.Error.MISSING_PLUGIN);
}
break;
}
};
/**
* @param {string} client
* @param {function():void} onDone
* @param {function(remoting.Error):void} onError
* @return {void}
*/
remoting.HostDispatcher.prototype.deletePairedClient =
function(client, onDone, onError) {
var callback =
remoting.HostDispatcher.runCallback_.bind(null, onDone, onError);
switch (this.state_) {
case remoting.HostDispatcher.State.UNKNOWN:
this.pendingRequests_.push(
this.deletePairedClient.bind(this, client, onDone, onError));
break;
case remoting.HostDispatcher.State.NATIVE_MESSAGING:
this.nativeMessagingHost_.deletePairedClient(client, callback, onError);
break;
case remoting.HostDispatcher.State.NPAPI:
try {
this.npapiHost_.deletePairedClient(client, callback);
} catch (err) {
onError(remoting.Error.MISSING_PLUGIN);
}
break;
}
};
/**
* @param {function(string):void} onDone
* @param {function(remoting.Error):void} onError
* @return {void}
*/
remoting.HostDispatcher.prototype.getHostClientId =
function(onDone, onError) {
switch (this.state_) {
case remoting.HostDispatcher.State.UNKNOWN:
this.pendingRequests_.push(
this.getHostClientId.bind(this, onDone, onError));
break;
case remoting.HostDispatcher.State.NATIVE_MESSAGING:
this.nativeMessagingHost_.getHostClientId(onDone, onError);
break;
case remoting.HostDispatcher.State.NPAPI:
// The NPAPI plugin is packaged with the webapp, not the host, so it
// doesn't have access to the API keys baked into the installed host.
onError(remoting.Error.UNEXPECTED);
break;
}
};
/**
* @param {string} authorizationCode
* @param {function(string, string):void} onDone
* @param {function(remoting.Error):void} onError
* @return {void}
*/
remoting.HostDispatcher.prototype.getCredentialsFromAuthCode =
function(authorizationCode, onDone, onError) {
switch (this.state_) {
case remoting.HostDispatcher.State.UNKNOWN:
this.pendingRequests_.push(
this.getCredentialsFromAuthCode.bind(
this, authorizationCode, onDone, onError));
break;
case remoting.HostDispatcher.State.NATIVE_MESSAGING:
this.nativeMessagingHost_.getCredentialsFromAuthCode(
authorizationCode, onDone, onError);
break;
case remoting.HostDispatcher.State.NPAPI:
// The NPAPI plugin is packaged with the webapp, not the host, so it
// doesn't have access to the API keys baked into the installed host.
onError(remoting.Error.UNEXPECTED);
break;
}
};