Home | History | Annotate | Download | only in auto_provider
      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.
      7 function ConnectionHandler() {
      8   // Event listener registration map socket->event->callback
      9   this.eventListener_ = {};
     10 }
     11 
     12 ConnectionHandler.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