vapir-firefox 1.8.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/vapir-firefox/browser.rb +226 -115
- data/lib/vapir-firefox/clear_tracks.rb +6 -6
- data/lib/vapir-firefox/config.rb +5 -0
- data/lib/vapir-firefox/container.rb +74 -65
- data/lib/vapir-firefox/element.rb +13 -13
- data/lib/vapir-firefox/firefox_socket/base.rb +790 -0
- data/lib/vapir-firefox/firefox_socket/jssh.rb +49 -0
- data/lib/vapir-firefox/firefox_socket/mozrepl.rb +58 -0
- data/lib/vapir-firefox/{prototype.functional.js → firefox_socket/prototype.functional.js} +0 -0
- data/lib/vapir-firefox/javascript_object.rb +736 -0
- data/lib/vapir-firefox/modal_dialog.rb +4 -4
- data/lib/vapir-firefox/page_container.rb +4 -4
- data/lib/vapir-firefox/version.rb +1 -1
- metadata +16 -13
- data/lib/vapir-firefox/jssh_socket.rb +0 -1418
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'vapir-firefox/firefox_socket/base'
|
2
|
+
|
3
|
+
# A JsshSocket represents a connection to Firefox over a socket opened to the JSSH extension.
|
4
|
+
class JsshSocket < FirefoxSocket
|
5
|
+
@configuration_parent = FirefoxSocket.config
|
6
|
+
config.update_hash({
|
7
|
+
:port => 9997,
|
8
|
+
})
|
9
|
+
|
10
|
+
# returns an array of command line flags that should be used to invoke firefox for jssh
|
11
|
+
def self.command_line_flags(options={})
|
12
|
+
options = config.defined_hash.merge(options)
|
13
|
+
['-jssh', '-jssh-port', options['port']]
|
14
|
+
end
|
15
|
+
|
16
|
+
def eat_welcome_message
|
17
|
+
@prompt="\n> "
|
18
|
+
welcome="Welcome to the Mozilla JavaScript Shell!\n"
|
19
|
+
read=read_value
|
20
|
+
if !read
|
21
|
+
@expecting_extra_maybe=true
|
22
|
+
raise FirefoxSocketUnableToStart, "Something went wrong initializing - no response"
|
23
|
+
elsif read != welcome
|
24
|
+
@expecting_extra_maybe=true
|
25
|
+
raise FirefoxSocketUnableToStart, "Something went wrong initializing - message #{read.inspect} != #{welcome.inspect}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
def initialize_environment
|
29
|
+
# set up objects that are needed: nativeJSON_encode_length, VapirTemp, and Vapir
|
30
|
+
ret=send_and_read(%Q((function()
|
31
|
+
{ nativeJSON_encode_length=function(object)
|
32
|
+
{ var encoded=JSON.stringify(object);
|
33
|
+
return encoded.length.toString()+"\\n"+encoded;
|
34
|
+
}
|
35
|
+
VapirTemp = {};
|
36
|
+
Vapir = {};
|
37
|
+
return 'done!';
|
38
|
+
})()))
|
39
|
+
if ret != "done!"
|
40
|
+
@expecting_extra_maybe=true
|
41
|
+
raise FirefoxSocketError, "Something went wrong initializing environment - message #{ret.inspect}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# returns a JavascriptObject representing the return value of JSSH's builtin getWindows() function.
|
46
|
+
def getWindows
|
47
|
+
root.getWindows
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'vapir-firefox/firefox_socket/base'
|
2
|
+
|
3
|
+
# A MozreplSocket represents a connection to Firefox over a socket opened to the MozRepl extension.
|
4
|
+
class MozreplSocket < FirefoxSocket
|
5
|
+
@configuration_parent = FirefoxSocket.config
|
6
|
+
config.update_hash({
|
7
|
+
:port => 4242,
|
8
|
+
})
|
9
|
+
|
10
|
+
# returns an array of command line flags that should be used to invoke firefox for mozrepl
|
11
|
+
def self.command_line_flags(options={})
|
12
|
+
options = config.defined_hash.merge(options)
|
13
|
+
['-repl', options['port']]
|
14
|
+
end
|
15
|
+
|
16
|
+
def eat_welcome_message
|
17
|
+
read=read_value
|
18
|
+
if !read
|
19
|
+
@expecting_extra_maybe=true
|
20
|
+
raise FirefoxSocketUnableToStart, "Something went wrong initializing - no response"
|
21
|
+
elsif read !~ /Welcome to MozRepl/
|
22
|
+
@expecting_extra_maybe=true
|
23
|
+
raise FirefoxSocketUnableToStart, "Something went wrong initializing - message #{read.inspect}"
|
24
|
+
end
|
25
|
+
if read =~ /yours will be named "([^"]+)"/
|
26
|
+
@replname=$1
|
27
|
+
else
|
28
|
+
@replname='repl'
|
29
|
+
end
|
30
|
+
@prompt="#{@replname}> "
|
31
|
+
@expecting_prompt = read !~ /#{Regexp.escape(@prompt)}\z/
|
32
|
+
end
|
33
|
+
def initialize_environment
|
34
|
+
# change the prompt mode to something less decorative
|
35
|
+
#send_and_read("#{@replname}.home()")
|
36
|
+
send_and_read("#{@replname}.setenv('printPrompt', false)")
|
37
|
+
@prompt="\n"
|
38
|
+
@expecting_prompt=false
|
39
|
+
send_and_read("#{@replname}.setenv('inputMode', 'multiline')")
|
40
|
+
@input_terminator = "--end-remote-input\n"
|
41
|
+
|
42
|
+
# set up objects that are needed: nativeJSON_encode_length, VapirTemp, and Vapir
|
43
|
+
ret=send_and_read(%Q((function(the_repl, context)
|
44
|
+
{ context.nativeJSON_encode_length=function(object)
|
45
|
+
{ var encoded=JSON.stringify(object);
|
46
|
+
the_repl.print(encoded.length.toString()+"\\n"+encoded, false);
|
47
|
+
}
|
48
|
+
context.VapirTemp = {};
|
49
|
+
context.Vapir = {};
|
50
|
+
return 'done!';
|
51
|
+
})(#{@replname}, this)))
|
52
|
+
if ret !~ /done!/
|
53
|
+
@expecting_extra_maybe=true
|
54
|
+
raise FirefoxSocketError, "Something went wrong initializing environment - message #{ret.inspect}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
File without changes
|
@@ -0,0 +1,736 @@
|
|
1
|
+
# represents a javascript object in ruby.
|
2
|
+
class JavascriptObject
|
3
|
+
# the reference to the javascript object this JavascriptObject represents
|
4
|
+
attr_reader :ref
|
5
|
+
# the FirefoxSocket this JavascriptObject is on
|
6
|
+
attr_reader :firefox_socket
|
7
|
+
# whether this represents the result of a function call (if it does, then FirefoxSocket#typeof won't be called on it)
|
8
|
+
attr_reader :function_result
|
9
|
+
# this tracks the origins of this object - what calls were made along the way to get it.
|
10
|
+
attr_reader :debug_name
|
11
|
+
# :stopdoc:
|
12
|
+
# def logger
|
13
|
+
# firefox_socket.logger
|
14
|
+
# end
|
15
|
+
|
16
|
+
# :startdoc:
|
17
|
+
|
18
|
+
public
|
19
|
+
# initializes a JavascriptObject with a string of javascript containing a reference to
|
20
|
+
# the object, and a FirefoxSocket that the object is defined on.
|
21
|
+
def initialize(ref, firefox_socket, other={})
|
22
|
+
other={:debug_name => ref, :function_result => false}.merge(other)
|
23
|
+
raise ArgumentError, "Empty object reference!" if !ref || ref==''
|
24
|
+
raise ArgumentError, "Reference must be a string - got #{ref.inspect} (#{ref.class.name})" unless ref.is_a?(String)
|
25
|
+
raise ArgumentError, "Not given a FirefoxSocket, instead given #{firefox_socket.inspect} (#{firefox_socket.class.name})" unless firefox_socket.is_a?(FirefoxSocket)
|
26
|
+
@ref=ref
|
27
|
+
@firefox_socket=firefox_socket
|
28
|
+
@debug_name=other[:debug_name]
|
29
|
+
@function_result=other[:function_result]
|
30
|
+
# logger.info { "#{self.class} initialized: #{debug_name} (type #{type})" }
|
31
|
+
end
|
32
|
+
|
33
|
+
# returns the value, via FirefoxSocket#value_json
|
34
|
+
def val
|
35
|
+
firefox_socket.value_json(ref, :error_on_undefined => !function_result)
|
36
|
+
end
|
37
|
+
|
38
|
+
# whether JavascriptObject shall try to dynamically define methods on initialization, using
|
39
|
+
# #define_methods! default is false.
|
40
|
+
def self.always_define_methods
|
41
|
+
unless class_variable_defined?('@@always_define_methods')
|
42
|
+
# if not defined, set the default.
|
43
|
+
@@always_define_methods=false
|
44
|
+
end
|
45
|
+
@@always_define_methods
|
46
|
+
end
|
47
|
+
# set whether JavascriptObject shall try to dynamically define methods in #val_or_object, using
|
48
|
+
# #define_methods!
|
49
|
+
#
|
50
|
+
# I find this useful to set to true in irb, for tab-completion of methods. it may cause
|
51
|
+
# operations to be considerably slower, however.
|
52
|
+
#
|
53
|
+
# for always setting this in irb, I set this beforehand, overriding the default,
|
54
|
+
# by including in my .irbrc the following (which doesn't require this file to be
|
55
|
+
# required):
|
56
|
+
#
|
57
|
+
# class JavascriptObject
|
58
|
+
# @@always_define_methods=true
|
59
|
+
# end
|
60
|
+
def self.always_define_methods=(val)
|
61
|
+
@@always_define_methods = val
|
62
|
+
end
|
63
|
+
|
64
|
+
# returns the value just as a string with no attempt to deal with type using json. via FirefoxSocket#value
|
65
|
+
#
|
66
|
+
# note that this can be slow if it evaluates to a blank string. for example, if ref is just ""
|
67
|
+
# then FirefoxSocket#value will wait DEFAULT_SOCKET_TIMEOUT seconds for data that is not to come.
|
68
|
+
# this also happens with functions that return undefined. if ref="function(){do_some_stuff;}"
|
69
|
+
# (with no return), it will also wait DEFAULT_SOCKET_TIMEOUT.
|
70
|
+
def val_str
|
71
|
+
firefox_socket.value(ref)
|
72
|
+
end
|
73
|
+
|
74
|
+
# returns javascript typeof this object
|
75
|
+
def type
|
76
|
+
if function_result # don't get type for function results, causes function evaluations when you probably didn't want that.
|
77
|
+
nil
|
78
|
+
else
|
79
|
+
# logger.add(-1) { "retrieving type for #{debug_name}" }
|
80
|
+
@type||= firefox_socket.typeof(ref)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# calls the javascript instanceof operator on this object and the given interface (expected to
|
85
|
+
# be a JavascriptObject) note that the javascript instanceof operator is not to be confused with
|
86
|
+
# ruby's #instance_of? method - this takes a javascript interface; #instance_of? takes a ruby
|
87
|
+
# module.
|
88
|
+
#
|
89
|
+
# example:
|
90
|
+
# window.instanceof(window.firefox_socket.Components.interfaces.nsIDOMChromeWindow)
|
91
|
+
# => true
|
92
|
+
def instanceof(interface)
|
93
|
+
firefox_socket.instanceof(self.ref, interface.ref)
|
94
|
+
end
|
95
|
+
# returns an array of interfaces which this object is an instance of. this is achieved
|
96
|
+
# by looping over each value of Components.interfaces (see https://developer.mozilla.org/en/Components.interfaces )
|
97
|
+
# and calling the #instanceof operator with this and the interface.
|
98
|
+
#
|
99
|
+
# this may be rather slow.
|
100
|
+
def implemented_interfaces
|
101
|
+
firefox_socket.Components.interfaces.to_hash.inject([]) do |list, (key, interface)|
|
102
|
+
list << interface if (instanceof(interface) rescue false)
|
103
|
+
list
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# returns the type of object that is reported by the javascript toString() method, which
|
108
|
+
# returns such as "[object Object]" or "[object XPCNativeWrapper [object HTMLDocument]]"
|
109
|
+
# This method returns 'Object' or 'XPCNativeWrapper [object HTMLDocument]' respectively.
|
110
|
+
# Raises an error if this JavascriptObject points to something other than a javascript 'object'
|
111
|
+
# type ('function' or 'number' or whatever)
|
112
|
+
#
|
113
|
+
# this isn't used, doesn't seem useful, and may go away in the future.
|
114
|
+
def object_type
|
115
|
+
@object_type ||= begin
|
116
|
+
case type
|
117
|
+
when 'object'
|
118
|
+
self.toString! =~ /\A\[object\s+(.*)\]\Z/
|
119
|
+
$1
|
120
|
+
else
|
121
|
+
raise FirefoxSocketJavascriptError, "Type is #{type}, not object"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# checks the type of this object, and if it is a type that can be simply converted to a ruby
|
127
|
+
# object via json, returns the ruby value. that occurs if the type is one of:
|
128
|
+
#
|
129
|
+
# 'boolean','number','string','null'
|
130
|
+
#
|
131
|
+
# otherwise - if the type is something else (probably 'function' or 'object'; or maybe something else)
|
132
|
+
# then this JavascriptObject is returned.
|
133
|
+
#
|
134
|
+
# if the object this refers to is undefined in javascript, then behavor depends on the options
|
135
|
+
# hash. if :error_on_undefined is true, then nil is returned; otherwise FirefoxSocketUndefinedValueError
|
136
|
+
# is raised.
|
137
|
+
#
|
138
|
+
# if this is a function result, this will store the result in a temporary location (thereby
|
139
|
+
# calling the function to acquire the result) before making the above decision.
|
140
|
+
#
|
141
|
+
# this method also calls #define_methods! on this if JavascriptObject.always_define_methods is true.
|
142
|
+
# this can be overridden in the options hash using the :define_methods key (true or false).
|
143
|
+
def val_or_object(options={})
|
144
|
+
options={:error_on_undefined=>true, :define_methods => self.class.always_define_methods}.merge(options)
|
145
|
+
if function_result # calling functions multiple times is bad, so store in temp before figuring out what to do with it
|
146
|
+
store_rand_object_key(firefox_socket.temp_object).val_or_object(options.merge(:error_on_undefined => false))
|
147
|
+
else
|
148
|
+
# if we don't know our type, stick everything into one call to avoid multiple socket calls
|
149
|
+
types_to_convert = ['boolean', 'number', 'string', 'null']
|
150
|
+
if !@type
|
151
|
+
type_and_value = firefox_socket.value_json(%Q((function()
|
152
|
+
{ var result={};
|
153
|
+
var object;
|
154
|
+
try
|
155
|
+
{ result.type=(function(object){ return (object===null) ? 'null' : (typeof object); })(object=#{ref});
|
156
|
+
}
|
157
|
+
catch(e)
|
158
|
+
{ if(e.name=='ReferenceError')
|
159
|
+
{ result.type='undefined';
|
160
|
+
}
|
161
|
+
else
|
162
|
+
{ throw(e);
|
163
|
+
};
|
164
|
+
}
|
165
|
+
if($A(#{FirefoxSocket.to_javascript(types_to_convert)}).include(result.type))
|
166
|
+
{ result.value = object;
|
167
|
+
}
|
168
|
+
return result;
|
169
|
+
})()))
|
170
|
+
@type = type_and_value['type']
|
171
|
+
end
|
172
|
+
|
173
|
+
if type=='undefined'
|
174
|
+
if !options[:error_on_undefined]
|
175
|
+
nil
|
176
|
+
else
|
177
|
+
raise FirefoxSocketUndefinedValueError, "undefined expression represented by #{self.inspect} (javascript reference is #{@ref})"
|
178
|
+
end
|
179
|
+
elsif types_to_convert.include?(type)
|
180
|
+
if type_and_value
|
181
|
+
raise "internal error - type_and_value had no value key; was #{type_and_value.inspect}" unless type_and_value.key?('value') # this shouldn't happen
|
182
|
+
type_and_value['value']
|
183
|
+
else
|
184
|
+
val
|
185
|
+
end
|
186
|
+
else # 'function','object', or anything else
|
187
|
+
if options[:define_methods] && type=='object'
|
188
|
+
define_methods!
|
189
|
+
end
|
190
|
+
self
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
# does the work of #method_missing to determine whether to call a function what to return based
|
195
|
+
# on the defined behavior of the given suffix. see #method_missing for more. information.
|
196
|
+
def assign_or_call_or_val_or_object_by_suffix(suffix, *args)
|
197
|
+
if suffix=='='
|
198
|
+
assign(*args)
|
199
|
+
else
|
200
|
+
obj = if !args.empty? || type=='function'
|
201
|
+
pass(*args)
|
202
|
+
else
|
203
|
+
self
|
204
|
+
end
|
205
|
+
case suffix
|
206
|
+
when nil
|
207
|
+
obj.val_or_object
|
208
|
+
when '?'
|
209
|
+
obj.val_or_object(:error_on_undefined => false)
|
210
|
+
when '!'
|
211
|
+
obj.val
|
212
|
+
else
|
213
|
+
raise ArgumentError, "suffix should be one of: nil, '?', '!', '='; got: #{suffix.inspect}"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# returns a JavascriptObject representing the given attribute. Checks the type, and if it is a
|
219
|
+
# function, calls the function with any arguments given (which are converted to javascript)
|
220
|
+
# and returns the return value of the function (or nil if the function returns undefined).
|
221
|
+
#
|
222
|
+
# If the attribute is undefined, raises an error (if you want an attribute even if it's
|
223
|
+
# undefined, use #invoke? or #attr).
|
224
|
+
def invoke(attribute, *args)
|
225
|
+
attr(attribute).assign_or_call_or_val_or_object_by_suffix(nil, *args)
|
226
|
+
end
|
227
|
+
# same as #invoke, but returns nil for undefined attributes rather than raising an
|
228
|
+
# error.
|
229
|
+
def invoke?(attribute, *args)
|
230
|
+
attr(attribute).assign_or_call_or_val_or_object_by_suffix('?', *args)
|
231
|
+
end
|
232
|
+
|
233
|
+
# returns a JavascriptObject referencing the given attribute of this object
|
234
|
+
def attr(attribute)
|
235
|
+
unless (attribute.is_a?(String) || attribute.is_a?(Symbol)) && attribute.to_s =~ /\A[a-z_][a-z0-9_]*\z/i
|
236
|
+
raise FirefoxSocketSyntaxError, "#{attribute.inspect} (#{attribute.class.inspect}) is not a valid attribute!"
|
237
|
+
end
|
238
|
+
JavascriptObject.new("#{ref}.#{attribute}", firefox_socket, :debug_name => "#{debug_name}.#{attribute}")
|
239
|
+
end
|
240
|
+
|
241
|
+
# assigns the given ruby value (converted to javascript) to the reference
|
242
|
+
# for this object. returns self.
|
243
|
+
def assign(val)
|
244
|
+
@debug_name="(#{debug_name}=#{val.is_a?(JavascriptObject) ? val.debug_name : FirefoxSocket.to_javascript(val)})"
|
245
|
+
result=assign_expr(FirefoxSocket.to_javascript(val))
|
246
|
+
# logger.info { "#{self.class} assigned: #{debug_name} (type #{type})" }
|
247
|
+
result
|
248
|
+
end
|
249
|
+
# assigns the given javascript expression (string) to the reference for this object
|
250
|
+
def assign_expr(javascript_expression)
|
251
|
+
# don't want to use FirefoxSocket#assign_json because converting the result of the assignment
|
252
|
+
# (that is, the expression assigned) to json is error-prone and we don't really care about the
|
253
|
+
# result.
|
254
|
+
#
|
255
|
+
# don't want to use FirefoxSocket#assign because the result can be blank and cause send_and_read
|
256
|
+
# to wait for data that's not coming - also using a json function is better because it catches
|
257
|
+
# errors much more elegantly.
|
258
|
+
#
|
259
|
+
# so, wrap it in its own function, whose return value is unrelated to the actual assignment.
|
260
|
+
# for efficiency, it returns the type (it used to just return nil and uncache type for later
|
261
|
+
# retrieval), as this is desirable information, and since there's no other information we
|
262
|
+
# particularly desire from this call, that is a good thing to return.
|
263
|
+
@type=firefox_socket.value_json("(function(val){#{ref}=val; return (val===null) ? 'null' : (typeof val);}(#{javascript_expression}))")
|
264
|
+
self
|
265
|
+
end
|
266
|
+
|
267
|
+
# returns a JavascriptObject for the result of calling the function represented by this object, passing
|
268
|
+
# the given arguments, which are converted to javascript. if this is not a function, javascript will raise an error.
|
269
|
+
def pass(*args)
|
270
|
+
JavascriptObject.new("#{ref}(#{args.map{|arg| FirefoxSocket.to_javascript(arg)}.join(', ')})", firefox_socket, :function_result => true, :debug_name => "#{debug_name}(#{args.map{|arg| arg.is_a?(JavascriptObject) ? arg.debug_name : FirefoxSocket.to_javascript(arg)}.join(', ')})")
|
271
|
+
end
|
272
|
+
|
273
|
+
# returns the value (via FirefoxSocket#value_json) or a JavascriptObject (see #val_or_object) of the return
|
274
|
+
# value of this function (assumes this object is a function) passing it the given arguments (which
|
275
|
+
# are converted to javascript).
|
276
|
+
#
|
277
|
+
# simply, it just calls self.pass(*args).val_or_object
|
278
|
+
def call(*args)
|
279
|
+
pass(*args).val_or_object
|
280
|
+
end
|
281
|
+
|
282
|
+
# assuming the javascript object represented is a constructor, this returns a new
|
283
|
+
# instance passing the given arguments.
|
284
|
+
#
|
285
|
+
# date_class = firefox_socket.object('Date')
|
286
|
+
# => #<JavascriptObject:0x0118eee8 type=function, debug_name=Date>
|
287
|
+
# date = date_class.new
|
288
|
+
# => #<JavascriptObject:0x01188a84 type=object, debug_name=new Date()>
|
289
|
+
# date.getFullYear
|
290
|
+
# => 2010
|
291
|
+
# date_class.new('october 4, 1978').getFullYear
|
292
|
+
# => 1978
|
293
|
+
def new(*args)
|
294
|
+
JavascriptObject.new("new #{ref}", firefox_socket, :debug_name => "new #{debug_name}").call(*args)
|
295
|
+
end
|
296
|
+
|
297
|
+
# sets the given javascript variable to this object, and returns a JavascriptObject referring
|
298
|
+
# to the variable.
|
299
|
+
#
|
300
|
+
# >> foo=document.getElementById('guser').store('foo')
|
301
|
+
# => #<JavascriptObject:0x2dff870 @ref="foo" ...>
|
302
|
+
# >> foo.tagName
|
303
|
+
# => "DIV"
|
304
|
+
#
|
305
|
+
# the second argument is only used internally and shouldn't be used.
|
306
|
+
def store(js_variable, somewhere_meaningful=true)
|
307
|
+
stored=JavascriptObject.new(js_variable, firefox_socket, :function_result => false, :debug_name => somewhere_meaningful ? "(#{js_variable}=#{debug_name})" : debug_name)
|
308
|
+
stored.assign_expr(self.ref)
|
309
|
+
stored
|
310
|
+
end
|
311
|
+
|
312
|
+
private
|
313
|
+
# takes a block which, when yielded a random key, should result in a random reference. this checks
|
314
|
+
# that the reference is not already in use and stores this object in that reference, and returns
|
315
|
+
# a JavascriptObject referring to the stored object.
|
316
|
+
def store_rand_named(&name_proc)
|
317
|
+
base=36
|
318
|
+
length=32
|
319
|
+
begin
|
320
|
+
name=name_proc.call(("%#{length}s"%rand(base**length).to_s(base)).tr(' ','0'))
|
321
|
+
end #while JavascriptObject.new(name,firefox_socket).type!='undefined'
|
322
|
+
# okay, more than one iteration is ridiculously unlikely, sure, but just to be safe.
|
323
|
+
store(name, false)
|
324
|
+
end
|
325
|
+
public
|
326
|
+
|
327
|
+
# stores this object in a random key of the given object and returns the stored object.
|
328
|
+
def store_rand_object_key(object)
|
329
|
+
raise ArgumentError("Object is not a JavascriptObject: got #{object.inspect}") unless object.is_a?(JavascriptObject)
|
330
|
+
store_rand_named do |r|
|
331
|
+
object.sub(r).ref
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# stores this object in a random key of the designated temporary object for this socket and returns the stored object.
|
336
|
+
def store_rand_temp
|
337
|
+
store_rand_object_key(firefox_socket.temp_object)
|
338
|
+
end
|
339
|
+
|
340
|
+
# returns a JavascriptObject referring to a subscript of this object, specified as a ruby object converted to
|
341
|
+
# javascript.
|
342
|
+
#
|
343
|
+
# similar to [], but [] calls #val_or_object; this always returns a JavascriptObject.
|
344
|
+
def sub(key)
|
345
|
+
JavascriptObject.new("#{ref}[#{FirefoxSocket.to_javascript(key)}]", firefox_socket, :debug_name => "#{debug_name}[#{key.is_a?(JavascriptObject) ? key.debug_name : FirefoxSocket.to_javascript(key)}]")
|
346
|
+
end
|
347
|
+
|
348
|
+
# returns a JavascriptObject referring to a subscript of this object, or a value if it is simple (see #val_or_object)
|
349
|
+
#
|
350
|
+
# subscript is specified as ruby (converted to javascript).
|
351
|
+
def [](key)
|
352
|
+
sub(key).val_or_object(:error_on_undefined => false)
|
353
|
+
end
|
354
|
+
|
355
|
+
# assigns the given ruby value (which is converted to javascript) to the given subscript
|
356
|
+
# (the key is also converted to javascript).
|
357
|
+
def []=(key, value)
|
358
|
+
self.sub(key).assign(value)
|
359
|
+
end
|
360
|
+
|
361
|
+
# calls a binary operator (in javascript) with self and another operand.
|
362
|
+
#
|
363
|
+
# the operator should be string of javascript; the operand will be converted to javascript.
|
364
|
+
def binary_operator(operator, operand)
|
365
|
+
JavascriptObject.new("(#{ref}#{operator}#{FirefoxSocket.to_javascript(operand)})", firefox_socket, :debug_name => "(#{debug_name}#{operator}#{operand.is_a?(JavascriptObject) ? operand.debug_name : FirefoxSocket.to_javascript(operand)})").val_or_object
|
366
|
+
end
|
367
|
+
# addition, using the + operator in javascript
|
368
|
+
def +(operand)
|
369
|
+
binary_operator('+', operand)
|
370
|
+
end
|
371
|
+
# subtraction, using the - operator in javascript
|
372
|
+
def -(operand)
|
373
|
+
binary_operator('-', operand)
|
374
|
+
end
|
375
|
+
# division, using the / operator in javascript
|
376
|
+
def /(operand)
|
377
|
+
binary_operator('/', operand)
|
378
|
+
end
|
379
|
+
# multiplication, using the * operator in javascript
|
380
|
+
def *(operand)
|
381
|
+
binary_operator('*', operand)
|
382
|
+
end
|
383
|
+
# modulus, using the % operator in javascript
|
384
|
+
def %(operand)
|
385
|
+
binary_operator('%', operand)
|
386
|
+
end
|
387
|
+
# returns true if the javascript object represented by this is equal to the given operand.
|
388
|
+
def ==(operand)
|
389
|
+
operand.is_a?(JavascriptObject) && binary_operator('==', operand)
|
390
|
+
end
|
391
|
+
# javascript triple-equals (===) operator. very different from ruby's tripl-equals operator -
|
392
|
+
# in javascript this means "really really equal"; in ruby it means "sort of equal-ish"
|
393
|
+
def triple_equals(operand)
|
394
|
+
operand.is_a?(JavascriptObject) && binary_operator('===', operand)
|
395
|
+
end
|
396
|
+
# inequality, using the > operator in javascript
|
397
|
+
def >(operand)
|
398
|
+
binary_operator('>', operand)
|
399
|
+
end
|
400
|
+
# inequality, using the < operator in javascript
|
401
|
+
def <(operand)
|
402
|
+
binary_operator('<', operand)
|
403
|
+
end
|
404
|
+
# inequality, using the >= operator in javascript
|
405
|
+
def >=(operand)
|
406
|
+
binary_operator('>=', operand)
|
407
|
+
end
|
408
|
+
# inequality, using the <= operator in javascript
|
409
|
+
def <=(operand)
|
410
|
+
binary_operator('<=', operand)
|
411
|
+
end
|
412
|
+
|
413
|
+
# method_missing handles unknown method calls in a way that makes it possible to write
|
414
|
+
# javascript-like syntax in ruby, to some extent.
|
415
|
+
#
|
416
|
+
# method_missing checks the attribute of the represented javascript object with with the name of the given method. if that
|
417
|
+
# attribute refers to a function, then that function is called with any given arguments
|
418
|
+
# (like #invoke does). If that attribute is undefined, an error will be raised, unless a '?'
|
419
|
+
# suffix is used (see below).
|
420
|
+
#
|
421
|
+
# method_missing will only try to deal with methods that look like /^[a-z_][a-z0-9_]*$/i - no
|
422
|
+
# special characters, only alphanumeric/underscores, starting with alpha or underscore - with
|
423
|
+
# the exception of three special behaviors:
|
424
|
+
#
|
425
|
+
# If the method ends with an equals sign (=), it does assignment - it calls #assign on the given
|
426
|
+
# attribute, with the given (single) argument, to do the assignment and returns the assigned
|
427
|
+
# value.
|
428
|
+
#
|
429
|
+
# If the method ends with a bang (!), then it will attempt to get the value of the reference,
|
430
|
+
# using JavascriptObject#val, which converts the javascript to json and then to ruby. For simple types
|
431
|
+
# (null, string, boolean, number), this is what gets returned anyway. With other types (usually
|
432
|
+
# the 'object' type), attempting to convert to json can raise errors or cause infinite
|
433
|
+
# recursion, so is not attempted. but if you have an object or an array that you know you can
|
434
|
+
# json-ize, you can use ! to force that.
|
435
|
+
#
|
436
|
+
# If the method ends with a question mark (?), then if the attribute is undefined, no error is
|
437
|
+
# raised (as usually happens) - instead nil is just returned.
|
438
|
+
#
|
439
|
+
# otherwise, method_missing behaves like #invoke, and returns a JavascriptObject, a string, a boolean,
|
440
|
+
# a number, or null.
|
441
|
+
#
|
442
|
+
# Since method_missing returns a JavascriptObject for javascript objects, this means that you can
|
443
|
+
# string together method_missings and the result looks rather like javascript.
|
444
|
+
#--
|
445
|
+
# $A and $H, used below, are methods of the Prototype javascript library, which add nice functional
|
446
|
+
# methods to arrays and hashes - see http://www.prototypejs.org/
|
447
|
+
# You can use these methods with method_missing just like any other:
|
448
|
+
#
|
449
|
+
# >> js_hash=firefox_socket.object('$H')
|
450
|
+
# => #<JavascriptObject:0x2beb598 @ref="$H" ...>
|
451
|
+
# >> js_arr=firefox_socket.object('$A')
|
452
|
+
# => #<JavascriptObject:0x2be40e0 @ref="$A" ...>
|
453
|
+
#
|
454
|
+
# >> js_arr.call(document.body.childNodes).pluck! :tagName
|
455
|
+
# => ["TEXTAREA", "DIV", "NOSCRIPT", "DIV", "DIV", "DIV", "BR", "TABLE", "DIV", "DIV", "DIV", "TEXTAREA", "DIV", "DIV", "SCRIPT"]
|
456
|
+
# >> js_arr.call(document.body.childNodes).pluck! :id
|
457
|
+
# => ["csi", "header", "", "ssb", "tbd", "res", "", "nav", "wml", "", "", "hcache", "xjsd", "xjsi", ""]
|
458
|
+
# >> js_hash.call(document.getElementById('tbd')).keys!
|
459
|
+
# => ["addEventListener", "appendChild", "className", "parentNode", "getElementsByTagName", "title", ...]
|
460
|
+
def method_missing(method, *args)
|
461
|
+
method=method.to_s
|
462
|
+
if method =~ /\A([a-z_][a-z0-9_]*)([=?!])?\z/i
|
463
|
+
method = $1
|
464
|
+
suffix = $2
|
465
|
+
attr(method).assign_or_call_or_val_or_object_by_suffix(suffix, *args)
|
466
|
+
else
|
467
|
+
# don't deal with any special character crap
|
468
|
+
super
|
469
|
+
end
|
470
|
+
end
|
471
|
+
# calls define_method for each key of this object as a hash. useful for tab-completing attributes
|
472
|
+
# in irb, mostly.
|
473
|
+
def define_methods! # :nodoc:
|
474
|
+
metaclass=(class << self; self; end)
|
475
|
+
keys=firefox_socket.object("function(obj) { var keys=[]; for(var key in obj) { keys.push(key); } return keys; }").pass(self).val
|
476
|
+
|
477
|
+
keys.grep(/\A[a-z_][a-z0-9_]*\z/i).reject{|k| self.class.method_defined?(k)}.each do |key|
|
478
|
+
metaclass.send(:define_method, key) do |*args|
|
479
|
+
invoke(key, *args)
|
480
|
+
end
|
481
|
+
end
|
482
|
+
end
|
483
|
+
# returns true if this object responds to the given method (that is, it's a defined ruby method)
|
484
|
+
# or if #method_missing will handle it
|
485
|
+
def respond_to?(method, include_private = false)
|
486
|
+
super || object_respond_to?(method)
|
487
|
+
end
|
488
|
+
# returns true if the javascript object this represents responds to the given method. this does not pay attention
|
489
|
+
# to any defined ruby methods, just javascript.
|
490
|
+
def object_respond_to?(method)
|
491
|
+
method=method.to_s
|
492
|
+
if method =~ /^([a-z_][a-z0-9_]*)([=?!])?$/i
|
493
|
+
method = $1
|
494
|
+
suffix = $2
|
495
|
+
else # don't deal with any special character crap
|
496
|
+
return false
|
497
|
+
end
|
498
|
+
|
499
|
+
if self.type=='undefined'
|
500
|
+
return false
|
501
|
+
elsif suffix=='='
|
502
|
+
if self.type=='object'
|
503
|
+
return true # yeah, you can generally assign attributes to objects
|
504
|
+
else
|
505
|
+
return false # no, you can't generally assign attributes to (boolean, number, string, null)
|
506
|
+
end
|
507
|
+
else
|
508
|
+
attr=attr(method)
|
509
|
+
return attr.type!='undefined'
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
# undefine Object#id, and... anything else I think of that needs undef'ing in the future
|
514
|
+
[:id, :display].each do |method_name|
|
515
|
+
if method_defined?(method_name)
|
516
|
+
undef_method(method_name)
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
# returns this object passed through the $A function of the prototype javascript library.
|
521
|
+
def to_js_array
|
522
|
+
firefox_socket.object('$A').call(self)
|
523
|
+
end
|
524
|
+
# returns this object passed through the $H function of the prototype javascript library.
|
525
|
+
def to_js_hash
|
526
|
+
firefox_socket.object('$H').call(self)
|
527
|
+
end
|
528
|
+
# returns this object passed through a javascript function which copies each key onto a blank object and rescues any errors.
|
529
|
+
def to_js_hash_safe
|
530
|
+
firefox_socket.object('$_H').call(self)
|
531
|
+
end
|
532
|
+
# returns a JavascriptArray representing this object
|
533
|
+
def to_array
|
534
|
+
JavascriptArray.new(self.ref, self.firefox_socket, :debug_name => debug_name)
|
535
|
+
end
|
536
|
+
# returns a JavascriptHash representing this object
|
537
|
+
def to_hash
|
538
|
+
JavascriptHash.new(self.ref, self.firefox_socket, :debug_name => debug_name)
|
539
|
+
end
|
540
|
+
# returns a JavascriptDOMNode representing this object
|
541
|
+
def to_dom
|
542
|
+
JavascriptDOMNode.new(self.ref, self.firefox_socket, :debug_name => debug_name)
|
543
|
+
end
|
544
|
+
# returns a JavascriptFunction representing this object
|
545
|
+
def to_function
|
546
|
+
JavascriptFunction.new(self.ref, self.firefox_socket, :debug_name => debug_name)
|
547
|
+
end
|
548
|
+
|
549
|
+
# returns a ruby Hash. each key/value pair of this object
|
550
|
+
# is represented in the returned hash.
|
551
|
+
#
|
552
|
+
# if an error is encountered trying to access the value for an attribute, then in the
|
553
|
+
# returned hash, that attribute is set to the error that was encountered rather than
|
554
|
+
# the actual value (since the value wasn't successfully retrieved).
|
555
|
+
#
|
556
|
+
# options may be specified. the only option currently supported is:
|
557
|
+
# * :recurse => a number or nil. if it's a number, then this will recurse to that
|
558
|
+
# depth. If it's nil, this won't recurse at all.
|
559
|
+
#
|
560
|
+
# below the specified recursion level, this will return this JavascriptObject rather than recursing
|
561
|
+
# down into it.
|
562
|
+
#
|
563
|
+
# this function isn't expected to raise any errors, since encountered errors are set as
|
564
|
+
# attribute values.
|
565
|
+
def to_ruby_hash(options={})
|
566
|
+
options={:recurse => 1}.merge(options)
|
567
|
+
return self if !options[:recurse] || options[:recurse]==0
|
568
|
+
return self if self.type!='object'
|
569
|
+
next_options=options.merge(:recurse => options[:recurse]-1)
|
570
|
+
begin
|
571
|
+
keys=self.to_hash.keys
|
572
|
+
rescue FirefoxSocketError
|
573
|
+
return self
|
574
|
+
end
|
575
|
+
keys.inject({}) do |hash, key|
|
576
|
+
val=begin
|
577
|
+
self[key]
|
578
|
+
rescue FirefoxSocketError
|
579
|
+
$!
|
580
|
+
end
|
581
|
+
hash[key]=if val.is_a?(JavascriptObject)
|
582
|
+
val.to_ruby_hash(next_options)
|
583
|
+
else
|
584
|
+
val
|
585
|
+
end
|
586
|
+
hash
|
587
|
+
end
|
588
|
+
end
|
589
|
+
|
590
|
+
# returns an Array in which each element is the #val_or_Object of each element of this javascript array.
|
591
|
+
def to_ruby_array
|
592
|
+
self.to_array.to_a
|
593
|
+
end
|
594
|
+
|
595
|
+
# represents this javascript object in one line, displaying the type and debug name.
|
596
|
+
def inspect
|
597
|
+
"\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)} #{[:type, :debug_name].map{|attr| attr.to_s+'='+send(attr).to_s}.join(', ')}>"
|
598
|
+
end
|
599
|
+
def pretty_print(pp) # :nodoc:
|
600
|
+
pp.object_address_group(self) do
|
601
|
+
pp.seplist([:type, :debug_name], lambda { pp.text ',' }) do |attr|
|
602
|
+
pp.breakable ' '
|
603
|
+
pp.group(0) do
|
604
|
+
pp.text attr.to_s
|
605
|
+
pp.text ': '
|
606
|
+
#pp.breakable
|
607
|
+
pp.text send(attr)
|
608
|
+
end
|
609
|
+
end
|
610
|
+
end
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
# represents a node on the DOM. not substantially from JavascriptObject, but #inspect
|
615
|
+
# is more informative, and #dump is defined for extensive debug info.
|
616
|
+
#
|
617
|
+
# This class is mostly useful for debug, not used anywhere in production at the moment.
|
618
|
+
class JavascriptDOMNode < JavascriptObject
|
619
|
+
def inspect_stuff # :nodoc:
|
620
|
+
[:nodeName, :nodeType, :nodeValue, :tagName, :textContent, :id, :name, :value, :type, :className, :hidden].map do |attrn|
|
621
|
+
attr=attr(attrn)
|
622
|
+
if ['undefined','null'].include?(attr.type)
|
623
|
+
nil
|
624
|
+
else
|
625
|
+
[attrn, attr.val_or_object(:error_on_undefined => false)]
|
626
|
+
end
|
627
|
+
end.compact
|
628
|
+
end
|
629
|
+
# returns a string with a bunch of information about this dom node
|
630
|
+
def inspect
|
631
|
+
"\#<#{self.class.name} #{inspect_stuff.map{|(k,v)| "#{k}=#{v.inspect}"}.join(', ')}>"
|
632
|
+
end
|
633
|
+
def pretty_print(pp) # :nodoc:
|
634
|
+
pp.object_address_group(self) do
|
635
|
+
pp.seplist(inspect_stuff, lambda { pp.text ',' }) do |attr_val|
|
636
|
+
pp.breakable ' '
|
637
|
+
pp.group(0) do
|
638
|
+
pp.text attr_val.first.to_s
|
639
|
+
pp.text ': '
|
640
|
+
#pp.breakable
|
641
|
+
pp.text attr_val.last.inspect
|
642
|
+
end
|
643
|
+
end
|
644
|
+
end
|
645
|
+
end
|
646
|
+
# returns a string (most useful when written to STDOUT or to a file) consisting of this dom node
|
647
|
+
# and its child nodes, recursively. each node is one line and depth is indicated by spacing.
|
648
|
+
#
|
649
|
+
# call #dump(:recurse => n) to recurse down only n levels. default is to recurse all the way down the dom tree.
|
650
|
+
def dump(options={})
|
651
|
+
options={:recurse => nil, :level => 0}.merge(options)
|
652
|
+
next_options=options.merge(:recurse => options[:recurse] && (options[:recurse]-1), :level => options[:level]+1)
|
653
|
+
result=(" "*options[:level]*2)+self.inspect+"\n"
|
654
|
+
if options[:recurse]==0
|
655
|
+
result+=(" "*next_options[:level]*2)+"...\n"
|
656
|
+
else
|
657
|
+
self.childNodes.to_array.each do |child|
|
658
|
+
result+=child.to_dom.dump(next_options)
|
659
|
+
end
|
660
|
+
end
|
661
|
+
result
|
662
|
+
end
|
663
|
+
end
|
664
|
+
|
665
|
+
# this class represents a javascript array - that is, a javascript object that has a 'length'
|
666
|
+
# attribute which is a non-negative integer, and returns elements at each subscript from 0
|
667
|
+
# to less than than that length.
|
668
|
+
class JavascriptArray < JavascriptObject
|
669
|
+
# yields the element at each subscript of this javascript array, from 0 to self.length.
|
670
|
+
def each
|
671
|
+
length=self.length
|
672
|
+
raise FirefoxSocketJavascriptError, "length #{length.inspect} is not a non-negative integer on #{self.ref}" unless length.is_a?(Integer) && length >= 0
|
673
|
+
for i in 0...length
|
674
|
+
element=self[i]
|
675
|
+
if element.is_a?(JavascriptObject)
|
676
|
+
# yield a more permanent reference than the array subscript
|
677
|
+
element=element.store_rand_temp
|
678
|
+
end
|
679
|
+
yield element
|
680
|
+
end
|
681
|
+
end
|
682
|
+
include Enumerable
|
683
|
+
end
|
684
|
+
|
685
|
+
# this class represents a hash, or 'object' type in javascript.
|
686
|
+
class JavascriptHash < JavascriptObject
|
687
|
+
# returns an array of keys of this javascript object
|
688
|
+
def keys
|
689
|
+
@keys=firefox_socket.call_function(:obj => self){ "var keys=[]; for(var key in obj) { keys.push(key); } return keys;" }.val
|
690
|
+
end
|
691
|
+
# returns whether the given key is a defined key of this javascript object
|
692
|
+
def key?(key)
|
693
|
+
firefox_socket.call_function(:obj => self, :key => key){ "return key in obj;" }
|
694
|
+
end
|
695
|
+
# yields each key and value
|
696
|
+
def each(&block) # :yields: key, value
|
697
|
+
keys.each do |key|
|
698
|
+
if block.arity==1
|
699
|
+
yield [key, self[key]]
|
700
|
+
else
|
701
|
+
yield key, self[key]
|
702
|
+
end
|
703
|
+
end
|
704
|
+
end
|
705
|
+
# yields each key and value for this object
|
706
|
+
def each_pair
|
707
|
+
each do |key,value|
|
708
|
+
yield key,value
|
709
|
+
end
|
710
|
+
end
|
711
|
+
|
712
|
+
include Enumerable
|
713
|
+
end
|
714
|
+
|
715
|
+
# represents a javascript function
|
716
|
+
class JavascriptFunction < JavascriptObject
|
717
|
+
def to_proc
|
718
|
+
@the_proc ||= begin
|
719
|
+
javascript_function = self
|
720
|
+
the_proc = proc do |*args|
|
721
|
+
javascript_function.call(*args)
|
722
|
+
end
|
723
|
+
# this makes it so you can recover the javascript_function from the proc
|
724
|
+
# this method returns - js_func.to_proc.javascript_function returns js_func.
|
725
|
+
# unfortunately it's not so useful because when you pass the proc as a block,
|
726
|
+
# it's reinstantated without the metaclass or instance variable defined here.
|
727
|
+
# still, may have some potential use, so leaving this here.
|
728
|
+
the_proc_metaclass = (class << the_proc; self; end)
|
729
|
+
the_proc_metaclass.send(:define_method, :javascript_function) do
|
730
|
+
javascript_function
|
731
|
+
end
|
732
|
+
the_proc.instance_variable_set('@javascript_function', javascript_function)
|
733
|
+
the_proc
|
734
|
+
end
|
735
|
+
end
|
736
|
+
end
|