1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// Automation connection handler is responsible for reading requests from the
6// stream, finding and executing appropriate extension API method.
7function ConnectionHandler() {
8  // Event listener registration map socket->event->callback
9  this.eventListener_ = {};
10}
11
12ConnectionHandler.prototype = {
13  // Stream delegate callback.
14  onStreamError: function(stream) {
15    this.unregisterListeners_(stream);
16  },
17
18  // Stream delegate callback.
19  onStreamTerminated: function(stream) {
20    this.unregisterListeners_(stream);
21  },
22
23  // Pairs event |listenerMethod| with a given |stream|.
24  registerListener_: function(stream, eventName, eventObject,
25                              listenerMethod) {
26    if (!this.eventListener_[stream.socketId_])
27      this.eventListener_[stream.socketId_] = {};
28
29    if (!this.eventListener_[stream.socketId_][eventName]) {
30      this.eventListener_[stream.socketId_][eventName] = {
31          'event': eventObject,
32          'method': listenerMethod };
33    }
34  },
35
36  // Removes event listeners.
37  unregisterListeners_: function(stream) {
38    if (!this.eventListener_[stream.socketId_])
39    return;
40
41    for (var eventName in this.eventListener_[stream.socketId_]) {
42      var listenerDefinition = this.eventListener_[stream.socketId_][eventName];
43      var removeFunction = listenerDefinition.event['removeListener'];
44      if (removeFunction) {
45        removeFunction.call(listenerDefinition.event,
46                            listenerDefinition.method);
47      }
48    }
49    delete this.eventListener_[stream.socketId_];
50  },
51
52  // Finds appropriate method/event to invoke/register.
53  findExecutionTarget_: function(functionName) {
54    var funcSegments = functionName.split('.');
55    if (funcSegments.size < 2)
56      return null;
57
58    if (funcSegments[0] != 'chrome')
59      return null;
60
61    var eventName = "";
62    var prevSegName = null;
63    var prevSegment = null;
64    var segmentObject = null;
65    var segName = null;
66    for (var i = 0; i < funcSegments.length; i++) {
67      if (prevSegName) {
68        if (eventName.length)
69          eventName += '.';
70
71        eventName += prevSegName;
72      }
73
74      segName = funcSegments[i];
75      prevSegName = segName;
76      if (!segmentObject) {
77        // TODO(zelidrag): Get rid of this eval.
78        segmentObject = eval(segName);
79        continue;
80      }
81
82      prevSegment = segmentObject;
83      if (segmentObject[segName])
84        segmentObject = segmentObject[segName];
85      else
86        segmentObject = null;
87    }
88    if (segmentObject == window)
89      return null;
90
91    var isEventMethod = segName == 'addListener';
92    return {'method': segmentObject,
93            'eventName': (isEventMethod ? eventName : null),
94            'event': (isEventMethod ? prevSegment : null)};
95  },
96
97  // TODO(zelidrag): Figure out how to automatically detect or generate list of
98  // sync API methods.
99  isSyncFunction_: function(funcName) {
100    if (funcName == 'chrome.omnibox.setDefaultSuggestion')
101      return true;
102
103    return false;
104  },
105
106  // Parses |command|, finds appropriate JS method runs it with |argsJson|.
107  // If the method is an event registration, it will register an event listener
108  // method and start sending data from its callback.
109  processCommand_: function(stream, command, argsJson) {
110    var target = this.findExecutionTarget_(command);
111    if (!target || !target.method) {
112      return {'result': false,
113              'objectName': command};
114    }
115
116    var args = JSON.parse(decodeURIComponent(argsJson));
117    if (!args)
118      args = [];
119
120    console.log(command + '(' + decodeURIComponent(argsJson) + ')',
121                stream.socketId_);
122    // Check if we need to register an event listener.
123    if (target.event) {
124      // Register listener method.
125      var listener = function() {
126        stream.write(JSON.stringify({ 'type': 'eventCallback',
127                                      'eventName': target.eventName,
128                                      'arguments' : arguments}));
129      }.bind(this);
130      // Add event handler method to arguments.
131      args.push(listener);
132      args.push(null);    // for |filters|.
133      target.method.apply(target.event, args);
134      this.registerListener_(stream, target.eventName,
135                             target.event, listener);
136      stream.write(JSON.stringify({'type': 'eventRegistration',
137                                   'eventName': command}));
138      return {'result': true,
139              'wasEvent': true};
140    }
141
142    // Run extension method directly.
143    if (this.isSyncFunction_(command)) {
144      // Run sync method.
145      console.log(command + '(' + unescape(argsJson) + ')');
146      var result = target.method.apply(undefined, args);
147      stream.write(JSON.stringify({'type': 'methodResult',
148                                   'methodName': command,
149                                   'isCallback': false,
150                                   'result' : result}));
151    } else {    // Async method.
152      // Add callback method to arguments.
153      args.push(function() {
154        stream.write(JSON.stringify({'type': 'methodCallback',
155                                     'methodName': command,
156                                     'isCallback': true,
157                                     'arguments' : arguments}));
158      }.bind(this));
159      target.method.apply(undefined, args);
160    }
161    return {'result': true,
162            'wasEvent': false};
163  },
164
165  arrayBufferToString_: function(buffer) {
166    var str = '';
167    var uArrayVal = new Uint8Array(buffer);
168    for(var s = 0; s < uArrayVal.length; s++) {
169      str += String.fromCharCode(uArrayVal[s]);
170    }
171    return str;
172  },
173
174  // Callback for stream read requests.
175  onStreamRead_: function(stream, readInfo) {
176    console.log("READ", readInfo);
177    // Parse the request.
178    var data = this.arrayBufferToString_(readInfo.data);
179    var spacePos = data.indexOf(" ");
180    try {
181      if (spacePos == -1) {
182        spacePos = data.indexOf("\r\n");
183        if (spacePos == -1)
184          throw {'code': 400, 'description': 'Bad Request'};
185      }
186
187      var verb = data.substring(0, spacePos);
188      var isEvent = false;
189      switch (verb) {
190        case 'TERMINATE':
191          throw {'code': 200, 'description': 'OK'};
192          break;
193        case 'RUN':
194          break;
195        case 'LISTEN':
196          this.isEvent = true;
197          break;
198        default:
199          throw {'code': 400, 'description': 'Bad Request: ' + verb};
200          return;
201      }
202
203      var command = data.substring(verb.length + 1);
204      var endLine = command.indexOf('\r\n');
205      if (endLine)
206        command = command.substring(0, endLine);
207
208      var objectNames = command;
209      var argsJson = null;
210      var funcNameEnd =  command.indexOf("?");
211      if (funcNameEnd >= 0) {
212        objectNames = command.substring(0, funcNameEnd);
213        argsJson = command.substring(funcNameEnd + 1);
214      }
215      var functions = objectNames.split(',');
216      for (var i = 0; i < functions.length; i++) {
217        var objectName = functions[i];
218        var commandStatus =
219            this.processCommand_(stream, objectName, argsJson);
220        if (!commandStatus.result) {
221          throw {'code': 404,
222                 'description': 'Not Found: ' + commandStatus.objectName};
223        }
224        // If we have run all requested commands, read the socket again.
225        if (i == (functions.length - 1)) {
226          setTimeout(function() {
227            this.readRequest_(stream);
228          }.bind(this), 0);
229        }
230      }
231    } catch(err) {
232      console.warn('Error', err);
233      stream.writeError(err.code, err.description);
234    }
235  },
236
237  // Reads next request from the |stream|.
238  readRequest_: function(stream) {
239    console.log("Reading socket " + stream.socketId_);
240    //  Read in the data
241    stream.read(this.onStreamRead_.bind(this));
242  }
243};
244