File Coverage

blib/lib/Serengeti/Context.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Serengeti::Context;
2              
3 5     5   25 use strict;
  5         61  
  5         196  
4 5     5   26 use warnings;
  5         9  
  5         125  
5              
6 5     5   27 use File::Basename qw();
  5         10  
  5         111  
7 5     5   24 use File::Spec;
  5         10  
  5         95  
8 5     5   18730 use JavaScript;
  0            
  0            
9             use List::Util qw(first);
10             use Module::Load qw();
11             use Scalar::Util qw(blessed);
12             use Regexp::Common qw(URI);
13              
14             use Serengeti;
15             use Serengeti::NotificationCenter;
16             use Serengeti::Notifications;
17             use Serengeti::Util qw(trim);
18              
19             use accessors::ro qw(js_ctx search_paths backend callbacks session windows);
20              
21             {
22             my $JSRuntime;
23             sub shared_js_runtime {
24             return $JSRuntime if $JSRuntime;
25            
26             $JSRuntime = JavaScript::Runtime->new();
27              
28             return $JSRuntime;
29             }
30             }
31              
32             sub new {
33             my ($pkg, $args) = @_;
34            
35             my $ctx = shared_js_runtime->create_context();
36            
37             my $backend = $args->{backend} || $Serengeti::DefaultBackend;
38             Module::Load::load $backend;
39            
40             my $self = bless {
41             js_ctx => $ctx,
42             search_paths => ["."],
43             backend => $backend->new,
44             callbacks => {},
45             session => undef,
46             }, $pkg;
47              
48             $self->_setup_jsapi();
49            
50             return $self;
51             }
52              
53             sub register_callback {
54             my ($self, $name, $callback) = @_;
55            
56             $self->callbacks->{$name} = $callback;
57             }
58              
59             sub _setup_jsapi {
60             my $self = shift;
61            
62             my $ctx = $self->js_ctx;
63            
64             my $common = {
65             include => sub {
66             my $path = shift;
67            
68             # We can actually include remote files which contains
69             # stuff that the website needs
70             if ($path =~ $RE{URI}{HTTP}) {
71             my $response = $self->backend->get($path);
72             if ($response->is_success) {
73             $ctx->eval($response->decoded_content);
74             if ($@) {
75             warn $@;
76             }
77             }
78            
79             return;
80             }
81              
82             $self->load($path);
83             },
84             gimme => sub {
85             my $name = pop;
86             # $self->session->log_action("requested data", @_);
87             die "Missing data request name" unless defined $name;
88             # Calls a registered perl function to retrieve stuff like
89             # passwords which might not want to be sent as args
90             my $callback = $self->callbacks->{$name};
91             die "Missing callback for '${name}'" unless $callback;
92             return $callback->(@_);
93             },
94             get => sub {
95             $self->backend->get(@_);
96             },
97             post => sub {
98             $self->backend->post(@_);
99             },
100             head => sub {
101             $self->backend->get(@_);
102             },
103             log => sub {
104             # This should tell the session object to log an entry
105             print STDERR join("", @_), "\n";
106             },
107             match => \&match,
108             };
109            
110             $self->backend->setup_document_jsapi($self->js_ctx);
111             $self->backend->setup_window_jsapi($self->js_ctx);
112            
113             $ctx->bind_object('$Browser' => $common);
114              
115             # Listen to when we get new documents
116             Serengeti::NotificationCenter->add_observer(
117             $self,
118             selector => "document_changed",
119             for => DOCUMENT_CHANGED_NOTIFICATION,
120             from => $self->backend,
121             );
122            
123             Serengeti::NotificationCenter->add_observer(
124             $self,
125             selector => "session_changed",
126             for => NEW_SESSION_NOTIFICATION,
127             );
128            
129             Serengeti::NotificationCenter->add_observer(
130             $self,
131             selector => "log_session_event",
132             for => SESSION_EVENT_NOTIFICATION,
133             from => $self->backend,
134             );
135            
136             1;
137             }
138              
139             sub DESTROY {
140             my $self = shift;
141             Serengeti::NotificationCenter->remove_observer($self);
142             }
143              
144             sub load {
145             my ($self, $file) = @_;
146            
147             my $path;
148             my @inc = @{$self->search_paths};
149              
150             for my $dir (@inc) {
151             my $lp = File::Spec->catfile($dir, $file);
152             $path = $lp, last if -e $lp;
153             }
154              
155             die "Can't find file: $file" unless $path;
156            
157             my $dirname = File::Basename::dirname($path);
158            
159             # Temporary add the file's basename to the list of directories to search.
160             my $inc = $self->search_paths;
161             my @new_inc = @$inc;
162             push @new_inc, $dirname unless first { $_ eq $dirname } @new_inc;
163             local $self->{search_paths} = \@new_inc;
164            
165             $self->js_ctx->eval_file($path);
166            
167             die "$@" if $@;
168             }
169              
170             sub has_action {
171             my ($self, $action) = @_;
172              
173             return $self->js_ctx->can($action);
174             }
175              
176             sub invoke_action {
177             my ($self, $action, $args, $options) = @_;
178            
179             $args = {} unless ref $args eq "HASH";
180             $options = {} unless ref $args eq "HASH";
181            
182             return $self->js_ctx->call($action, $args, $options);
183             }
184              
185             sub eval {
186             my ($self, $source, $filename, $lineno) = @_;
187            
188             return $self->js_ctx->eval($source, $filename, $lineno);
189             }
190              
191             sub session_changed {
192             my ($self, $sender, $notification, $data) = @_;
193              
194             $self->{session} = $data;
195             }
196              
197             sub document_changed {
198             my ($self) = @_;
199             $self->js_ctx->unbind_value("document");
200             $self->js_ctx->bind_object(document => $self->backend->current_document);
201             1;
202             }
203              
204             sub log_session_event {
205             my ($self, $sender, $notification, $data) = @_;
206             if ($self->session) {
207             my ($action, $event_args) = @{$data}{qw(event data)};
208             $event_args = [] unless ref $event_args eq "ARRAY";
209             $self->session->log_event($action, @$event_args);
210             }
211             }
212              
213             sub match {
214             my $self = shift;
215            
216             my $content;
217             if (ref $_[0] eq "Regexp") {
218             # Default to document.body.innerHTML;
219             $content = $self->backend->current_document->get_body->as_HTML;
220             }
221             else {
222             $content = shift;
223             if (blessed $content && $content->isa("HTML::Element")) {
224             $content = $content->as_HTML;
225             }
226             }
227              
228             my $re = shift;
229             $re = qr/$re/ unless ref $re eq "Regexp";
230            
231             my $options = shift;
232             $options = {} unless ref $options eq "HASH";
233            
234             # Perform matching
235             my $matches = 0;
236              
237             my $session = $self->session;
238             my $stash = $session ? $session->stash : undef;
239            
240             my @set;
241             if (defined $options->{set} && $stash) {
242             @set = map trim, split /\s*,\s*/, $options->{set};
243            
244             delete @{$stash}{@set} if $stash;
245             }
246            
247             my %set;
248             while (my @matches = ($content =~ $re)) {
249             $content =~ s/$re//;
250             $matches++;
251              
252             for my $key (@set) {
253             my $v = shift @matches;
254             if (ref $stash->{$key} eq "ARRAY") {
255             push @{$stash->{$key}}, $v;
256             }
257             elsif (exists $stash->{$key}) {
258             $stash->{$key} = [delete $stash->{$key}, $v];
259             }
260             else {
261             $stash->{$key} = $v;
262             }
263             }
264             }
265            
266             if (exists $options->{strict}) {
267             my $expect_matches = $options->{strict} || 0;
268             if ($matches != $expect_matches) {
269             die "Match matches ", $matches, " time(s) instead of required ",
270             ${expect_matches}, " time(s)";
271             }
272             }
273            
274             return $matches;
275             }
276              
277             1;
278             __END__