javascriptrubyinternet-explorerbho

WIN32OLE - Inject Ruby function into Internet Explorer's JavaScript


In AHK, the following code can be used to inject a AHK function into Internet Explorer's JavaScript engine:

#Persistent

html =
(
<html>
  <head>
    <script>
      document.setVar = function`(name,val`){
        document[name]=val;
      }
    </script>
  </head>
  <body>
    <h1>Hello!</h1>
  </body>
</html>
)

ie := ComObjCreate("InternetExplorer.Application")
ie.navigate("about:blank")
sleep, 2000


msgbox, %html%
ie.document.writeln(html)
ie.visible := true
ie.document.setVar("someFunc",Func("someFunc"))


someFunc(){
  msgbox, "hello"
}

After injecting the function, JavaScript can call document.someFunc(), which will result in JavaScript calling the AHK function, and ultimately running a message box.

I want to port this code to Ruby. So far I have this:

require 'win32ole'
ie = WIN32OLE.new('InternetExplorer.Application')
ie.navigate("about:blank")
sleep(0.1) until !ie.busy
html = <<Heredoc
<html>
  <head>
    <script>
      document.setVar = function(name,val){
        document[name]=val;
      }
    </script>
  </head>
  <body>
    <h1>Hello!</h1>
  </body>
</html>
Heredoc

ie.document.writeln(html)
ie.visible = true

Now that we are here we should be able to inject the Ruby method, however currently I have no idea how to implement this. Every time I try something, the JavaScript engine freezes. Some of the things I have tried:

ie.document.setVar("someFunc",method(:someFunc))
#----------------------------------
ie.document.setVar("someFunc",->{puts "hello"})
#----------------------------------
class someClass
  def someFunc
    puts "hello"
  end
end
ie.document.setVar("someClass",someClass})
#----------------------------------
closure = Class.new(Fiddle::Closure) {
  def call
    puts "hello world"
  end
}.new(Fiddle::TYPE_INT,[])
someFunc = Fiddle::Function.new(closure, [], Fiddle::TYPE_INT)
#Both:
doc.setVar("someFunc",closure)
#and
doc.setVar("someFunc",someFunc)
#----------------------------------

None of the above methods work. In the end they all end up freezing the JavaScript engine... Does anyone have any idea how I can pass an actual reference to the Ruby function to JavaScript?


Solution

  • After a week of scouring the internet and finding nothing I thought "What if we are naive and pass an object with method_missing defined?"

    30 minutes later, and I can call Ruby from JavaScript.

    class MyFunc
      # Called when no arguments are passed to JavaScript function
      def call
        #Execute any ruby code here!!
    
        #You can also return values back to JavaScript!!
        return 1
      end
    
      # Called when arguments are passed to JavaScript function
      def value(*args)
        if args.length == 0
          # This will be called if the function is called without parenthesis in JS
          # e.g. console.log(document.someFunc)
          return nil
        else
          #This is called with the parsed arguments. Note: Functions passed in from JS are of type WIN32OLE. Theoretically this should be callable, but better would be to make a JS function which can call other JS functions
          #Execute any ruby code here!!
          puts "#{args.inspect}"
    
          #Can also return values here as well
          return 1
        end
      end
    end
    ie.document.setVar("myFunc",MyFunc.new})
    

    You can also initialise and access instance variables:

    class MyClass
      def initialize
        @hello = "world"
      end
    end
    ie.document.setVar("myClass",MyClass.new})
    
    #IN IE: document.myClass["hello"] //=> "world"
    

    Note:

    Some things can go drastically wrong and even cause ruby crashes. Some examples of things that don't work:

    Some things also make little sense at all, for example I made the following loop:

    Given a class:

    class MyClass
      def call
        puts "Call"
      end
      def method_missing(m,*args,&block)
        puts "#{m}(#{args.inspect})"
      end
    end
    ie.document.setVar("obj",MyClass.new)
    

    and JavaScript:

    for(var i=0;i<24;i++){
      document.obj[chars[i]]()
    }
    

    This will execute every function of obj named with an alphabetical character. And indeed, it does do this for the most part, however sometimes it does not. Sometimes it will call the main call method, and in the case of document.obj.j() it will do nothing at all...

    Full log:

    a([])
    b([])
    c([])
    d([])
    e([])
    f([])
    Hello world
    h([])
    i([])
    k([])
    Hello world
    m([])
    n([])
    o([])
    q([])
    s([])
    t([])
    Hello world
    v([])
    w([])
    x([])
    y([])
    Hello world
    

    EDIT

    I've written a GIST which makes this generally much easier to achieve. E.G. To pass the File object to IE you can do the following:

    ie.document.setVar("RubyFile",WIN32OLE::getDispatch(File))