// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Automation connection handler is responsible for reading requests from the
// stream, finding and executing appropriate extension API method.
function ConnectionHandler() {
// Event listener registration map socket->event->callback
this.eventListener_ = {};
}
ConnectionHandler.prototype = {
// Stream delegate callback.
onStreamError: function(stream) {
this.unregisterListeners_(stream);
},
// Stream delegate callback.
onStreamTerminated: function(stream) {
this.unregisterListeners_(stream);
},
// Pairs event |listenerMethod| with a given |stream|.
registerListener_: function(stream, eventName, eventObject,
listenerMethod) {
if (!this.eventListener_[stream.socketId_])
this.eventListener_[stream.socketId_] = {};
if (!this.eventListener_[stream.socketId_][eventName]) {
this.eventListener_[stream.socketId_][eventName] = {
'event': eventObject,
'method': listenerMethod };
}
},
// Removes event listeners.
unregisterListeners_: function(stream) {
if (!this.eventListener_[stream.socketId_])
return;
for (var eventName in this.eventListener_[stream.socketId_]) {
var listenerDefinition = this.eventListener_[stream.socketId_][eventName];
var removeFunction = listenerDefinition.event['removeListener'];
if (removeFunction) {
removeFunction.call(listenerDefinition.event,
listenerDefinition.method);
}
}
delete this.eventListener_[stream.socketId_];
},
// Finds appropriate method/event to invoke/register.
findExecutionTarget_: function(functionName) {
var funcSegments = functionName.split('.');
if (funcSegments.size < 2)
return null;
if (funcSegments[0] != 'chrome')
return null;
var eventName = "";
var prevSegName = null;
var prevSegment = null;
var segmentObject = null;
var segName = null;
for (var i = 0; i < funcSegments.length; i++) {
if (prevSegName) {
if (eventName.length)
eventName += '.';
eventName += prevSegName;
}
segName = funcSegments[i];
prevSegName = segName;
if (!segmentObject) {
// TODO(zelidrag): Get rid of this eval.
segmentObject = eval(segName);
continue;
}
prevSegment = segmentObject;
if (segmentObject[segName])
segmentObject = segmentObject[segName];
else
segmentObject = null;
}
if (segmentObject == window)
return null;
var isEventMethod = segName == 'addListener';
return {'method': segmentObject,
'eventName': (isEventMethod ? eventName : null),
'event': (isEventMethod ? prevSegment : null)};
},
// TODO(zelidrag): Figure out how to automatically detect or generate list of
// sync API methods.
isSyncFunction_: function(funcName) {
if (funcName == 'chrome.omnibox.setDefaultSuggestion')
return true;
return false;
},
// Parses |command|, finds appropriate JS method runs it with |argsJson|.
// If the method is an event registration, it will register an event listener
// method and start sending data from its callback.
processCommand_: function(stream, command, argsJson) {
var target = this.findExecutionTarget_(command);
if (!target || !target.method) {
return {'result': false,
'objectName': command};
}
var args = JSON.parse(decodeURIComponent(argsJson));
if (!args)
args = [];
console.log(command + '(' + decodeURIComponent(argsJson) + ')',
stream.socketId_);
// Check if we need to register an event listener.
if (target.event) {
// Register listener method.
var listener = function() {
stream.write(JSON.stringify({ 'type': 'eventCallback',
'eventName': target.eventName,
'arguments' : arguments}));
}.bind(this);
// Add event handler method to arguments.
args.push(listener);
args.push(null); // for |filters|.
target.method.apply(target.event, args);
this.registerListener_(stream, target.eventName,
target.event, listener);
stream.write(JSON.stringify({'type': 'eventRegistration',
'eventName': command}));
return {'result': true,
'wasEvent': true};
}
// Run extension method directly.
if (this.isSyncFunction_(command)) {
// Run sync method.
console.log(command + '(' + unescape(argsJson) + ')');
var result = target.method.apply(undefined, args);
stream.write(JSON.stringify({'type': 'methodResult',
'methodName': command,
'isCallback': false,
'result' : result}));
} else { // Async method.
// Add callback method to arguments.
args.push(function() {
stream.write(JSON.stringify({'type': 'methodCallback',
'methodName': command,
'isCallback': true,
'arguments' : arguments}));
}.bind(this));
target.method.apply(undefined, args);
}
return {'result': true,
'wasEvent': false};
},
arrayBufferToString_: function(buffer) {
var str = '';
var uArrayVal = new Uint8Array(buffer);
for(var s = 0; s < uArrayVal.length; s++) {
str += String.fromCharCode(uArrayVal[s]);
}
return str;
},
// Callback for stream read requests.
onStreamRead_: function(stream, readInfo) {
console.log("READ", readInfo);
// Parse the request.
var data = this.arrayBufferToString_(readInfo.data);
var spacePos = data.indexOf(" ");
try {
if (spacePos == -1) {
spacePos = data.indexOf("\r\n");
if (spacePos == -1)
throw {'code': 400, 'description': 'Bad Request'};
}
var verb = data.substring(0, spacePos);
var isEvent = false;
switch (verb) {
case 'TERMINATE':
throw {'code': 200, 'description': 'OK'};
break;
case 'RUN':
break;
case 'LISTEN':
this.isEvent = true;
break;
default:
throw {'code': 400, 'description': 'Bad Request: ' + verb};
return;
}
var command = data.substring(verb.length + 1);
var endLine = command.indexOf('\r\n');
if (endLine)
command = command.substring(0, endLine);
var objectNames = command;
var argsJson = null;
var funcNameEnd = command.indexOf("?");
if (funcNameEnd >= 0) {
objectNames = command.substring(0, funcNameEnd);
argsJson = command.substring(funcNameEnd + 1);
}
var functions = objectNames.split(',');
for (var i = 0; i < functions.length; i++) {
var objectName = functions[i];
var commandStatus =
this.processCommand_(stream, objectName, argsJson);
if (!commandStatus.result) {
throw {'code': 404,
'description': 'Not Found: ' + commandStatus.objectName};
}
// If we have run all requested commands, read the socket again.
if (i == (functions.length - 1)) {
setTimeout(function() {
this.readRequest_(stream);
}.bind(this), 0);
}
}
} catch(err) {
console.warn('Error', err);
stream.writeError(err.code, err.description);
}
},
// Reads next request from the |stream|.
readRequest_: function(stream) {
console.log("Reading socket " + stream.socketId_);
// Read in the data
stream.read(this.onStreamRead_.bind(this));
}
};