Firefox-Marionette-1.22/0000755000175000017500000000000014175144502013570 5ustar davedaveFirefox-Marionette-1.22/lib/0000755000175000017500000000000014175144502014336 5ustar davedaveFirefox-Marionette-1.22/lib/Firefox/0000755000175000017500000000000014175144502015740 5ustar davedaveFirefox-Marionette-1.22/lib/Firefox/Marionette/0000755000175000017500000000000014175144502020047 5ustar davedaveFirefox-Marionette-1.22/lib/Firefox/Marionette/Exception/0000755000175000017500000000000014175144502022005 5ustar davedaveFirefox-Marionette-1.22/lib/Firefox/Marionette/Exception/InsecureCertificate.pm0000644000175000017500000000553614175143706026301 0ustar davedavepackage Firefox::Marionette::Exception::InsecureCertificate; use strict; use warnings; use base qw(Firefox::Marionette::Exception::Response); our $VERSION = '1.22'; sub throw { my ( $class, $response, $parameters ) = @_; my $self = bless { string => 'Insecure certificate', response => $response, parameters => $parameters, }, $class; return $self->SUPER::_throw(); } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Exception::InsecureCertificate - Represents a 'insecure certificate' exception thrown by Firefox =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; =head1 DESCRIPTION This module handles the implementation of a 'insecure certificate' error thrown by Firefox =head1 SUBROUTINES/METHODS =head2 throw accepts a Marionette L and calls Carp::croak. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Exception::InsecureCertificate requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Exception/Response.pm0000755000175000017500000000713314175143706024155 0ustar davedavepackage Firefox::Marionette::Exception::Response; use strict; use warnings; use base qw(Firefox::Marionette::Exception); our $VERSION = '1.22'; sub throw { my ( $class, $response ) = @_; my $self = bless { string => $response->error()->{error} . q[: ] . $response->error()->{message}, response => $response }, $class; return $self->SUPER::_throw(); } sub status { my ($self) = @_; return $self->{response}->error()->{status}; } sub message { my ($self) = @_; return $self->{response}->error()->{message}; } sub error { my ($self) = @_; return $self->{response}->error()->{error}; } sub trace { my ($self) = @_; return $self->{response}->error()->{stacktrace}; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Exception::Response - Represents an exception thrown by Firefox =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; =head1 DESCRIPTION This module handles the implementation of an error in a Marionette protocol response. =head1 SUBROUTINES/METHODS =head2 error returns the firefox error message. Only available in recent firefox versions =head2 message returns a text description of the error. This is the most reliable method to give the user some indication of what is happening across all firefox versions. =head2 status returns the firefox status, a numeric identifier in older versions of firefox (such as 38.8) =head2 throw accepts a Marionette L as it's only parameter and calls Carp::croak. =head2 trace returns the firefox trace. Only available in recent firefox versions. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Exception::Response requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Exception/NoSuchAlert.pm0000755000175000017500000000547214175143706024552 0ustar davedavepackage Firefox::Marionette::Exception::NoSuchAlert; use strict; use warnings; use base qw(Firefox::Marionette::Exception::Response); our $VERSION = '1.22'; sub throw { my ( $class, $response, $parameters ) = @_; my $self = bless { string => 'Failed to find alert', response => $response, parameters => $parameters, }, $class; return $self->SUPER::_throw(); } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Exception::NoSuchAlert - Represents a 'no such alert' exception thrown by Firefox =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; =head1 DESCRIPTION This module handles the implementation of a 'no such element' error thrown by Firefox =head1 SUBROUTINES/METHODS =head2 throw accepts a Marionette L and calls Carp::croak. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Exception::NoSuchAlert requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Exception/NotFound.pm0000755000175000017500000000574014175143706024115 0ustar davedavepackage Firefox::Marionette::Exception::NotFound; use strict; use warnings; use base qw(Firefox::Marionette::Exception::Response); our $VERSION = '1.22'; sub throw { my ( $class, $response, $parameters ) = @_; my $self = bless { string => $parameters->{using} ? 'Failed to find ' . $parameters->{using} . ' of "' . $parameters->{value} . q["] : 'Failed to find element', response => $response, parameters => $parameters, }, $class; return $self->SUPER::_throw(); } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Exception::NotFound - Represents a 'no such element' exception thrown by Firefox =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; =head1 DESCRIPTION This module handles the implementation of a 'no such element' error thrown by Firefox =head1 SUBROUTINES/METHODS =head2 throw accepts a Marionette L and the original find parameters and calls Carp::croak. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Exception::NotFound requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Exception/StaleElement.pm0000755000175000017500000000612714175143706024743 0ustar davedavepackage Firefox::Marionette::Exception::StaleElement; use strict; use warnings; use base qw(Firefox::Marionette::Exception::Response); our $VERSION = '1.22'; sub throw { my ( $class, $response, $parameters ) = @_; my $string; if ( defined $parameters ) { $string = 'Failed to find ' . $parameters->{using} . ' of "' . $parameters->{value}; } else { $string = $response->{error}->{message}; } my $self = bless { string => $string, response => $response, parameters => $parameters, }, $class; return $self->SUPER::_throw(); } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Exception::StaleElement - Represents a 'stale element reference' exception thrown by Firefox =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; =head1 DESCRIPTION This module handles the implementation of a 'stale element reference' error thrown by Firefox =head1 SUBROUTINES/METHODS =head2 throw accepts a Marionette L and the original find parameters and calls Carp::croak. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Exception::StaleElement requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Proxy.pm0000755000175000017500000001452014175143706021540 0ustar davedavepackage Firefox::Marionette::Proxy; use strict; use warnings; our $VERSION = '1.22'; sub new { my ( $class, %parameters ) = @_; if ( $parameters{pac} ) { $parameters{pac} = "$parameters{pac}"; } elsif ( $parameters{host} ) { $parameters{type} = 'manual'; my $host = "$parameters{host}"; if ( $host !~ /:\d+$/smx ) { $host .= q[:80]; } $parameters{http} = $host; $parameters{https} = $host; } my $element = bless {%parameters}, $class; return $element; } sub type { my ($self) = @_; return $self->{type}; } sub pac { my ($self) = @_; return URI->new( $self->{pac} ); } sub ftp { my ($self) = @_; return $self->{ftp}; } sub http { my ($self) = @_; return $self->{http}; } sub none { my ($self) = @_; if ( defined $self->{none} ) { if ( ref $self->{none} ) { return @{ $self->{none} }; } else { return ( $self->{none} ); } } else { return (); } } sub https { my ($self) = @_; return $self->{https}; } sub socks { my ($self) = @_; return $self->{socks}; } sub socks_version { my ($self) = @_; return $self->{socks_version}; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Proxy - Represents a Proxy used by Firefox Capabilities using the Marionette protocol =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; my $proxy = Firefox::Marionette::Proxy->new( pac => 'http://gateway.example.com/' ); my $firefox = Firefox::Marionette->new( capabilities => Firefox::Marionette::Capabilities->new( proxy => $proxy ) ); foreach my $address ($firefox->capabilities->proxy()->none()) { say "Browser will ignore the proxy for $address"; } # OR my $proxy = Firefox::Marionette::Proxy->new( host => 'squid.example.com:3128' ); my $firefox = Firefox::Marionette->new( capabilities => Firefox::Marionette::Capabilities->new( proxy => $proxy ) ); =head1 DESCRIPTION This module handles the implementation of a Proxy in Firefox Capabilities using the Marionette protocol =head1 SUBROUTINES/METHODS =head2 new accepts a hash as a parameter. Allowed keys are below; =over 4 =item * type - indicates the type of proxy configuration. Must be one of 'pac', 'direct', 'autodetect', 'system', or 'manual'. =item * pac - defines the L for a proxy auto-config file if the L is equal to 'pac'. =item * host - defines the host for FTP, HTTP, and HTTPS traffic and sets the L to 'manual'. =item * http - defines the proxy host for HTTP traffic when the L is 'manual'. =item * https - defines the proxy host for encrypted TLS traffic when the L is 'manual'. =item * none - lists the addresses for which the proxy should be bypassed when the L is 'manual'. This may be a list of domains, IPv4 addresses, or IPv6 addresses. =item * socks - defines the proxy host for a SOCKS proxy traffic when the L is 'manual'. =item * socks_version - defines the SOCKS proxy version when the L is 'manual'. It must be any integer between 0 and 255 inclusive, but it defaults to '5'. =back This method returns a new L object. =head2 type returns the type of proxy configuration. Must be one of 'pac', 'direct', 'autodetect', 'system', or 'manual'. =head2 pac returns the L for a proxy auto-config file if the L is equal to 'pac'. =head2 http returns the proxy host for HTTP traffic when the L is 'manual'. =head2 https returns the proxy host for encrypted TLS traffic when the L is 'manual'. =head2 none returns a list of the addresses for which the proxy should be bypassed when the L is 'manual'. This may be a list of domains, IPv4 addresses, or IPv6 addresses. =head2 socks returns the proxy host for a SOCKS proxy traffic when the L is 'manual'. =head2 socks_version returns the SOCKS proxy version when the L is 'manual'. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Proxy requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Element.pm0000755000175000017500000007107514175143706022020 0ustar davedavepackage Firefox::Marionette::Element; use strict; use warnings; our $VERSION = '1.22'; sub IDENTIFIER { return 'element-6066-11e4-a52e-4f735466cecf' } sub new { my ( $class, $browser, %parameters ) = @_; if ( !defined $parameters{ IDENTIFIER() } ) { $parameters{ IDENTIFIER() } = delete $parameters{ELEMENT}; $parameters{_old_protocols_key} = 'ELEMENT'; } else { delete $parameters{ELEMENT}; } my $element = bless { browser => $browser, %parameters }, $class; return $element; } sub TO_JSON { my ($self) = @_; my $json = {}; if ( $self->{_old_protocols_key} ) { $json->{ $self->{_old_protocols_key} } = $self->uuid(); } else { $json->{ IDENTIFIER() } = $self->uuid(); } return $json; } sub uuid { my ($self) = @_; return $self->{ IDENTIFIER() }; } sub browser { my ($self) = @_; return $self->{browser}; } sub click { my ($self) = @_; return $self->browser()->click($self); } sub clear { my ($self) = @_; return $self->browser()->clear($self); } sub text { my ($self) = @_; return $self->browser()->text($self); } sub tag_name { my ($self) = @_; return $self->browser()->tag_name($self); } sub rect { my ($self) = @_; return $self->browser()->rect($self); } sub send_keys { my ( $self, $text ) = @_; Carp::carp( '**** DEPRECATED METHOD - send_keys HAS BEEN REPLACED BY type ****'); return $self->browser()->type( $self, $text ); } sub type { my ( $self, $text ) = @_; return $self->browser()->type( $self, $text ); } sub attribute { my ( $self, $name ) = @_; return $self->browser()->attribute( $self, $name ); } sub property { my ( $self, $name ) = @_; return $self->browser()->property( $self, $name ); } sub css { my ( $self, $property_name ) = @_; return $self->browser()->css( $self, $property_name ); } sub switch_to_frame { my ($self) = @_; return $self->browser()->switch_to_frame($self); } sub shadow_root { my ($self) = @_; return $self->browser()->shadow_root($self); } sub shadowy { my ($self) = @_; return $self->browser()->shadowy($self); } sub switch_to_shadow_root { my ($self) = @_; return $self->browser()->switch_to_shadow_root($self); } sub selfie { my ( $self, %extra ) = @_; return $self->browser()->selfie( $self, %extra ); } sub is_enabled { my ($self) = @_; return $self->browser()->is_enabled($self); } sub is_selected { my ($self) = @_; return $self->browser()->is_selected($self); } sub is_displayed { my ($self) = @_; return $self->browser()->is_displayed($self); } sub list { my ( $self, $value, $using ) = @_; Carp::carp( '**** DEPRECATED METHOD - using list HAS BEEN REPLACED BY find ****'); return $self->browser()->find( $value, $using, $self ); } sub list_by_id { my ( $self, $value ) = @_; Carp::carp( '**** DEPRECATED METHOD - using list_by_id HAS BEEN REPLACED BY find_id ****' ); return $self->browser()->find_id( $value, $self ); } sub list_by_name { my ( $self, $value ) = @_; Carp::carp( '**** DEPRECATED METHOD - using list_by_name HAS BEEN REPLACED BY find_name ****' ); return $self->browser()->find_name( $value, $self ); } sub list_by_tag { my ( $self, $value ) = @_; Carp::carp( '**** DEPRECATED METHOD - using list_by_tag HAS BEEN REPLACED BY find_tag ****' ); return $self->browser()->find_tag( $value, $self ); } sub list_by_class { my ( $self, $value ) = @_; Carp::carp( '**** DEPRECATED METHOD - using list_by_class HAS BEEN REPLACED BY find_class ****' ); return $self->browser()->find_class( $value, $self ); } sub list_by_selector { my ( $self, $value ) = @_; Carp::carp( '**** DEPRECATED METHOD - using list_by_selector HAS BEEN REPLACED BY find_selector ****' ); return $self->browser()->find_selector( $value, $self ); } sub list_by_link { my ( $self, $value ) = @_; Carp::carp( '**** DEPRECATED METHOD - using list_by_link HAS BEEN REPLACED BY find_link ****' ); return $self->browser()->find_link( $value, $self ); } sub list_by_partial { my ( $self, $value ) = @_; Carp::carp( '**** DEPRECATED METHOD - using list_by_partial HAS BEEN REPLACED BY find_partial ****' ); return $self->browser()->find_partial( $value, $self ); } sub find_by_id { my ( $self, $value ) = @_; Carp::carp( '**** DEPRECATED METHOD - using find_by_id HAS BEEN REPLACED BY find_id ****' ); return $self->browser()->find_id( $value, $self ); } sub find_by_name { my ( $self, $value ) = @_; Carp::carp( '**** DEPRECATED METHOD - using find_by_name HAS BEEN REPLACED BY find_name ****' ); return $self->browser()->find_name( $value, $self ); } sub find_by_tag { my ( $self, $value ) = @_; Carp::carp( '**** DEPRECATED METHOD - using find_by_tag HAS BEEN REPLACED BY find_tag ****' ); return $self->browser()->find_tag( $value, $self ); } sub find_by_class { my ( $self, $value ) = @_; Carp::carp( '**** DEPRECATED METHOD - using find_by_class HAS BEEN REPLACED BY find_class ****' ); return $self->browser()->find_class( $value, $self ); } sub find_by_selector { my ( $self, $value ) = @_; Carp::carp( '**** DEPRECATED METHOD - using find_by_selector HAS BEEN REPLACED BY find_selector ****' ); return $self->browser()->find_selector( $value, $self ); } sub find_by_link { my ( $self, $value ) = @_; Carp::carp( '**** DEPRECATED METHOD - using find_by_link HAS BEEN REPLACED BY find_link ****' ); return $self->browser()->find_link( $value, $self ); } sub find_by_partial { my ( $self, $value ) = @_; Carp::carp( '**** DEPRECATED METHOD - using find_by_partial HAS BEEN REPLACED BY find_partial ****' ); return $self->browser()->find_partial( $value, $self ); } sub find { my ( $self, $value, $using ) = @_; return $self->browser()->find( $value, $using, $self ); } sub find_id { my ( $self, $value ) = @_; return $self->browser()->find_id( $value, $self ); } sub find_name { my ( $self, $value ) = @_; return $self->browser()->find_name( $value, $self ); } sub find_tag { my ( $self, $value ) = @_; return $self->browser()->find_tag( $value, $self ); } sub find_class { my ( $self, $value ) = @_; return $self->browser()->find_class( $value, $self ); } sub find_selector { my ( $self, $value ) = @_; return $self->browser()->find_selector( $value, $self ); } sub find_link { my ( $self, $value ) = @_; return $self->browser()->find_link( $value, $self ); } sub find_partial { my ( $self, $value ) = @_; return $self->browser()->find_partial( $value, $self ); } sub has { my ( $self, $value, $using, $from ) = @_; return $self->browser()->has( $value, $using, $self ); } sub has_id { my ( $self, $value, $from ) = @_; return $self->browser()->has_id( $value, $self ); } sub has_name { my ( $self, $value, $from ) = @_; return $self->browser()->has_name( $value, $self ); } sub has_tag { my ( $self, $value, $from ) = @_; return $self->browser()->has_tag( $value, $self ); } sub has_class { my ( $self, $value, $from ) = @_; return $self->browser()->has_class( $value, $self ); } sub has_selector { my ( $self, $value, $from ) = @_; return $self->browser()->has_selector( $value, $self ); } sub has_link { my ( $self, $value, $from ) = @_; return $self->browser()->has_link( $value, $self ); } sub has_partial { my ( $self, $value, $from ) = @_; return $self->browser()->has_partial( $value, $self ); } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Element - Represents a Firefox element retrieved using the Marionette protocol =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $element = $firefox->find('//input[@id="metacpan_search-input"]'); $element->type('Test::More'); =head1 DESCRIPTION This module handles the implementation of a Firefox Element using the Marionette protocol =head1 SUBROUTINES/METHODS =head2 attribute accepts a scalar name a parameter. It returns the initial value of the attribute with the supplied name. Compare with the current value returned by L method. =head2 browser returns the L connected with the L. =head2 clear clears any user supplied input from the L =head2 click sends a 'click' to the L. The browser will wait for any page load to complete or the session's L duration to elapse before returning, which, by default is 5 minutes. The L method is also used to choose an option in a select dropdown. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new(visible => 1)->go('https://ebay.com'); my $select = $firefox->find_tag('select'); foreach my $option ($select->find_tag('option')) { if ($option->property('value') == 58058) { # Computers/Tablets & Networking $option->click(); } } =head2 css accepts a scalar CSS property name as a parameter. It returns the value of the computed style for that property. =head2 find accepts an L expression> as the first parameter and returns the first L that matches this expression. This method is subject to the L timeout. use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $div = $firefox->find_class('main-content'); $div->find('//input[@id="metacpan_search-input"]')->type('Test::More'); # OR in list context my $div = $firefox->find_class('main-content'); foreach my $element ($div->find('//input[@id="metacpan_search-input"]')) { $element->type('Test::More'); } If no elements are found, a L exception will be thrown. For the same functionality that returns undef if no elements are found, see the L method. =head2 find_id accepts an L as the first parameter and returns the first L with a matching 'id' property. This method is subject to the L timeout. use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $div = $firefox->find_class('main-content'); $div->find_id('metacpan_search-input')->type('Test::More'); # OR in list context my $div = $firefox->find_class('main-content'); foreach my $element ($div->find_id('metacpan_search-input')) { $element->type('Test::More'); } If no elements are found, a L exception will be thrown. For the same functionality that returns undef if no elements are found, see the L method. =head2 find_name This method returns the first L with a matching 'name' property. This method is subject to the L timeout. use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $div = $firefox->find_class('main-content'); $div->find_name('q')->type('Test::More'); # OR in list context my $div = $firefox->find_class('main-content'); foreach my $element ($div->find_name('q')) { $element->type('Test::More'); } If no elements are found, a L exception will be thrown. For the same functionality that returns undef if no elements are found, see the L method. =head2 find_class accepts a L as the first parameter and returns the first L with a matching 'class' property. This method is subject to the L timeout. use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $div = $firefox->find_class('main-content'); $div->find_class('form-control home-metacpan_search-input')->type('Test::More'); # OR in list context my $div = $firefox->find_class('main-content'); foreach my $element ($div->find_class('form-control home-metacpan_search-input')) { $element->type('Test::More'); } If no elements are found, a L exception will be thrown. For the same functionality that returns undef if no elements are found, see the L method. =head2 find_selector accepts a L as the first parameter and returns the first L that matches that selector. This method is subject to the L timeout. use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $div = $firefox->find_class('main-content'); $div->find_selector('input.home-metacpan_search-input')->type('Test::More'); # OR in list context my $div = $firefox->find_class('main-content'); foreach my $element ($div->find_selector('input.home-metacpan_search-input')) { $element->type('Test::More'); } If no elements are found, a L exception will be thrown. For the same functionality that returns undef if no elements are found, see the L method. =head2 find_tag accepts a L as the first parameter and returns the first L with this tag name. This method is subject to the L timeout. use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $div = $firefox->find_class('main-content'); my $input = $div->find_tag('input'); # OR in list context my $div = $firefox->find_class('main-content'); foreach my $element ($div->find_tag('input')) { # do something } If no elements are found, a L exception will be thrown. For the same functionality that returns undef if no elements are found, see the L method. =head2 find_link accepts a text string as the first parameter and returns the first link L that has a matching link text. This method is subject to the L timeout. use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $div = $firefox->find_class('container-fluid'); $div->find_link('API')->click(); # OR in list context my $div = $firefox->find_class('container-fluid'); foreach my $element ($div->find_link('API')) { $element->click(); } If no elements are found, a L exception will be thrown. For the same functionality that returns undef if no elements are found, see the L method. =head2 find_partial accepts a text string as the first parameter and returns the first link L that has a partially matching link text. This method is subject to the L timeout. use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $div = $firefox->find_class('container-fluid'); $div->find_partial('AP')->click(); # OR in list context my $div = $firefox->find_class('container-fluid'); foreach my $element ($div->find_partial('AP')) { $element->click(); } If no elements are found, a L exception will be thrown. For the same functionality that returns undef if no elements are found, see the L method. =head2 has accepts an L as the first parameter and returns the first L that matches this expression. This method is subject to the L timeout, which, by default is 0 seconds. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $div = $firefox->find_class('main-content'); if (my $element = $div->has('//input[@id="metacpan_search-input"]')) { $element->type('Test::More'); } If no elements are found, this method will return undef. For the same functionality that throws a L exception, see the L method. =head2 has_id accepts an L as the first parameter and returns the first L with a matching 'id' property. This method is subject to the L timeout, which, by default is 0 seconds. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $div = $firefox->find_class('main-content'); if (my $element = $div->has_id('metacpan_search-input')) { $element->type('Test::More'); } If no elements are found, this method will return undef. For the same functionality that throws a L exception, see the L method. =head2 has_name This method returns the first L with a matching 'name' property. This method is subject to the L timeout, which, by default is 0 seconds. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $div = $firefox->find_class('main-content'); if (my $element = $div->has_name('q')) { $element->type('Test::More'); } If no elements are found, this method will return undef. For the same functionality that throws a L exception, see the L method. =head2 has_class accepts a L as the first parameter and returns the first L with a matching 'class' property. This method is subject to the L timeout, which, by default is 0 seconds. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $div = $firefox->find_class('main-content'); if (my $element = $div->has_class('form-control home-metacpan_search-input')) { $element->type('Test::More'); } If no elements are found, this method will return undef. For the same functionality that throws a L exception, see the L method. =head2 has_selector accepts a L as the first parameter and returns the first L that matches that selector. This method is subject to the L timeout, which, by default is 0 seconds. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $div = $firefox->find_class('main-content'); if (my $element = $div->has_selector('input.home-metacpan_search-input')) { $element->type('Test::More'); } If no elements are found, this method will return undef. For the same functionality that throws a L exception, see the L method. =head2 has_tag accepts a L as the first parameter and returns the first L with this tag name. This method is subject to the L timeout, which, by default is 0 seconds. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $div = $firefox->find_class('main-content'); if (my $element = $div->has_tag('input'); # do something } If no elements are found, this method will return undef. For the same functionality that throws a L exception, see the L method. =head2 has_link accepts a text string as the first parameter and returns the first link L that has a matching link text. This method is subject to the L timeout, which, by default is 0 seconds. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $div = $firefox->find_class('container-fluid'); if (my $element = $div->has_link('API')->click(); $element->click(); } If no elements are found, this method will return undef. For the same functionality that throws a L exception, see the L method. =head2 has_partial accepts a text string as the first parameter and returns the first link L that has a partially matching link text. This method is subject to the L timeout, which, by default is 0 seconds. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $div = $firefox->find_class('container-fluid'); if (my $element = $div->has_partial('AP')->click(); $element->click(); } If no elements are found, this method will return undef. For the same functionality that throws a L exception, see the L method. =head2 IDENTIFIER returns the L =head2 is_enabled returns true or false if the element is enabled. =head2 is_selected returns true or false if the element is selected. =head2 is_displayed returns true or false if the element is displayed. =head2 new returns a new L. =head2 property accepts a scalar name a parameter. It returns the current value of the property with the supplied name. Compare with the initial value returned by L method. =head2 rect returns the current L of the L =head2 send_keys *** DEPRECATED - see L. *** =head2 selfie returns a L object containing a lossless PNG image screenshot of the L. accepts the following optional parameters as a hash; =over 4 =item * hash - return a SHA256 hex encoded digest of the PNG image rather than the image itself =item * full - take a screenshot of the whole document unless the first L parameter has been supplied. =item * scroll - scroll to the L supplied =item * highlights - a reference to a list containing L to draw a highlight around =back =head2 shadow_root returns the L's L as a L object or throws an exception. use Firefox::Marionette(); use Cwd(); my $firefox = Firefox::Marionette->new()->go('file://' . Cwd::cwd() . '/t/data/elements.html'); $firefox->find_class('add')->click(); my $shadow_root = $firefox->find_tag('custom-square')->shadow_root(); foreach my $element (@{$firefox->script('return arguments[0].children', args => [ $shadow_root ])}) { warn $element->tag_name(); } =head2 shadowy returns true if the L has a L or false otherwise. use Firefox::Marionette(); use Cwd(); my $firefox = Firefox::Marionette->new()->go('file://' . Cwd::cwd() . '/t/data/elements.html'); $firefox->find_class('add')->click(); if ($firefox->find_tag('custom-square')->shadowy()) { my $shadow_root = $firefox->find_tag('custom-square')->shadow_root(); warn $firefox->script('return arguments[0].innerHTML', args => [ $shadow_root ]); ... } This function will probably be used to see if the L method can be called on this element without raising an exception. =head2 switch_to_frame switches to this frame within the current window. =head2 tag_name returns the relevant tag name. For example 'a' or 'input'. =head2 text returns the text that is contained by that L (if any) =head2 type accepts a scalar string as a parameter. It sends the string to this L, such as filling out a text box. This method returns L to aid in chaining methods. =head2 uuid returns the browser generated UUID connected with this L. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Element requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Login.pm0000644000175000017500000002425714175143706021474 0ustar davedavepackage Firefox::Marionette::Login; use strict; use warnings; our $VERSION = '1.22'; sub _NUMBER_OF_MILLISECONDS_IN_A_SECOND { return 1000 } sub new { my ( $class, %parameters ) = @_; if ( !exists $parameters{realm} ) { $parameters{realm} = undef; } foreach my $key (qw(creation last_used password_changed)) { if ( defined $parameters{ $key . '_in_ms' } ) { delete $parameters{ $key . '_time' }; } elsif ( defined $parameters{ $key . '_time' } ) { my $value = delete $parameters{ $key . '_time' }; $parameters{ $key . '_in_ms' } = $value * _NUMBER_OF_MILLISECONDS_IN_A_SECOND(); } } my $self = bless {%parameters}, $class; return $self; } sub TO_JSON { my ($self) = @_; my $json = {}; foreach my $key ( sort { $a cmp $b } keys %{$self} ) { $json->{$key} = $self->{$key}; } return $json; } sub _convert_time_to_seconds { my ( $self, $milliseconds ) = @_; if ( defined $milliseconds ) { my $seconds = $milliseconds / _NUMBER_OF_MILLISECONDS_IN_A_SECOND(); return int $seconds; } else { return; } } sub host { my ($self) = @_; return $self->{host}; } sub user { my ($self) = @_; return $self->{user}; } sub user_field { my ($self) = @_; return $self->{user_field}; } sub password { my ($self) = @_; return $self->{password}; } sub password_field { my ($self) = @_; return $self->{password_field}; } sub realm { my ($self) = @_; return $self->{realm}; } sub origin { my ($self) = @_; return $self->{origin}; } sub guid { my ($self) = @_; return $self->{guid}; } sub times_used { my ($self) = @_; return $self->{times_used}; } sub creation_time { my ($self) = @_; return $self->_convert_time_to_seconds( $self->creation_in_ms() ); } sub creation_in_ms { my ($self) = @_; return $self->{creation_in_ms}; } sub last_used_time { my ($self) = @_; return $self->_convert_time_to_seconds( $self->last_used_in_ms() ); } sub last_used_in_ms { my ($self) = @_; return $self->{last_used_in_ms}; } sub password_changed_time { my ($self) = @_; return $self->_convert_time_to_seconds( $self->password_changed_in_ms() ); } sub password_changed_in_ms { my ($self) = @_; return $self->{password_changed_in_ms}; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Login - Represents a login from the Firefox Password Manager =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new(); foreach my $login ($firefox->logins()) { if ($login->user() eq 'me@example.org') { ... } } =head1 DESCRIPTION This module handles the implementation of a L from the Firefox L =head1 SUBROUTINES/METHODS =head2 creation_time returns the time, in Unix Epoch seconds, when the login was first created. =head2 creation_in_ms returns the time, in Unix Epoch milliseconds, when the login was first created. This is the same time as L but divided by 1000 and turned back into an integer. =head2 guid returns the GUID to uniquely identify the login. =head2 host returns the scheme + hostname (for example "https://example.com") of the page containing the login form. =head2 last_used_time returns the time, in Unix Epoch seconds, when the login was last submitted in a form or used to begin an HTTP auth session. This is the same time as L but divided by 1000 and turned back into an integer. =head2 last_used_in_ms returns the time, in Unix Epoch milliseconds, when the login was last submitted in a form or used to begin an HTTP auth session. =head2 origin returns the scheme + hostname (for example "https://example.org") of the L attribute of the form that is being submitted. =head2 new accepts an optional hash as a parameter. Allowed keys are below; =over 4 =item * creation_in_ms - the time, in Unix Epoch milliseconds, when the login was first created. =item * creation_time - the time, in Unix Epoch seconds, when the login was first created. This value will be overridden by the more precise creation_in_ms parameter, if provided. =item * guid - the GUID to uniquely identify the login. This can be any arbitrary string, but a format as created by L is recommended. For example, "{d4e1a1f6-5ea0-40ee-bff5-da57982f21cf}". =item * host - this is the scheme + hostname (for example "https://example.com") of the page containing the login form. =item * last_used_in_ms returns the time, in Unix Epoch milliseconds, when the login was last submitted in a form or used to begin an HTTP auth session. =item * last_used_time - the time, in Unix Epoch seconds, when the login was last submitted in a form or used to begin an HTTP auth session. This value will be overridden by the more precise last_used_in_ms parameter, if provided. =item * origin - this is the scheme + hostname (for example "https://example.org") of the L attribute of the form that is being submitted. If the L attribute has an empty or relative URL, then this value should be the same as the host. If this value is ignored, it will apply for forms with L of all values. =item * password - the password for the login. =item * password_changed_in_ms - the time, in Unix Epoch milliseconds, when the login's password was last modified. =item * password_changed_time - the time, in Unix Epoch seconds, when the login's password was last modified. This value will be overridden by the more precise password_changed_in_ms parameter, if provided. =item * password_field - the L attribute for the password input in a form. This is ignored for http auth logins. =item * realm - the HTTP Realm for which the login was requested. This is ignored for HTML Form logins. =item * times_used - the number of times the login was submitted in a form or used to begin an HTTP auth session. =item * user - the user name for the login. =item * user_field - the L attribute for the user input in a form. This is ignored for http auth logins. =back This method returns a new C object. =head2 password returns the password for the login. =head2 password_changed_time returns the time, in Unix Epoch seconds, when the login's password was last modified. This is the same time as L but divided by 1000 and turned back into an integer. =head2 password_changed_in_ms returns the time, in Unix Epoch milliseconds, when the login's password was last modified. =head2 password_field returns the L attribute for the password input in a form or undef for non-form logins. =head2 realm returns the HTTP Realm for which the login was requested. When an HTTP server sends a 401 result, the WWW-Authenticate header includes a realm to identify the "protection space." See RFC 2617. If the result did not include a realm, or it was blank, the hostname is used instead. For logins obtained from HTML forms, this field is null. =head2 times_used returns the number of times the login was submitted in a form or used to begin an HTTP auth session. =head2 user returns the user name for the login. =head2 user_field returns the L attribute for the user input in a form or undef for non-form logins. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Login requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Certificate.pm0000644000175000017500000002340714175143706022642 0ustar davedavepackage Firefox::Marionette::Certificate; use strict; use warnings; our $VERSION = '1.22'; sub _NUMBER_OF_MICROSECOND_DIGITS { return -6 } sub new { my ( $class, $parameters ) = @_; my $self = bless { %{$parameters} }, $class; return $self; } sub issuer_name { my ($self) = @_; return $self->{issuerName}; } sub common_name { my ($self) = @_; return $self->{commonName}; } sub is_any_cert { my ($self) = @_; return $self->{ANY_CERT} & $self->{certType}; } sub email_address { my ($self) = @_; return $self->{emailAddress} eq '(no email address)' ? undef : $self->{emailAddress}; } sub sha256_subject_public_key_info_digest { my ($self) = @_; return $self->{sha256SubjectPublicKeyInfoDigest}; } sub issuer_organization { my ($self) = @_; return $self->{issuerOrganization}; } sub db_key { my ($self) = @_; return $self->{dbKey}; } sub is_unknown_cert { my ($self) = @_; return $self->{UNKNOWN_CERT} & $self->{certType}; } sub is_built_in_root { my ($self) = @_; return $self->{isBuiltInRoot}; } sub token_name { my ($self) = @_; return $self->{tokenName}; } sub sha256_fingerprint { my ($self) = @_; return $self->{sha256Fingerprint}; } sub is_server_cert { my ($self) = @_; return $self->{SERVER_CERT} & $self->{certType}; } sub is_user_cert { my ($self) = @_; return $self->{USER_CERT} & $self->{certType}; } sub subject_name { my ($self) = @_; return $self->{subjectName}; } sub key_usages { my ($self) = @_; return $self->{keyUsages}; } sub is_ca_cert { my ($self) = @_; return $self->{CA_CERT} & $self->{certType}; } sub issuer_organization_unit { my ($self) = @_; return $self->{issuerOrganizationUnit}; } sub _convert_time_to_seconds { my ( $self, $microseconds ) = @_; my $seconds = substr $microseconds, 0, _NUMBER_OF_MICROSECOND_DIGITS(); return $seconds + 0; } sub not_valid_after { my ($self) = @_; return $self->_convert_time_to_seconds( $self->{validity}->{notAfter} ); } sub not_valid_before { my ($self) = @_; return $self->_convert_time_to_seconds( $self->{validity}->{notBefore} ); } sub serial_number { my ($self) = @_; return $self->{serialNumber}; } sub is_email_cert { my ($self) = @_; return $self->{EMAIL_CERT} & $self->{certType}; } sub issuer_common_name { my ($self) = @_; return $self->{issuerCommonName}; } sub organization { my ($self) = @_; return $self->{organization}; } sub nickname { my ($self) = @_; return $self->{nickname}; } sub sha1_fingerprint { my ($self) = @_; return $self->{sha1Fingerprint}; } sub display_name { my ($self) = @_; return $self->{displayName}; } sub organizational_unit { my ($self) = @_; return $self->{organizationalUnit}; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Certificate - Represents a x509 Certificate from Firefox =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new(); foreach my $certificate (sort { $a->display_name() cmp $b->display_name() } $firefox->certificates()) { ... } =head1 DESCRIPTION This module handles the implementation of a x509 Certificate from Firefox =head1 SUBROUTINES/METHODS =head2 common_name returns the common name from the certificate. This can contain the domain name (or wildcard) attached to the certificate or a Certificate Authority name, such as 'VeriSign Class 3 Public Primary Certification Authority - G4' =head2 db_key returns a unique value for the certificate, such as 'AAAAAAAAAAAAAAAQAAAAzS+A/iOMDiIPSGcSKJGHrLMwgcoxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDIwMDcgVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNpZ24gQ2xhc3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0' =head2 display_name returns the display name field, such as 'VeriSign Class 3 Public Primary Certification Authority - G4' =head2 email_address returns the emailAddress field if supplied, otherwise it will return undef. =head2 is_any_cert returns a boolean value to determine if the certificate is a certificate. I would regard it as quite surprising to get a certificate that returned false. =head2 is_built_in_root returns a boolean value to determine if the certificate is a built in root certificate. =head2 is_ca_cert returns a boolean value to determine if the certificate is a certificate authority certificate =head2 is_email_cert returns a boolean value to determine if the certificate is an email certificate. =head2 is_server_cert returns a boolean value to determine if the certificate is a server certificate. =head2 is_unknown_cert returns a boolean value to determine if the certificate type is unknown. =head2 is_user_cert returns a boolean value to determine if the certificate is a user certificate. =head2 issuer_common_name returns the L from the certificate, such as 'VeriSign Class 3 Public Primary Certification Authority - G4' =head2 issuer_name returns the L from the certificate, such as 'CN=VeriSign Class 3 Public Primary Certification Authority - G4,OU="(c) 2007 VeriSign, Inc. - For authorized use only",OU=VeriSign Trust Network,O="VeriSign, Inc.",C=US' =head2 issuer_organization returns the L from the certificate, such as 'VeriSign, Inc.' =head2 issuer_organization_unit returns the L from the certificate, such as 'VeriSign Trust Network' =head2 key_usages returns a string describing the intended usages of the certificate, such as 'Certificate Signer' =head2 new This method is intended for use exclusively by the L module. You should not need to call this method from your code. =head2 nickname returns the nickname field, such as 'Builtin Object Token:VeriSign Class 3 Public Primary Certification Authority - G4' =head2 not_valid_after returns the L time in seconds since the UNIX epoch. =head2 not_valid_before returns the L time in seconds since the UNIX epoch. =head2 organization returns the organization field, such as 'VeriSign, Inc.' =head2 organizational_unit returns the organization unit field, such as 'VeriSign Trust Network' =head2 serial_number returns the L of the certificate, such as '2F:80:FE:23:8C:0E:22:0F:48:67:12:28:91:87:AC:B3' =head2 sha1_fingerprint returns the sha1Fingerprint field, such as '22:D5:D8:DF:8F:02:31:D1:8D:F7:9D:B7:CF:8A:2D:64:C9:3F:6C:3A' =head2 sha256_fingerprint returns the sha256Fingerprint field, such as '69:DD:D7:EA:90:BB:57:C9:3E:13:5D:C8:5E:A6:FC:D5:48:0B:60:32:39:BD:C4:54:FC:75:8B:2A:26:CF:7F:79' =head2 sha256_subject_public_key_info_digest returns the base64 encoded sha256 digest of the L field, such as 'UZJDjsNp1+4M5x9cbbdflB779y5YRBcV6Z6rBMLIrO4=' =head2 subject_name returns the name from the L field, such as 'CN=VeriSign Class 3 Public Primary Certification Authority - G4,OU="(c) 2007 VeriSign, Inc. - For authorized use only",OU=VeriSign Trust Network,O="VeriSign, Inc.",C=US' =head2 token_name returns a string describing the type of certificate, such as 'Builtin Object Token' =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Certificate requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Response.pm0000755000175000017500000001630614175143706022221 0ustar davedavepackage Firefox::Marionette::Response; use strict; use warnings; use Firefox::Marionette::Exception::NotFound(); use Firefox::Marionette::Exception::NoSuchAlert(); use Firefox::Marionette::Exception::StaleElement(); use Firefox::Marionette::Exception::InsecureCertificate(); use Firefox::Marionette::Exception::Response(); our $VERSION = '1.22'; sub _TYPE_INDEX { return 0 } sub _MESSAGE_ID_INDEX { return 1 } sub _ERROR_INDEX { return 2 } sub _RESULT_INDEX { return 3 } sub _DEFAULT_RESPONSE_TYPE { return 1 } my %_known_exceptions = ( 'stale element reference' => 'Firefox::Marionette::Exception::StaleElement', 'no such alert' => 'Firefox::Marionette::Exception::NoSuchAlert', 'insecure certificate' => 'Firefox::Marionette::Exception::InsecureCertificate', ); sub new { my ( $class, $message, $parameters, $options ) = @_; my $response; if ( ref $message eq 'ARRAY' ) { $response = bless { type => $message->[ _TYPE_INDEX() ], message_id => $message->[ _MESSAGE_ID_INDEX() ], error => $message->[ _ERROR_INDEX() ], result => $message->[ _RESULT_INDEX() ], }, $class; } else { if ( $message->{error} ) { my $error; if ( ref $message->{error} ) { $error = $message->{error}; } else { $error = $message; } if ( !defined $error->{error} ) { $error->{error} = q[]; } if ( defined $error->{message} ) { if ( ( ref $error->{message} ) && ( ref $error->{message} eq 'HASH' ) && ( scalar keys %{ $error->{message} } == 0 ) ) { $error->{message} = q[]; } } else { $error->{message} = q[]; } $response = bless { type => _DEFAULT_RESPONSE_TYPE(), message_id => undef, error => $error, result => undef, }, $class; } else { $response = bless { type => _DEFAULT_RESPONSE_TYPE(), message_id => undef, error => undef, result => $message, }, $class; } } if ( $response->error() ) { if ( $response->_check_old_exception_cases( $parameters, $options ) ) { } elsif ( my $class = $_known_exceptions{ $response->error()->{error} } ) { $class->throw( $response, $parameters ); } else { Firefox::Marionette::Exception::Response->throw($response); } } return $response; } sub _check_old_exception_cases { my ( $self, $parameters, $options ) = @_; if ( ( $self->error()->{error} eq 'no such element' ) || ( $self->error()->{message} =~ /^Unable[ ]to[ ]locate[ ]element/smx ) ) { if ( $options->{return_undef_if_no_such_element} ) { $self->{ignored_exception} = 1; return 1; } else { Firefox::Marionette::Exception::NotFound->throw( $self, $parameters ); } } elsif ( ( $self->error()->{error} eq q[] ) && ( ( $self->error()->{message} =~ /^Stale[ ]element[ ]reference$/smx ) || ( $self->error()->{message} =~ /^The[ ]element[ ]reference[ ]is[ ]stale/smx ) ) ) { Firefox::Marionette::Exception::StaleElement->throw( $self, $parameters ); } return; } sub ignored_exception { my ($self) = @_; return $self->{ignored_exception}; } sub type { my ($self) = @_; return $self->{type}; } sub message_id { my ($self) = @_; return $self->{message_id}; } sub error { my ($self) = @_; return $self->{error}; } sub result { my ($self) = @_; return $self->{result}; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Response - Represents a Marionette protocol response =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; =head1 DESCRIPTION This module handles the implementation of a Marionette protocol response. This should not be used by users of L =head1 SUBROUTINES/METHODS =head2 new accepts a reference to an array as a parameter. The four components of a Marionette Response are below =over 4 =item * type - This should be type =item * message_id - the identifier to allow Marionette to track request / response pairs =item * error - the value of an error (if the response is an error, an L is thrown) =item * result - the object that is returned from the browser =back This method returns a new L object. =head2 type returns the type of the response. =head2 message_id returns the message_id of the response. =head2 error returns the error of the response or undef. =head2 result returns the result value. =head2 ignored_exception returns if the response should have generated an exception but was instructed not to. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Response requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Profile.pm0000755000175000017500000006236714175143706022033 0ustar davedavepackage Firefox::Marionette::Profile; use strict; use warnings; use English qw( -no_match_vars ); use File::Spec(); use FileHandle(); use Fcntl(); use Config::INI::Reader(); BEGIN { if ( $OSNAME eq 'MSWin32' ) { require Win32; } } our $VERSION = '1.22'; sub ANY_PORT { return 0 } sub _GETPWUID_DIR_INDEX { return 7 } sub profile_ini_directory { my $profile_ini_directory; if ( $OSNAME eq 'darwin' ) { my $home_directory = ( getpwuid $EFFECTIVE_USER_ID )[ _GETPWUID_DIR_INDEX() ]; defined $home_directory or Firefox::Marionette::Exception->throw( "Failed to execute getpwuid for $OSNAME:$EXTENDED_OS_ERROR"); $profile_ini_directory = File::Spec->catdir( $home_directory, 'Library', 'Application Support', 'Firefox' ); } elsif ( $OSNAME eq 'MSWin32' ) { $profile_ini_directory = File::Spec->catdir( Win32::GetFolderPath( Win32::CSIDL_APPDATA() ), 'Mozilla', 'Firefox' ); } elsif ( $OSNAME eq 'cygwin' ) { $profile_ini_directory = File::Spec->catdir( $ENV{APPDATA}, 'Mozilla', 'Firefox' ); } else { my $home_directory = ( getpwuid $EFFECTIVE_USER_ID )[ _GETPWUID_DIR_INDEX() ]; defined $home_directory or Firefox::Marionette::Exception->throw( "Failed to execute getpwuid for $OSNAME:$EXTENDED_OS_ERROR"); $profile_ini_directory = File::Spec->catdir( $home_directory, '.mozilla', 'firefox' ); } return $profile_ini_directory; } sub _read_ini_file { my ( $class, $profile_ini_directory ) = @_; if ( -d $profile_ini_directory ) { my $profile_ini_path = File::Spec->catfile( $profile_ini_directory, 'profiles.ini' ); if ( -f $profile_ini_path ) { my $config = Config::INI::Reader->read_file($profile_ini_path); return $config; } } return {}; } sub default_name { my ($class) = @_; my $profile_ini_directory = $class->profile_ini_directory(); my $config = $class->_read_ini_file($profile_ini_directory); foreach my $key ( sort { $config->{$a}->{Name} cmp $config->{$b}->{Name} } grep { exists $config->{$_}->{Name} } keys %{$config} ) { if ( ( $config->{$key}->{Default} ) && ( $config->{$key}->{Name} ) ) { return $config->{$key}->{Name}; } } return; } sub names { my ($class) = @_; my $profile_ini_directory = $class->profile_ini_directory(); my $config = $class->_read_ini_file($profile_ini_directory); my @names; foreach my $key ( sort { $config->{$a}->{Name} cmp $config->{$b}->{Name} } grep { exists $config->{$_}->{Name} } keys %{$config} ) { if ( defined $config->{$key}->{Name} ) { push @names, $config->{$key}->{Name}; } } return @names; } sub path { my ( $class, $name ) = @_; return File::Spec->catfile( $class->directory($name), 'prefs.js' ); } sub directory { my ( $class, $name ) = @_; my $profile_ini_directory = $class->profile_ini_directory(); my $config = $class->_read_ini_file($profile_ini_directory); my $path; my $first_key; foreach my $key ( sort { $a cmp $b } keys %{$config} ) { if ( ( !defined $first_key ) && ( defined $config->{$key}->{Name} ) ) { $first_key = $key; } my $selected; if ( ( defined $name ) && ( defined $config->{$key}->{Name} ) && ( $name eq $config->{$key}->{Name} ) ) { $selected = 1; } elsif ( ( !defined $name ) && ( $config->{$key}->{Default} ) ) { $selected = 1; } if ($selected) { if ( $config->{$key}->{IsRelative} ) { $path = File::Spec->catfile( $profile_ini_directory, $config->{$key}->{Path} ); } elsif ( $config->{$key}->{Path} ) { $path = File::Spec->catfile( $config->{$key}->{Path} ); } else { $path = File::Spec->catfile( $profile_ini_directory, $config->{$key}->{Default} ); } } } if ( ( !$path ) && ( defined $first_key ) ) { if ( $config->{$first_key}->{IsRelative} ) { $path = File::Spec->catfile( $profile_ini_directory, $config->{$first_key}->{Path} ); } else { $path = File::Spec->catfile( $config->{$first_key}->{Path} ); } } return $path; } sub existing { my ( $class, $name ) = @_; my $path = $class->path($name); if ( ($path) && ( -f $path ) ) { return $class->parse($path); } else { return; } } sub new { my ( $class, %parameters ) = @_; my $profile = bless { comments => q[], keys => {} }, $class; $profile->set_value( 'bookmarks.initialized.pref', 'true', 0 ); $profile->set_value( 'browser.bookmarks.restore_default_bookmarks', 'false', 0 ); $profile->set_value( 'browser.download.useDownloadDir', 'true', 0 ); $profile->set_value( 'browser.download.folderList', 2, 0 ) ; # the last folder specified for a download $profile->set_value( 'browser.places.importBookmarksHTML', 'true', 0 ); $profile->set_value( 'browser.reader.detectedFirstArticle', 'true', 0 ); $profile->set_value( 'browser.region.network.scan', 'false', 0 ); $profile->set_value( 'browser.region.network.url', q[], 0 ); $profile->set_value( 'browser.region.update.enabled', 'false', 0 ); $profile->set_value( 'browser.shell.checkDefaultBrowser', 'false', 0 ); $profile->set_value( 'browser.showQuitWarning', 'false', 0 ); $profile->set_value( 'browser.startup.homepage', 'about:blank', 1 ); $profile->set_value( 'browser.startup.homepage_override.mstone', 'ignore', 1 ); $profile->set_value( 'browser.startup.page', '0', 0 ); $profile->set_value( 'browser.tabs.warnOnClose', 'false', 0 ); $profile->set_value( 'browser.warnOnQuit', 'false', 0 ); $profile->set_value( 'datareporting.policy.firstRunURL', q[], 1 ); $profile->set_value( 'devtools.jsonview.enabled', 'false', 0 ); $profile->set_value( 'devtools.netmonitor.persistlog', 'true', 0 ); $profile->set_value( 'devtools.toolbox.host', 'window', 1 ); $profile->set_value( 'dom.disable_open_click_delay', 0, 0 ); $profile->set_value( 'extensions.installDistroAddons', 'false', 0 ); $profile->set_value( 'focusmanager.testmode', 'true', 0 ); $profile->set_value( 'marionette.port', ANY_PORT() ); $profile->set_value( 'network.http.prompt-temp-redirect', 'false', 0 ); $profile->set_value( 'network.http.request.max-start-delay', '0', 0 ); $profile->set_value( 'security.osclientcerts.autoload', 'true', 0 ); $profile->set_value( 'signon.autofillForms', 'false', 0 ); $profile->set_value( 'signon.rememberSignons', 'false', 0 ); $profile->set_value( 'startup.homepage_welcome_url', 'about:blank', 1 ); $profile->set_value( 'startup.homepage_welcome_url.additional', 'about:blank', 1 ); if ( !$parameters{seer} ) { $profile->set_value( 'browser.urlbar.speculativeConnect.enable', 'false', 0 ); $profile->set_value( 'network.dns.disablePrefetch', 'false', 0 ); $profile->set_value( 'network.http.speculative-parallel-limit', '0', 0 ); $profile->set_value( 'network.prefetch-next', 'false', 0 ); } if ( !$parameters{chatty} ) { $profile->set_value( 'app.normandy.enabled', 'false', 0 ); $profile->set_value( 'app.update.auto', 'false', 0 ); $profile->set_value( 'app.update.doorhanger', 'false', 0 ); $profile->set_value( 'app.update.enabled', 'false', 0 ); $profile->set_value( 'app.update.checkInstallTime', 'false', 0 ); $profile->set_value( 'app.update.disabledForTesting', 'true', 0 ); $profile->set_value( 'app.update.idletime', '1314000', 0 ); $profile->set_value( 'app.update.lastUpdateDate.background-update-timer', time, 0 ); $profile->set_value( 'app.update.staging.enabled', 'false', 0 ); $profile->set_value( 'app.update.timer', '131400000', 0 ); $profile->set_value( 'beacon.enabled', 'false', 0 ); $profile->set_value( 'browser.aboutHomeSnippets.updateUrl', q[], 1 ); $profile->set_value( 'browser.beacon.enabled', 'false', 0 ); $profile->set_value( 'browser.casting.enabled', 'false', 0 ); $profile->set_value( 'browser.chrome.favicons', 'false', 0 ); $profile->set_value( 'browser.chrome.site_icons', 'false', 0 ); $profile->set_value( 'browser.dom.window.dump.enabled', 'false', 0 ); $profile->set_value( 'browser.download.panel.shown', 'true', 0 ); $profile->set_value( 'browser.EULA.override', 'true', 0 ); $profile->set_value( 'browser.newtabpage.activity-stream.feeds.section.highlights', 'false', 0 ); $profile->set_value( 'browser.newtabpage.activity-stream.feeds.section.topstories.options', q[{}], 1 ); $profile->set_value( 'browser.newtabpage.activity-stream.feeds.snippets', 'false', 0 ); $profile->set_value( 'browser.newtabpage.activity-stream.feeds.topsites', 'false', 0 ); $profile->set_value( 'browser.newtabpage.introShown', 'true', 0 ); $profile->set_value( 'browser.offline', 'false', 0 ); $profile->set_value( 'browser.pagethumbnails.capturing_disabled', 'false', 0 ); $profile->set_value( 'browser.reader.detectedFirstArticle', 'true', 0 ); $profile->set_value( 'browser.safebrowsing.blockedURIs.enabled', 'false', 0 ); $profile->set_value( 'browser.safebrowsing.downloads.enabled', 'false', 0 ); $profile->set_value( 'browser.safebrowsing.downloads.remote.enabled', 'false', 0 ); $profile->set_value( 'browser.safebrowsing.enabled', 'false', 0 ); $profile->set_value( 'browser.safebrowsing.forbiddenURIs.enabled', 'false', 0 ); $profile->set_value( 'browser.safebrowsing.malware.enabled', 'false', 0 ); $profile->set_value( 'browser.safebrowsing.phishing.enabled', 'false', 0 ); $profile->set_value( 'browser.safebrowsing.provider.google.lists', q[], 1 ); $profile->set_value( 'browser.search.geoip.url', q[], 1 ); $profile->set_value( 'browser.search.update', 'false', 0 ); $profile->set_value( 'browser.selfsupport', 'false', 0 ); $profile->set_value( 'browser.send_pings', 'false', 0 ); $profile->set_value( 'browser.sessionstore.resume_from_crash', 'false', 0 ); $profile->set_value( 'browser.shell.shortcutFavicons', 'false', 0 ); $profile->set_value( 'browser.snippets.enabled', 'false', 0 ); $profile->set_value( 'browser.snippets.syncPromo.enabled', 'false', 0 ); $profile->set_value( 'browser.snippets.firstrunHomepage.enabled', 'false', 0 ); $profile->set_value( 'browser.tabs.animate', 'false', 0 ); $profile->set_value( 'browser.tabs.closeWindowWithLastTab', 'false', 0 ); $profile->set_value( 'browser.tabs.disableBackgroundZombification', 'false', 0 ); $profile->set_value( 'browser.tabs.warnOnCloseOtherTabs', 'false', 0 ); $profile->set_value( 'browser.tabs.warnOnOpen', 'false', 0 ); $profile->set_value( 'browser.usedOnWindows10.introURL', q[], 1 ); $profile->set_value( 'browser.uitour.enabled', 'false', 0 ); $profile->set_value( 'datareporting.healthreport.uploadEnabled', 'false', 0 ); $profile->set_value( 'dom.battery.enabled', 'false', 0 ); $profile->set_value( 'extensions.blocklist.enabled', 'false', 0 ); $profile->set_value( 'extensions.pocket.enabled', 'false', 0 ); $profile->set_value( 'extensions.pocket.site', q[], 1 ); $profile->set_value( 'extensions.getAddons.cache.enabled', 'false', 0 ); $profile->set_value( 'extensions.update.autoUpdateDefault', 'false', 0 ); $profile->set_value( 'extensions.update.enabled', 'false', 0 ); $profile->set_value( 'extensions.update.notifyUser', 'false', 0 ); $profile->set_value( 'general.useragent.updates.enabled', 'false', 0 ); $profile->set_value( 'geo.enabled', 'false', 0 ); $profile->set_value( 'geo.provider.testing', 'true', 0 ); $profile->set_value( 'geo.wifi.scan', 'false', 0 ); $profile->set_value( 'media.gmp-gmpopenh264.autoupdate', 'false', 0 ); $profile->set_value( 'media.gmp-gmpopenh264.enabled', 'false', 0 ); $profile->set_value( 'media.gmp-manager.cert.checkAttributes', 'false', 0 ); $profile->set_value( 'media.gmp-manager.cert.requireBuiltIn', 'false', 0 ); $profile->set_value( 'media.gmp-provider.enabled', 'false', 0 ); $profile->set_value( 'media.navigator.enabled', 'false', 0 ); $profile->set_value( 'network.captive-portal-service.enabled', 'false', 0 ); $profile->set_value( 'network.cookie.lifetimePolicy', '2', 0 ); $profile->set_value( 'privacy.trackingprotection.fingerprinting.enabled', 'false', 0 ); $profile->set_value( 'privacy.trackingprotection.pbmode.enabled', 'false', 0 ); $profile->set_value( 'profile.enable_profile_migration', 'false', 0 ); $profile->set_value( 'services.sync.prefs.sync.browser.search.update', 'false', 0 ); $profile->set_value( 'services.sync.prefs.sync.privacy.trackingprotection.cryptomining.enabled', 'false', 0 ); $profile->set_value( 'services.sync.prefs.sync.privacy.trackingprotection.enabled', 'false', 0 ); $profile->set_value( 'services.sync.prefs.sync.privacy.trackingprotection.fingerprinting.enabled', 'false', 0 ); $profile->set_value( 'services.sync.prefs.sync.privacy.trackingprotection.pbmode.enabled', 'false', 0 ); $profile->set_value( 'signon.rememberSignons', 'false', 0 ); $profile->set_value( 'toolkit.telemetry.archive.enabled', 'false', 0 ); $profile->set_value( 'toolkit.telemetry.enabled', 'false', 0 ); $profile->set_value( 'toolkit.telemetry.rejected', 'true', 0 ); $profile->set_value( 'toolkit.telemetry.server', q[], 1 ); $profile->set_value( 'toolkit.telemetry.unified', 'false', 0 ); $profile->set_value( 'toolkit.telemetry.unifiedIsOptIn', 'false', 0 ); $profile->set_value( 'toolkit.telemetry.prompted', '2', 0 ); $profile->set_value( 'toolkit.telemetry.rejected', 'true', 0 ); $profile->set_value( 'toolkit.telemetry.reportingpolicy.firstRun', 'false', 0 ); $profile->set_value( 'xpinstall.signatures.required', 'false', 0 ); } return $profile; } sub download_directory { my ( $self, $new ) = @_; my $old; $self->set_value( 'browser.download.downloadDir', $new, 1 ); $self->set_value( 'browser.download.dir', $new, 1 ); $self->set_value( 'browser.download.lastDir', $new, 1 ); $self->set_value( 'browser.download.defaultFolder', $new, 1 ); return $old; } sub save { my ( $self, $path ) = @_; my $temp_path = File::Temp::mktemp( $path . '.XXXXXXXXXXX' ); my $handle = FileHandle->new( $temp_path, Fcntl::O_WRONLY() | Fcntl::O_CREAT() | Fcntl::O_EXCL(), Fcntl::S_IRWXU() ) or Firefox::Marionette::Exception->throw( "Failed to open '$temp_path' for writing:$EXTENDED_OS_ERROR"); $handle->write( $self->as_string() ) or Firefox::Marionette::Exception->throw( "Failed to write to '$temp_path':$EXTENDED_OS_ERROR"); $handle->close() or Firefox::Marionette::Exception->throw( "Failed to close '$temp_path':$EXTENDED_OS_ERROR"); rename $temp_path, $path or Firefox::Marionette::Exception->throw( "Failed to rename '$temp_path' to '$path':$EXTENDED_OS_ERROR"); return; } sub as_string { my ($self) = @_; my $string = q[]; foreach my $key ( sort { $a cmp $b } keys %{ $self->{keys} } ) { my $value = $self->{keys}->{$key}->{value}; if ( ( defined $value ) && ( ( $value eq 'true' ) || ( $value eq 'false' ) || ( $value =~ /^\d{1,6}$/smx ) ) ) { $string .= "user_pref(\"$key\", $value);\n"; } elsif ( defined $value ) { $value =~ s/\\/\\\\/smxg; $value =~ s/"/\\"/smxg; $string .= "user_pref(\"$key\", \"$value\");\n"; } } return $string; } sub set_value { my ( $self, $name, $value ) = @_; $self->{keys}->{$name} = { value => $value }; return $self; } sub clear_value { my ( $self, $name ) = @_; return delete $self->{keys}->{$name}; } sub get_value { my ( $self, $name ) = @_; return $self->{keys}->{$name}->{value}; } sub parse { my ( $proto, $path ) = @_; my $handle = FileHandle->new( $path, Fcntl::O_RDONLY() ) or Firefox::Marionette::Exception->throw( "Failed to open '$path' for reading:$EXTENDED_OS_ERROR"); my $self = $proto->parse_by_handle($handle); close $handle or Firefox::Marionette::Exception->throw( "Failed to close '$path':$EXTENDED_OS_ERROR"); return $self; } sub parse_by_handle { my ( $proto, $handle ) = @_; my $self = ref $proto ? $proto : bless {}, $proto; $self->{comments} = q[]; $self->{keys} = {}; while ( my $line = <$handle> ) { chomp $line; if ( ( ( scalar keys %{ $self->{keys} } ) == 0 ) && ( ( $line !~ /\S/smx ) || ( $line =~ /^[#]/smx ) || ( $line =~ /^\/[*]/smx ) || ( $line =~ /^\/\//smx ) || ( $line =~ /^\s+[*]/smx ) ) ) { $self->{comments} .= $line; } elsif ( $line =~ /^user_pref[(]"([^"]+)",[ ](["]?)(.+)\2?[)];\s*$/smx ) { my ( $name, $quoted, $value ) = ( $1, $2, $3 ); $value =~ s/$quoted$//smx; $value =~ s/\\$quoted/$quoted/smxg; $self->{keys}->{$name} = { value => $value }; } else { Firefox::Marionette::Exception->throw("Failed to parse '$line'"); } } return $self; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Profile - Represents a prefs.js Firefox Profile =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; my $profile = Firefox::Marionette::Profile->new(); $profile->set_value('browser.startup.homepage', 'https://duckduckgo.com'); my $firefox = Firefox::Marionette->new(profile => $profile); $firefox->quit(); foreach my $profile_name (Firefox::Marionette::Profile->names()) { # start firefox using a specific existing profile $firefox = Firefox::Marionette->new(profile_name => $profile_name); $firefox->quit(); # OR start a new browser with a copy of a specific existing profile $profile = Firefox::Marionette::Profile->existing($profile_name); $firefox = Firefox::Marionette->new(profile => $profile); $firefox->quit(); } =head1 DESCRIPTION This module handles the implementation of a C Firefox Profile =head1 SUBROUTINES/METHODS =head2 ANY_PORT returns the port number for Firefox to listen on any port (0). =head2 new returns a new L. =head2 names returns a list of existing profile names that this module can discover on the filesystem. =head2 default_name returns the default profile name. =head2 directory accepts a profile name and returns the directory path that contains the C file. =head2 download_directory accepts a directory path that will contain downloaded files. Returns the previous value for download directory. =head2 existing accepts a profile name and returns a L object for that specified profile name. =head2 parse accepts a path as the parameter. This path should be to a C file. Parses the file and returns it as a L. =head2 parse_by_handle accepts a filehandle as the parameter to a C file. Parses the file and returns it as a L. =head2 path accepts a profile name and returns the corresponding path to the C file. =head2 profile_ini_directory returns the base directory for profiles. =head2 save accepts a path as the parameter. Saves the current profile to this location. =head2 as_string returns the contents of current profile as a string. =head2 get_value accepts a key name (such as C) and returns the value of the key from the profile. =head2 set_value accepts a key name (such as C) and a value (such as C) and sets this value in the profile. It returns itself to aid in chaining methods =head2 clear_value accepts a key name (such as C) and removes the key from the profile. It returns the old value of the key (if any). =head1 DIAGNOSTICS =over =item C<< Failed to execute getpwuid for %s:%s >> The module was unable to to execute L. This is probably a bug in this module's logic. Please report as described in the BUGS AND LIMITATIONS section below. =item C<< Failed to open '%s' for writing:%s >> The module was unable to open the named file. Maybe your disk is full or the file permissions need to be changed? =item C<< Failed to write to '%s':%s >> The module was unable to write to the named file. Maybe your disk is full? =item C<< Failed to close '%s':%s >> The module was unable to close a handle to the named file. Something is seriously wrong with your environment. =item C<< Failed to rename '%s' to '%s':%s >> The module was unable to rename the named file to the second file. Something is seriously wrong with your environment. =item C<< Failed to open '%s' for reading:%s >> The module was unable to open the named file. Maybe your disk is full or the file permissions need to be changed? =item C<< Failed to parse line '%s' >> The module was unable to parse the line for a Firefox prefs.js configuration. This is probably a bug in this module's logic. Please report as described in the BUGS AND LIMITATIONS section below. =back =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Profile requires no configuration files or environment variables. =head1 DEPENDENCIES Firefox::Marionette::Profile requires the following non-core Perl modules =over =item * L =back =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Image.pm0000644000175000017500000001202614175143706021435 0ustar davedavepackage Firefox::Marionette::Image; use strict; use warnings; use URI::URL(); use base qw(Firefox::Marionette::Element); our $VERSION = '1.22'; sub new { my ( $class, $element ) = @_; my $self = $element; bless $self, $class; return $self; } sub url { my ($self) = @_; my %attributes = $self->attrs(); return $attributes{src}; } sub height { my ($self) = @_; return $self->browser() ->script( 'return arguments[0].height;', args => [$self] ); } sub width { my ($self) = @_; return $self->browser() ->script( 'return arguments[0].width;', args => [$self] ); } sub alt { my ($self) = @_; return $self->browser() ->script( 'return arguments[0].alt;', args => [$self] ); } sub name { my ($self) = @_; my %attributes = $self->attrs(); return $attributes{name}; } sub tag { my ($self) = @_; my $tag_name = $self->tag_name(); if ( $tag_name eq 'img' ) { $tag_name = 'image'; } return $tag_name; } sub base { my ($self) = @_; return $self->browser()->uri(); } sub attrs { my ($self) = @_; return %{ $self->browser()->script( 'let namedNodeMap = arguments[0].attributes; let attributes = {}; for(let i = 0; i < namedNodeMap.length; i++) { var attr = namedNodeMap.item(i); if (attr.specified) { attributes[attr.name] = attr.value } }; return attributes;', args => [$self] ) }; } sub URI { my ($self) = @_; my %attributes = $self->attrs(); return URI::URL->new_abs( $attributes{href}, $self->base() ); } sub url_abs { my ($self) = @_; return $self->URI()->abs(); } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Image - Represents an image from the images method =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('http://metacpan.org');; foreach my $image ($firefox->images()) { say "Image at " . $link->URI() . " has a size of " . join q[x] $image->width(), $image->height(); } =head1 DESCRIPTION This module is a super class of L designed to be compatible with L. =head1 SUBROUTINES/METHODS =head2 alt returns the L for the image. =head2 attrs returns the attributes for the link as a hash. =head2 base returns the base url to which all links are relative. =head2 height returns the image height =head2 name returns the name attribute, if any. =head2 new accepts an L as a parameter and returns an L object =head2 tag returns the tag (one of: "a", "area", "frame", "iframe" or "meta"). =head2 url returns the URL of the link. =head2 URI returns the URL as a URI::URL object. =head2 url_abs returns the URL as an absolute URL string. =head2 width returns the image width =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Image requires no configuration files or environment variables. =head1 DEPENDENCIES Firefox::Marionette::Image requires the following non-core Perl modules =over =item * L =back =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Window/0000755000175000017500000000000014175144502021316 5ustar davedaveFirefox-Marionette-1.22/lib/Firefox/Marionette/Window/Rect.pm0000755000175000017500000000745214175143706022571 0ustar davedavepackage Firefox::Marionette::Window::Rect; use strict; use warnings; our $VERSION = '1.22'; sub new { my ( $class, %parameters ) = @_; my $window = bless {%parameters}, $class; return $window; } sub pos_x { my ($self) = @_; return $self->{pos_x}; } sub pos_y { my ($self) = @_; return $self->{pos_y}; } sub width { my ($self) = @_; return $self->{width}; } sub height { my ($self) = @_; return $self->{height}; } sub wstate { my ($self) = @_; return $self->{wstate}; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Window::Rect - Represents the browser window's shape and size =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $rect = $firefox->rect(); say "Current height of the window " . $window->height(); =head1 DESCRIPTION This module handles the representation of the browser window =head1 SUBROUTINES/METHODS =head2 new accepts a hash as a parameter. Allowed keys are below; =over 4 =item * pos_x - the X position of the window. This function will return undef for older Firefoxen. =item * pos_y - the Y position of the window. This function will return undef for older Firefoxen. =item * height - the height of the window =item * width - the width of the window =item * wstate - the state of the window as a scalar string. This function will return undef for Firefox versions less than 57 =back This method returns a new L object. =head2 pos_x returns the X position of the window =head2 pos_y returns the Y position of the window =head2 height returns the height of the window =head2 width returns the width of the window =head2 wstate returns a scalar representing the state of the window. For example 'maximized'. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Window::Rect requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Cookie.pm0000755000175000017500000001216614175143706021634 0ustar davedavepackage Firefox::Marionette::Cookie; use strict; use warnings; our $VERSION = '1.22'; sub new { my ( $class, %parameters ) = @_; my $cookie = bless { http_only => $parameters{http_only} ? 1 : 0, secure => $parameters{secure} ? 1 : 0, domain => $parameters{domain}, path => defined $parameters{path} ? $parameters{path} : q[/], value => $parameters{value}, name => $parameters{name}, }, $class; if ( defined $parameters{expiry} ) { $cookie->{expiry} = $parameters{expiry}; } if ( defined $parameters{same_site} ) { $cookie->{same_site} = $parameters{same_site}; } return $cookie; } sub http_only { my ($self) = @_; return $self->{http_only}; } sub secure { my ($self) = @_; return $self->{secure}; } sub domain { my ($self) = @_; return $self->{domain}; } sub path { my ($self) = @_; return $self->{path}; } sub value { my ($self) = @_; return $self->{value}; } sub expiry { my ($self) = @_; return $self->{expiry}; } sub same_site { my ($self) = @_; return $self->{same_site}; } sub name { my ($self) = @_; return $self->{name}; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Cookie - Represents a Firefox cookie retrieved using the Marionette protocol =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); foreach my $cookie ($firefox->cookies()) { say "Cookie name is " . $cookie->name(); } =head1 DESCRIPTION This module handles the implementation of a single Firefox cookie using the Marionette protocol =head1 SUBROUTINES/METHODS =head2 new accepts a hash as a parameter. Allowed keys are below; =over 4 =item * http_only - the httpOnly flag on the cookie. Allowed values are 1 or 0. Default is 0. =item * secure - the secure flag on the cookie. Allowed values are 1 or 0. Default is 0. =item * domain - the domain name belonging to the cookie. =item * path - the path belonging to the cookie. =item * expiry - the expiry time of the cookie in seconds since the UNIX epoch. expiry will return undef for Firefox versions less than 56 =item * value - the value of the cookie. =item * same_site - should the cookie be restricted to a first party or same-site context. See L. =item * name - the name of the cookie. =back This method returns a new L object. =head2 http_only returns the value of the httpOnly flag. =head2 secure returns the value of the secure flag. =head2 domain returns the value of cookies domain. For example '.metacpan.org' =head2 path returns the value of cookies path. For example '/search'. =head2 expiry returns the integer value of the cookies expiry time in seconds since the UNIX epoch. =head2 value returns the value of the cookie. =head2 same_site returns the L value for the cookie (if any). =head2 name returns the name of the cookie. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Cookie requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Buttons.pm0000644000175000017500000000624714175143706022061 0ustar davedavepackage Firefox::Marionette::Buttons; use strict; use warnings; use Exporter(); *import = \&Exporter::import; our @EXPORT_OK = qw( LEFT_BUTTON MIDDLE_BUTTON RIGHT_BUTTON ); our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK, ); our $VERSION = '1.22'; sub LEFT_BUTTON { return 0 } sub MIDDLE_BUTTON { return 1 } sub RIGHT_BUTTON { return 2 } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Buttons - Human readable mouse buttons for the Marionette protocol =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use Firefox::Marionette::Buttons qw(:all); use v5.10; my $firefox = Firefox::Marionette->new()->go('https://metacpan.org'); my $help_button = $firefox->find_class('btn search-btn help-btn'); $firefox->perform( $firefox->mouse_move($help_button), $firefox->mouse_down(RIGHT_BUTTON()), $firefox->mouse_up(RIGHT_BUTTON()), ); =head1 DESCRIPTION This module handles the implementation of the Firefox Marionette human readable mouse buttons =head1 SUBROUTINES/METHODS =head2 LEFT_BUTTON returns the left mouse button code, which is 0. =head2 MIDDLE_BUTTON returns the middle mouse button code, which is 1. =head2 RIGHT_BUTTON returns the right mouse button code, which is 2. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Buttons requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Capabilities.pm0000755000175000017500000003131314175143706023007 0ustar davedavepackage Firefox::Marionette::Capabilities; use strict; use warnings; our $VERSION = '1.22'; sub new { my ( $class, %parameters ) = @_; my $element = bless {%parameters}, $class; return $element; } sub enumerate { my ($self) = @_; my @enum = sort { $a cmp $b } grep { defined $self->{$_} } keys %{$self}; return @enum; } sub moz_use_non_spec_compliant_pointer_origin { my ($self) = @_; return $self->{moz_use_non_spec_compliant_pointer_origin}; } sub accept_insecure_certs { my ($self) = @_; return $self->{accept_insecure_certs}; } sub page_load_strategy { my ($self) = @_; return $self->{page_load_strategy}; } sub timeouts { my ($self) = @_; return $self->{timeouts}; } sub browser_version { my ($self) = @_; return $self->{browser_version}; } sub rotatable { my ($self) = @_; return $self->{rotatable}; } sub platform_version { my ($self) = @_; return $self->{platform_version}; } sub platform_name { my ($self) = @_; return $self->{platform_name}; } sub moz_profile { my ($self) = @_; return $self->{moz_profile}; } sub moz_webdriver_click { my ($self) = @_; return $self->{moz_webdriver_click}; } sub moz_process_id { my ($self) = @_; return $self->{moz_process_id}; } sub browser_name { my ($self) = @_; return $self->{browser_name}; } sub moz_headless { my ($self) = @_; return $self->{moz_headless}; } sub moz_accessibility_checks { my ($self) = @_; return $self->{moz_accessibility_checks}; } sub moz_build_id { my ($self) = @_; return $self->{moz_build_id}; } sub strict_file_interactability { my ($self) = @_; return $self->{strict_file_interactability}; } sub moz_shutdown_timeout { my ($self) = @_; return $self->{moz_shutdown_timeout}; } sub unhandled_prompt_behavior { my ($self) = @_; return $self->{unhandled_prompt_behavior}; } sub set_window_rect { my ($self) = @_; return $self->{set_window_rect}; } sub proxy { my ($self) = @_; return $self->{proxy}; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Capabilities - Represents Firefox Capabilities retrieved using the Marionette protocol =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new( capabilities => Firefox::Marionette::Capabilities->new( accept_insecure_certs => 0 ) ); if ($firefox->capabilities->accept_insecure_certs()) { say "Browser will now ignore certificate failures"; } =head1 DESCRIPTION This module handles the implementation of Firefox Capabilities using the Marionette protocol =head1 SUBROUTINES/METHODS =head2 accept_insecure_certs indicates whether untrusted and self-signed TLS certificates are implicitly trusted on navigation for the duration of the session. =head2 browser_name returns the browsers name. For example 'firefox' =head2 browser_version returns the version of L =head2 enumerate This method returns a list of strings describing the capabilities that this version of Firefox supports. =head2 moz_accessibility_checks returns the current accessibility (a11y) value =head2 moz_build_id returns the L =head2 moz_headless returns whether the browser is running in headless mode =head2 moz_process_id returns the process id belonging to the browser =head2 moz_profile returns the directory that contains the browsers profile =head2 moz_shutdown_timeout returns the value of L (aka the value of config toolkit.asyncshutdown.crash_timeout) =head2 moz_use_non_spec_compliant_pointer_origin returns a boolean value to indicate how the pointer origin for an action command will be calculated. With Firefox 59 the calculation will be based on the requirements by the WebDriver specification. This means that the pointer origin is no longer computed based on the top and left position of the referenced element, but on the in-view center point. To temporarily disable the WebDriver conformant behavior use 0 as value for this capability. Please note that this capability exists only temporarily, and that it will be removed once all Selenium bindings can handle the new behavior. =head2 moz_webdriver_click returns a boolean value to indicate which kind of interactability checks to run when performing a L or L to an elements. For Firefoxen prior to version 58.0 some legacy code as imported from an older version of FirefoxDriver was in use. With Firefox 58 the interactability checks as required by the WebDriver specification are enabled by default. This means geckodriver will additionally check if an element is obscured by another when clicking, and if an element is focusable for sending keys. Because of this change in behaviour, we are aware that some extra errors could be returned. In most cases the test in question might have to be updated so it's conform with the new checks. But if the problem is located in geckodriver, then please raise an issue in the issue tracker. To temporarily disable the WebDriver conformant checks use 0 as value for this capability. Please note that this capability exists only temporarily, and that it will be removed once the interactability checks have been stabilized. =head2 new accepts a hash as a parameter. Allowed keys are below; =over 4 =item * accept_insecure_certs - Indicates whether untrusted and self-signed TLS certificates are implicitly trusted on navigation for the duration of the session. Allowed values are 1 or 0. Default is 0. =item * moz_accessibility_checks - run a11y checks when clicking elements. Allowed values are 1 or 0. Default is 0. =item * moz_headless - the browser should be started with the -headless option. moz_headless is only supported in Firefox 56+ =item * moz_use_non_spec_compliant_pointer_origin - a boolean value to indicate how the pointer origin for an action command will be calculated. With Firefox 59 the calculation will be based on the requirements by the WebDriver specification. This means that the pointer origin is no longer computed based on the top and left position of the referenced element, but on the in-view center point. To temporarily disable the WebDriver conformant behavior use 0 as value for this capability. Please note that this capability exists only temporarily, and that it will be removed once all Selenium bindings can handle the new behavior. =item * moz_webdriver_click - a boolean value to indicate which kind of interactability checks to run when performing a L or L to an elements. For Firefoxen prior to version 58.0 some legacy code as imported from an older version of FirefoxDriver was in use. With Firefox 58 the interactability checks as required by the WebDriver specification are enabled by default. This means geckodriver will additionally check if an element is obscured by another when clicking, and if an element is focusable for sending keys. Because of this change in behaviour, we are aware that some extra errors could be returned. In most cases the test in question might have to be updated so it's conform with the new checks. But if the problem is located in geckodriver, then please raise an issue in the issue tracker. To temporarily disable the WebDriver conformant checks use 0 as value for this capability. Please note that this capability exists only temporarily, and that it will be removed once the interactability checks have been stabilized. =item * page_load_strategy - defines the L for the upcoming browser session. =item * proxy - describes the L setup for the upcoming browser session. =item * strict_file_interactability - a boolean value to indicate if interactability checks will be applied to . Allowed values are 1 or 0. Default is 0. =item * timeouts - describes the L imposed on certain session operations. =item * unhandled_prompt_behavior - defines what firefox should do on encountering a L. There are a range of L, including "dismiss", "accept", "dismiss and notify", "accept and notify" and "ignore". =back This method returns a new L object. =head2 page_load_strategy returns the L to use for the duration of the session. The page load strategy corresponds to the L and may be one of the following values; =over 4 =item * normal - Wait for the document and all sub-resources have finished loading. The corresponding L is "complete". The L event is about to fire. This strategy is the default value. =item * eager - Wait for the document to have finished loading and have been parsed. Sub-resources such as images, stylesheets and frames are still loading. The corresponding L is "interactive". =item * none - return immediately after starting navigation. The corresponding L is "loading". =back =head2 platform_name returns the operating system name. For example 'linux', 'darwin' or 'windows_nt'. =head2 proxy returns the current L object =head2 platform_version returns the operation system version. For example '4.14.11-300.fc27.x86_64', '17.3.0' or '10.0' =head2 rotatable does this version of L have a rotatable screen such as Android Fennec. =head2 set_window_rect returns true if Firefox fully supports L, otherwise it returns false. =head2 strict_file_interactability returns the current value of L =head2 timeouts returns the current L object =head2 unhandled_prompt_behavior returns the current value of L. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Capabilities requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Timeouts.pm0000755000175000017500000001013714175143706022230 0ustar davedavepackage Firefox::Marionette::Timeouts; use strict; use warnings; our $VERSION = '1.22'; sub new { my ( $class, %parameters ) = @_; my $element = bless {%parameters}, $class; return $element; } sub page_load { my ($self) = @_; return $self->{page_load}; } sub script { my ($self) = @_; return $self->{script}; } sub implicit { my ($self) = @_; return $self->{implicit}; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Timeouts - Represents the timeouts for page loading, searching, and scripts. =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $timeouts = $firefox->timeouts(); say "Page Load Timeouts is " . $timeouts->page_load() . " ms"; =head1 DESCRIPTION This module handles the implementation of the Firefox Marionette Timeouts =head1 SUBROUTINES/METHODS =head2 new accepts a hash as a parameter. Allowed keys are below; =over 4 =item * page_load - the timeout used for L, L, L, L and L methods in milliseconds =item * script - the timeout used for L and L methods in milliseconds =item * implicit - the timeout used for L and L methods in milliseconds =back This method returns a new L object. =head2 page_load returns the the timeout used for L, L, L, L and L methods in milliseconds. =head2 script returns the the timeout used for L and L methods in milliseconds. =head2 implicit returns the timeout used for L and L methods in milliseconds =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Timeouts requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Exception.pm0000755000175000017500000000614514175143706022361 0ustar davedavepackage Firefox::Marionette::Exception; use strict; use warnings; use Carp(); use overload '""' => 'string'; our $VERSION = '1.22'; sub throw { my ( $class, $string ) = @_; my $self = bless { string => $string }, $class; return $self->_throw(); } sub _throw { my ($self) = @_; my $index = 0; my ( $package, $file, $line ) = caller $index++; while ( $package =~ /^Firefox::Marionette/smx ) { ( $package, $file, $line ) = caller $index++; } $self->{origin} = $package eq 'main' ? $file : $package; $self->{line} = $line; Carp::croak($self); } sub string { my ($self) = @_; return $self->{string} . qq[\n]; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Exception - Represents an base exception class for exceptions for Firefox::Marionette =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; =head1 DESCRIPTION This module handles the implementation of an exception in Firefox::Marionette. =head1 SUBROUTINES/METHODS =head2 throw accepts a string as it's only parameter and calls Carp::croak. =head2 string returns a stringified version of the exception. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Exception::Response requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Keys.pm0000644000175000017500000002043614175143706021332 0ustar davedavepackage Firefox::Marionette::Keys; use strict; use warnings; use Exporter(); *import = \&Exporter::import; our @EXPORT_OK = qw( CANCEL HELP BACKSPACE TAB CLEAR ENTER SHIFT SHIFT_LEFT CONTROL CONTROL_LEFT ALT ALT_LEFT PAUSE ESCAPE SPACE PAGE_UP PAGE_DOWN END_KEY HOME ARROW_LEFT ARROW_UP ARROW_RIGHT ARROW_DOWN INSERT DELETE F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 META META_LEFT ZENKAKU_HANKAKU SHIFT_RIGHT CONTROL_RIGHT ALT_RIGHT META_RIGHT ); our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK, ); our $VERSION = '1.22'; sub CANCEL { return chr hex '0xE001' } sub HELP { return chr hex '0xE002' } sub BACKSPACE { return chr hex '0xE003' } sub TAB { return chr hex '0xE004' } sub CLEAR { return chr hex '0xE005' } sub ENTER { return chr hex '0xE006' } sub SHIFT { return chr hex '0xE008' } sub SHIFT_LEFT { return chr hex '0xE008' } sub CONTROL { return chr hex '0xE009' } sub CONTROL_LEFT { return chr hex '0xE009' } sub ALT { return chr hex '0xE00A' } sub ALT_LEFT { return chr hex '0xE00A' } sub PAUSE { return chr hex '0xE00B' } sub ESCAPE { return chr hex '0xE00C' } sub SPACE { return chr hex '0xE00D' } sub PAGE_UP { return chr hex '0xE00E' } sub PAGE_DOWN { return chr hex '0xE00F' } sub END_KEY { return chr hex '0xE010' } sub HOME { return chr hex '0xE011' } sub ARROW_LEFT { return chr hex '0xE012' } sub ARROW_UP { return chr hex '0xE013' } sub ARROW_RIGHT { return chr hex '0xE014' } sub ARROW_DOWN { return chr hex '0xE015' } sub INSERT { return chr hex '0xE016' } sub DELETE { return chr hex '0xE017' } sub F1 { return chr hex '0xE031' } sub F2 { return chr hex '0xE032' } sub F3 { return chr hex '0xE033' } sub F4 { return chr hex '0xE034' } sub F5 { return chr hex '0xE035' } sub F6 { return chr hex '0xE036' } sub F7 { return chr hex '0xE037' } sub F8 { return chr hex '0xE038' } sub F9 { return chr hex '0xE039' } sub F10 { return chr hex '0xE03A' } sub F11 { return chr hex '0xE03B' } sub F12 { return chr hex '0xE03C' } sub META { return chr hex '0xE03D' } sub META_LEFT { return chr hex '0xE03D' } sub ZENKAKU_HANKAKU { return chr hex '0xE040' } sub SHIFT_RIGHT { return chr hex '0xE050' } sub CONTROL_RIGHT { return chr hex '0xE051' } sub ALT_RIGHT { return chr hex '0xE052' } sub META_RIGHT { return chr hex '0xE053' } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Keys - Human readable special keys for the Marionette protocol =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use Firefox::Marionette::Keys qw(:all); use v5.10; my $firefox = Firefox::Marionette->new(); $firefox->chrome()->perform( $firefox->key_down(CONTROL()), $firefox->key_down('l'), $firefox->key_up('l'), $firefox->key_up(CONTROL()) )->content(); =head1 DESCRIPTION This module handles the implementation of the Firefox Marionette human readable special keys =head1 SUBROUTINES/METHODS =head2 ALT returns the Alt (the same as L) codepoint, which is 0xE00A =head2 ALT_LEFT returns the Alt Left codepoint, which is 0xE00A =head2 ALT_RIGHT returns the Alt Right codepoint, which is 0xE052 =head2 ARROW_DOWN returns the Arrow Down codepoint, which is 0xE015 =head2 ARROW_LEFT returns the Arrow Left codepoint, which is 0xE012 =head2 ARROW_RIGHT returns the Arrow Right codepoint, which is 0xE014 =head2 ARROW_UP returns the Arrow Up codepoint, which is 0xE013 =head2 BACKSPACE returns the Backspace codepoint, which is 0xE003 =head2 CANCEL returns the Cancel codepoint, which is 0xE001 =head2 CLEAR returns the Clear codepoint, which is 0xE005 =head2 CONTROL returns the Control (the same as L) codepoint, which is 0xE009 =head2 CONTROL_LEFT returns the Control Left codepoint, which is 0xE009 =head2 CONTROL_RIGHT returns the Control Right codepoint, which is 0xE051 =head2 DELETE returns the Delete codepoint, which is 0xE017 =head2 END_KEY returns the End codepoint, which is 0xE010 =head2 ENTER returns the Enter codepoint, which is 0xE006 =head2 ESCAPE returns the Escape codepoint, which is 0xE00C =head2 F1 returns the F1 codepoint, which is 0xE031 =head2 F2 returns the F2 codepoint, which is 0xE032 =head2 F3 returns the F3 codepoint, which is 0xE033 =head2 F4 returns the F4 codepoint, which is 0xE034 =head2 F5 returns the F5 codepoint, which is 0xE035 =head2 F6 returns the F6 codepoint, which is 0xE036 =head2 F7 returns the F7 codepoint, which is 0xE037 =head2 F8 returns the F8 codepoint, which is 0xE038 =head2 F9 returns the F9 codepoint, which is 0xE039 =head2 F10 returns the F10 codepoint, which is 0xE03A =head2 F11 returns the F11 codepoint, which is 0xE03B =head2 F12 returns the F12 codepoint, which is 0xE03C =head2 HELP returns the Help codepoint, which is 0xE002 =head2 HOME returns the Home codepoint, which is 0xE011 =head2 INSERT returns the Insert codepoint, which is 0xE016 =head2 META returns the Meta (the same as L) codepoint, which is 0xE03D =head2 META_LEFT returns the Meta Left codepoint, which is 0xE03D =head2 META_RIGHT returns the Meta Right codepoint, which is 0xE053 =head2 PAGE_UP returns the Page Up codepoint, which is 0xE00E =head2 PAGE_DOWN returns the Page Down codepoint, which is 0xE00F =head2 PAUSE returns the Pause codepoint, which is 0xE00B =head2 SHIFT returns the Shift (the same as L) codepoint, which is 0xE008 =head2 SHIFT_LEFT returns the Shift Left codepoint, which is 0xE008 =head2 SHIFT_RIGHT returns the Shift Right codepoint, which is 0xE050 =head2 SPACE returns the Space codepoint, which is 0xE00D =head2 TAB returns the Tab codepoint, which is 0xE004 =head2 ZENKAKU_HANKAKU returns the Zenkaku (full-width) - Hankaku (half-width) codepoint, which is 0xE040 =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Keys requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Element/0000755000175000017500000000000014175144502021440 5ustar davedaveFirefox-Marionette-1.22/lib/Firefox/Marionette/Element/Rect.pm0000755000175000017500000000671314175143706022712 0ustar davedavepackage Firefox::Marionette::Element::Rect; use strict; use warnings; our $VERSION = '1.22'; sub new { my ( $class, %parameters ) = @_; my $element = bless {%parameters}, $class; return $element; } sub pos_x { my ($self) = @_; return $self->{pos_x}; } sub pos_y { my ($self) = @_; return $self->{pos_y}; } sub width { my ($self) = @_; return $self->{width}; } sub height { my ($self) = @_; return $self->{height}; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Element::Rect - Represents the box around an Element =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $rect = $firefox->find('//button[@name="lucky"]')->rect(); say "Current height of the Lucky button " . $rect->height(); =head1 DESCRIPTION This module handles the representation of the box around an Element. =head1 SUBROUTINES/METHODS =head2 new accepts a hash as a parameter. Allowed keys are below; =over 4 =item * pos_x - the X position of the element =item * pos_y - the Y position of the element =item * height - the height of the element =item * width - the width of the element =back This method returns a new L object. =head2 pos_x returns the X position of the element =head2 pos_y returns the Y position of the element =head2 height returns the height of the element =head2 width returns the width of the element =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Element::Rect requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Extension/0000755000175000017500000000000014175144502022023 5ustar davedaveFirefox-Marionette-1.22/lib/Firefox/Marionette/Extension/HarExportTrigger.pm0000644000175000017500000013226214175143706025634 0ustar davedavepackage Firefox::Marionette::Extension::HarExportTrigger; use strict; use warnings; our $VERSION = '1.22'; sub as_string { return <<'_BASE64_'; UEsDBBQAAAAIAKNwtkwgNw/3Ig0AAHgQAAAUAAAATUVUQS1JTkYvbW96aWxsYS5yc2HVV2dUk2m3 JZVAIPQivUsLvKFHBEQcBQSROsCgCJEmkGAIEEFakCbNAgxKkSodpApShIB0KUpRmlJkUEB6UwYu 6J3RO/db9/66667v37vfc55nnXPWs/dZG6Awk+B00mG6YduMIFpwOoXZAaAw24FBIAwdQAuHyaAg YB4A+SMFlE5hhAEUWD9AgdSkQ8AgMBjBAewFO8tvcwGMf+eBGGA0QHAuBgnQwSHmUDgb2NwUwwfw HAF6NjZDgq+Lm5udkA6B6EEg2pFcCHiMPIA+iiLZJP6KahsaCV0kEq564Y4ShExdnPAueCchUwei twvOASMBiB0dgLDxe36P4OwwcnZXrxLwnnLu36+QIxCdMMKA4N910YE4WR0JZE8H3Km/UnAEd0CA ixGjBigrKGCUAGVAxfoQYn+CQDDlv3bynwDBBtbRxnAC7EeAlo3RkOCFJ9m54IUsXBx8MEyH8/jW Llz7W1EYNoDle4f0P7rCSAIS37pgEHS2IzqQD8dBIhFdnJwciKecHEiOLkQHey+nb0VSwCI/TxgE o4FQwEw0h//pwRQwiKbngcBvMMu6lyUrgydIDFjb0jHapt1BxrU2sYdfaNwrNWSRSBTzFeE/DGTs WJrTWSpvVd/ZmaebIjnjadeR/ky9dwaLbkzQ4+q5KsfK1j8FsOqQM3k8TRgDeDkieQOH3L6GeSqq dHW1JbqdyPYXODtZ1EczwKfFXEkksE5Kxcdq1Ry0pfq8L+tGteKUHTeTZfckg05F37MZ3BM70MKm Ti9be3kSazfi/b3ukdVNspy3KN7e+26aW6WiuhFFWmw3H72v8G3pqcWta89VUPNDp0dnU9h7h/Wv qcgO0Ez3mx1QqQs+0dZZwve2iEO00AptXaw1k0ca0qiuQZp5Qj5pizA6Ir4XJaxpXrWzdaCDIuWe g7NHemsJSVc1MCjO6qTKfY5rQ4gmgK48ChA2Orjwx5L7FwvuV7Ehu7yOghQ8wWoJzlYCvPKI15x5 eMVa5doCW8Fqzwey8XCsQFDFXfHboqkE6pyX//DTr+IB9zZ+p6UKbMHM1C1zwGOJQ+SJ9CjYTePJ mkXTecnu/c4pKHpHuEKVmYcc4oW9OBg781pj527ovQ3MDPE5q9HJIGod8nLfiBfdOmhqlHdAkpuR QyD0RJeJbYeVq3jUtIgBU1Gka8w4Aju+Gnjm2RDW77asjdX1UAOFU1f4dkeOoTKxOTNXtJsWmTXP ZxWe99yW8BjjMtJxR4MhIBpQlihw9N4h5vyiUG6AM4idU/Mr3wGDzP2dZI4RdBZ1jmw3vvQPokKO Xo/n15NBGvMse968AVDHTa3Wpbib6t229l0tizdGXfTX6IYr6ySXP/ZnBhqUkXhVChhGJNsVTgld Ovl44HrKqIJlI6rqdVTMk+hC/dJS+YzwB3NnfhWAMsym7vdjbD6uzfjfnKs3dJTwHUHnMF95nqDP q/ok+zb9n3QW0u3jUxnZr5P32PlkrnZovljERpjbtHmHCHftCGLLPeDLqLXL+ojLX9ba1VNx941Q 5n8kGzjHEz04NatLejcSXPOeixc4iNo2dnNMnbF2yGdNPn93eMsqwJPXxunFJVV1ieKMzvEER9ba PbdZ2/s+mojG0nk98dE/13cWZkOnvNa6Gn6Pp+eqWHBTPJ2xOSStR9eWL38QvtFjn7jHEvm44rdA wTb+5CR6UGxyRCgRxdChQPdoin5T1PV05elXQZfHTd4SUNnnnxUHiCAK5BU1cNsiYbxc7a2lvuE7 BCgtEx8zv/QpVz26CTcXqItxKvv0ej3yjdLQqklupl2iVqfmNbKO+FnhsBzy0n7ghgLDn3oLFpSE U4HJ3no3IvmF2OcaPBsHaHIlU0N2xW9Wssg31PDhdedgbefi9F3KDcqj/ErvtHW5IKMWlykvrRMU SrWIyBeKgt5xcurKc1Ou/mfhE03Tx6ShZcgqG3+l12MrFjuIamHsvKeEv7wFq9VQjjp96YCPYPUw i28inw9AoUUf6jrHd02HMNNA/ynn/v+nai4I8H9Xc04igUBC4+zQHn8no+3cCd80WvVQjpUAAMAq /aXRf8N/o23zvwn584LfMiPinqgXhEGi2peal603P3qrN0j4jVF9x+zd7Kr0bc+XIt3ftPGMfT2z HzpEPmcUwj65D6/pzhHUVrPaykluLy1rWILGG4itgcUDzBA1z8zSIkd38i+JBwf2/ObxPkAjVv9+ 5j5LUqLLJ3tFd6rXY5zS9GkagSfsN876LQ7e710Juze6eG2piPya0N1gKNaYjJhqpO1bobwtdvYk 3D3orJFgqnnjfDeCMW4Yda0q+NfKiY2MVg0NhUtGi/57WQ942WvH8AXXkPl7epuId6YiYrAmVadn RZDoFb08D7gSs5y+yW0uKV7u+ptTLaDZprAxv2wpQz3dgj1lYPeiZu/bunVQqlX5MaWC47yWmXqu /W23LhMNcWRVdtXNczr5uYRmljjz8Os2G1+e5dsL84QoWI2lZUjw7Diw5Kks1kbOLj1QqGBt7OSB SXzGTCiDkqjRIxH9kjWYxmlFK6svBaiUlvq0PRXG2m1NRtFXhhLZ+i8iN0JWI2DqrY/CED2jTpe4 J4xjO8uiavZ0Eo77SoKb0tvFtxDao67vaHV8qPsl1t5dddwZhV9CHveFxlrGNDZSF/IjzAIbSgzC ReXCgkLFqY2t663SLbN3yCP54Vm6uF4j+dskHDWzPmvn+ER99UOvdfBnvuEO3jkekwsWF1V8D/DV w0lVm2cvrkmGt74bkfou5BRQJEABhQIMR2LOBoUBEBDoAEAdIabDLyj0kKhwgPMIix9hBoAejpCB g2AwWgjkG5n4UVBO6L9eAMF531dEcDoQnBrEXlH/yZJUOIUnS5cXpogK5OFkf3+aERycHfj/THoK CAQowumuhOmCwnZOg6BQCU4xZxLJ44S8/H+nnjzOTh5HdJPzcHAHLhy1JwA9B/ySoQMIUQQOeYcn OeBJ6CPy2pG8iA4/nwQEKfz/c8K/WJgai+0XOh2w0ayBDwXiD0DxUtYZhU9kmbR9iZOJIEbeqaoa tT6Nvh6NnCh0J6QbrvRhpTat7zyas8RohmBZk0sriSuOEAu3jYhGdE2cbnVyWpyTH8gN9Ap3vu5f HyFIP5yp/KmmWDCy+1ZAx+MLWCV2J+UTDpzGC/YcXemVpusfrzPRi45u11NSzypPRa73aMrllaVU y6a/pKHlHOWWYncuHmCTree8YGlTcvMcd9ztZgvVnWg0rlFklaD4jFc0ma7qTbAQVzzfqNpba55r S9eyjj2Nthd6XwykHDwwnvTJa9X6RdAJ3YEed9QZcaU2CA8RTDFrWrUbMyK7DeapBT4c9M364eS+ Z6rIskKBqqedO6liCeJX+ftkHsow3sGDPLSelsyxbc982gN5Ydqb8DEsJR0JXKtqUe3tAM1LoxyE CtL1iW9KW39GQtvnTopvoGLE8lc6MUaVxphUjfhVpvJEFulsYc+MRPbBkBUTXJI5k0E73HStFML3 WL6oS2pQScCvo+Uj9UaBQntvUrV7RYWaLry4Ss24K2OfJVfI/WR5MzUFbIB+2JS9f3CwzqskOGsz XlSuNg6XklFwCuT25Qr+XaTZ70zt4NPI2V/OWmB8qz4cnzm7Pw7VbTjWGOKkPbVyMsc4psi93DAk OBtPEDPwDrZG2ZRnd76in657I4EenHyPoUCwhyZI+dArAcGV/z7r5ye/9pfBS78EcP84BcH87PcA vh8RGIbpH0YJEP0RhWIO1SZ8+K3Zwdvd42q13LLOyaHDyx9uMf/kH0EwKAVM46Q0KrRV564to2/6 sH5dOKM9llXuPk21Ru+5ljRrNfUmqcg/5WRGNc0B3NKtLEW5igg6oSEYaTKG9Wm8lhXPebeGR0Y2 uyg5vHhvk5GdusNMYj0a2hg+pcDgJgQOaxibqU/ItQj7tU1ve/nYZF1l+mJfUZIjlDzgeuJOP06K So92tixoPu/qHfXuDZyz4M651XosmpIgWibcYCGRIeuVUJ3ul8ZIitZTODFuYKOZwAJ9jfXsnOVT NbmJ44RPEsbFfFlrlOg2j5smoVVDB5pFaoVOE9xAE8VmzipaLD5F7HNZ5cECH43vjzvHiSS9N2it fKkbbu8Bdwwy6390QL0Z8SIys/V+7ixKRm4MnBXV8ACOFnVbi256k2f7sKewK8b+YHAdCQT0m4is yIVpILoZBGphW7kbWlSQwXyOWtbJy5sx66vcb2rez2mbXU+5ONveYWp8XS3tOOPCyPzHbaTC4GpO 3o0H4K2ERZCW0fNj2fYtH7SkZpc3gtyG6p8p/Cq0XVRw/QWS5H0l0/idT+x8ZtcplIyvOX9LbiCC Y4PFR/DLRBayHnxLiRHaWhj2HovqXEWmsS/MSHQnw0ICKsnirI+px1/Mv34wNBo5FxKeZbrIzQSX 0bGtzRId2/lFGV7LzwJn3xDGp+0+RQrfUvv86h5D+bLGFKi2QsxTSXynEeGXlkHzH1BLAwQUAAAA CAC6frZMEnNugnIBAAAWAwAADQAkAG1hbmlmZXN0Lmpzb24KACAAAAAAAAEAGAAmWi1S1PHTAaAH y773qtMBxTlQTCWG0wGFkj9PwzAQxXckvoOVmSYtQ4cKIRiQEAsSYkOV5ThXx238R2enRar63bFj twl0YIpy9+733l1yvL0hpNBMQbEixevzx8u3Neg/UQoBWNwNbdb71mAUvDFN3ps9w53JPRc6nv4D 2AM6aXRUzMtlucjlBvbemM5Ry8Qw7pBX52LZetVdhI6jtD4zMpwENwKDHdmgUeQgfSs1YeQANRmY aVwxLTfgPB1z3KdOaxREIe2xi+TWe+tWVSUCqa9LbtQlzyzmqVqGs2Q58ylFdT6StZ3kLGZ0AXWM xVAWwMOtLu+hIpvBiWHiZMyTAL+RCHUvom2CDnoXFNxTJfUkf7FclPMiaU7xcUopasZ3Ak2vm0mG dLyY6mukhlOP4nLrMmw9gVlAJZ3LG+XZ4oF1XbyXexxG1knLjfagPb3yGhdXzPMWfsW4xg0ZxuW3 f+Uxd/YaQ+fY+RQ5UfgJKOMcwgJ1BxTBmR751L/oZB2/KLPyzAqcwPgBUEsDBBQAAAAIAJuLsExt TJjleQYAAAUPAAAJACQAUkVBRE1FLm1kCgAgAAAAAAABABgApeDhmSrt0wEUzI9axK/TAbEPJE4l htMBnVf7b9s2EP7dgP8HogU2e6uluWnTIUM3pE0fGfpak24YgiGmpbPMRiI1krKTDuvfvu9ISXYe XdEaRdKQvLvv7r57+LZ4vv/2yXltrD+2qijIDgftf/hGULgSUl8IryoSubKU+fJCLKypxFr5pdJC iloWlAwHw8EfNH9y7kk7ZbRQVW3NSulCyMabSnrKg9LR8+PjN2LfZku1onFnwyxEZsoS6vEsl15G G35J4hX5tbFnMKOpTMTxUjlI9WacmDeqhAYtvKlZ0TYMJw5odWxM6cT+m0MxykxVS6/mJQX84ilc Wphz+JiLx0uYpHFw5RiGN0YiSBfws5o+EN4EiCECYh9x2vjqLpynSmRSiznBOe3o74Y0S10FzCpY LYPw1+PfuBBF4RBS4P5VruRRZlXtobwshdxkKBGHvrMoS2eEVhnBoNKeChtgBa8pKRJxRCVp1VTs RNBcAd2VZLUIrJkbuL8wFifK+ctZ9eS8cI3C7xC727fFoXYe2BBpo4eDA7PWpZF5zOnJ/svXf42W 3tduL01lniM0SWU+KLxPjC1S0pN3R+kiZiY+SJfSTiKYSRuhdMy2XltxYZrgc3BYRcNC+WjMmcZm tDccTBPxuDSaBHzIOzzxFsnJCZLIA9hkqTZOeWNDdqHcikqCrJo5fjcRL2Sjs6U46ZjzShVLJHXj 0Xq9/h93sqXU4HGakztD+tPbOsqPQ/ZNTVrM5Nw0fi+neVMUCPUs8Gs42GEXVHYmZi8Y/DFViIcE zv08nxg9CxpqfsB8WChwpS8ia4y/5G6kMHs5CmyYVVKrBTKZvHfQNRzcS8RbmgRAUm/S3pcT/5zD fwSpkmcEAlgKpi7VJnpATRYU5HiDK/OLvuQ6TYEzfyKJFUciprFxJGZrmiPnfgbAVcW+lUgC7JmS oSEClYFJ2Dl5efBqE/+cVlSy1RuyECPl0kstIn1Gnn07BXUsCH3KNXLaWh93lD7oA3bkbZN5uDsc /NZwuJHLUI/sc1vOm/C67nXPMMTkPS6D4u/EzNlsJiZdcjhtLpxbcnyuuHXcgZoLXNwR5LMkXJdq ztdcpVmpuHS5hbTiAfFbNBzA4LJ2McKakIIu/rvT7zl5tmlh9XnrG+ACHdmsOeudDBjphIQvlkqu /T2GcvLx46OmENOd6XT64MHHj5tU4HnMwHYm3NKsT3GTZIX6ReUPW7kxnBkt1DkQIlA9yB/G4rBv TiGytPKBNjrOhaQg3/XlivzS5NcwTb8S0/SrMRnNwUc1PVVauSXkGR6Iqf02uns7u7u7978cXZT7 BDqgvm4+mBZQ1JQ5TwfHf/KhUItuylZGc98T7w5RufpbD63KK1mqD5RfRn3vx/tfhZrlPh3TrYED Oq6Iiwf8a9l8lKGmXSAm+Mcc3NDThbuOlN1UlrxjeIrFF+ryGssRqzforVEBgIXlAv/WwUho/tx7 +Qh1omGrXUCiQDtysrLJGZq48sHQkrVCQ43NeOSI0MPm3BwujZvY+6nohr/bCgUPnbF4bDDBOYHb OOMfQYer0STCVsMzGiMmLzkuFTkH+G4zB+IkuQoUH0bAfbbByiBDpw+bAsv0LT8YBKCdsXgks7PC mgZSHaZrRzcg4wxdbCO7EQs+cxQSgZ2d4wzvoK2yNt0Acm985fCzoZFZBsvbg+xmBJdW2W43Q9Hk LAqlTYl9aA6PmWzZdna6efHcrDEjxTuHeLfr0NXxqLsFY27N2mHpG3Xl8E27jGKq1swCynkOdW2u 39ZiDc16ns0i0eB3u50p3hB5/3sfNuvhYGMz8HrEiHqZlVSl5Eix0jBfNmPsEl+Dj/u8Fciwksbx hxlvzhwYjm1AurY83R6/nc1mAX7SMjx+6RiNE9jXI+B/YQrx8GfxD2eDB54pKSlN0V6NfxoO/uUf UQlWwra/vcBWQposv3uiPXDepORW+xqexnZ46w78CEdbmgPG4eCI2ro2OuwbJ2G95WjFfod258zC r9GBoEvaPEHZpPyIl9RYtP2K2pHhaWPhqcVIjmM+TPjjTnFg5tWvYnviS62xTqbIUU1ZlA7r0Cpz yXontORlkWKtwWq0SK1cTzjxqVd16iDhUsimr9F2V4rWydJX5bZCMTIwo7Qsxz0y3nRvQDdHzMPC Pr07Yc0R2Na3PvE7LGx5+Dk9q/C89+9AuaxxoYSeod/UvR7uPrVLCmMKZJ5VhJOUrycy2g6I1CI0 OXyvGA7+A1BLAwQKAAAAAADRVFVMAAAAAAAAAAAAAAAABAAkAGxpYi8KACAAAAAAAAEAGABnucq+ 96rTAWe5yr73qtMBjjGuQ/KJ0wFQSwMEFAAAAAgABHxpTASc+sgcBQAABw4AAA0AJABsaWIvaGFy YXBpLmpzCgAgAAAAAAABABgAOrnjZ7O30wFnucq+96rTAXJf4kjyidMBrVZNbxs3EL0H8H+YqodI irzr9ijDLQzHhlXYiWA7KHqqqN2RxJpLbkmuZCXQf++Qu9wPSTGaIL5YS84MZ96bN2Q8hKcVN/Co Cp0gXKkU4UbpDGjNFPN/MLFgFdgVgkWdGVAL/3GvPnMhGEyLueDJyRsYwh1PUBocwTqCX6OzCCYL YJCofFs7Te9gwwxIZSHlxmo+LyymsOF2RQbc+DgLLijIX6qAhElQc8s4/ZMIzMLK2nwcx1l5eqT0 MqagMR0XRzCMT96cvOkVBsHFTmzv3C3Ew6GPeyU4Sgu3lw9wOZ24Jb88SwQFn0irZnAK+JIrbcEf HIrP2ZIiJprn1jhcXPqmyJ0hJU/JXa20yjDy4Sb2renu3nCNC/UyAoNUgzBq7A1dKYZqSXGNQuWo o3ZVKE8/PcaXaXqqpIn/xPn1iyV0ufu6UpJ+27+rlH6uC6hr8pQ6HIFnucCMrE0oHObMUGJKEjVZ VkieMEthCX+tiuUKZsRMzmyyul6T26xVFZeJKFLyZYVVGXklTIgtLRNGW+ofGEYrmwnoF4bLJa07 AMm8zBMsWw58MAepVmtOrUanY0gsqtO/fmEubTOuV8gkIkqXS9TXnqG+yl3WZhBRY8n+opCJ++6v mL5TywF88W5ANUqjBEZCLcPeud/alf+r6hytjhrIlTF8TsBRkoJaFCUslCYsJTcrqkXjvwUaa6JO aixNH8qNO++Dul8ZwsVvR3Optgej/WSoh5tqNlyWpQBfQN9ucyQl0WLkILu4gF4hU6TcMO1VdgAC qZoULuDsvFlwTJmJnGq11GgM7UrcwD3L+4OWlZJVFTdVuaGa4PCI1juULqWw3B9BGPqsbCZKM7Ba NmOlJo05ne/bkcFGs5z6vo7BqAFlCmumuSoMvP94DxklS+ojRTnpztFuEKWfJV6UTKa1N5331O4Q knKlmCjYxOWPGsAAGUCnt8ZQExC6rLEEqsEWWno8KFKqNhHBmnGDDW9UoxJr7LiVEDNvMHH8vHvH 0/P2/h5JkSG0g/0IQswa/iYoOqVWFFUpXRWGNOol3O8dqKc36mYGkCKNWTHeX4Y64XH9a7RvUoE0 Dj/2DHbtz91+/qlKCtc3UWfs9H1JgxY8u+ZjVx9wqLsWd6Ja6rDw9RZ3Im58OlnGMTzg0m1pmB1E mEHwAlX25pwlzyjTqBPhqdloHEgZuopMwyXdSpZVU5VGaIKdALwjHxIODaaMkRXMlbWC4iXP3SMv hYDbp6dpxa6BlFlGQQRzY7m61vdHWyeCREyNs5yTIRI7ZLegbveeD++npEu9pjpIiKRIY3zYTgRn GMrtZveoRsS+fEtvAeWK44tG2KlC43bc+bTXcnSD8BUSDf+MbjL+ckR7/0cmhx3VG5x/V78edqvG TK3xxzVsilQVfr1nP8nQW0cab+Ehr0F2t7rcZkrjt2N99p1YH8XjR8G9O3pN3VKj0uVOIsppUNG9 4i732cF8nNUXz/7dUedDjeJTqS/8wyl7+lAdQ+O2JrnMvAHMgfWlma1QPlJgR9h506jUbl1qtX8R fvwOfzx+/BDlTNMFFF4/Y5CFaHzcGdX1QY7718yydc00kDruj11j+95VEx4GgHBk+91VUgQo6I3e Dtp6GfWqh8OY+vdZqo30L4nA2E+E5R75FfW74w+TinHP8ZHRbZwqNvSELAdleLWFofiN9B8c8Brz ryiLuvKaJata2uEVGQQWDm1R326WKLwuWyANWih5zHaDfilMt/IfUEsDBAoAAAAAAGRQKkwAAAAA AAAAAAAAAAAEACQAcmVzLwoAIAAAAAAAAQAYAH+Z3NXxidMBf5nc1fGJ0wFYmTHS8YnTAVBLAwQU AAAACADDTYRKmHd+NMUwAAAFMQAADAAkAHJlcy9pY29uLnBuZwoAIAAAAAAAAQAYADx2GYUXrdIB N+s4TSWG0wE36zhNJYbTAUWaZVTc3BaGg7uUAsWtOINT3B2KO8VdizvF3YsUd4oUd3cvgxV3l1IK FHe4+X7dWZPJWplMkrNn73c/b06iVJRkMFCJUAEAwJCTlVQD14H/LciI4OeAsAkUXKE4yeq6AgDa 2/8WGCAzmwAAUJVtJCRUVKwd3RxdrR2dKOQkJCicXBwtbewtAMBrJUfdSkP9CF/oduNK/GNkYMpH RzU8ZAo18ZhA3ERaRkoUbPlIyoJlPNW+N1JS8MSj+ZFw8fGBsXiqEPRo1CWkPcrC+JCiyOixx23f IruFztuDiZemReHDysuchimkXmT0VFlO1Q+B6D/Eyd5shYwMDm/vsYgGoVI7AdjIDeaUrs/vYYA7 f0FBJqpepG4AxmsWDRnoVczqZSOJeMX6I56hDxMYCtPrlsj7ETkQFxD1iRcvAUSlYQJz0mnVgXx4 wBhqZroLqDADxnQBB7NAYM5dhi8MoJdCiQeTnwhQvLWIlASsBYDKKa1oKUCHDcC1/DjIBywxA2xy 2payQG0L0DuJjVYPIKMDbKqRYQwAvC9gPPr+vRcQmgHgSl9oC54zVWMfMoKRyqgXbKPmkwrSQ2Z2 RtLWZiGjn5R7x4TLbQJrkinK0u+dQcgRxIW1dTkOAPmJeOBoL5+9JjGrJye545sxjZj/9CC+Upma 7r7sT1U4iQLAtpv/rxcIawNRoABi4OeXQppTeGtDtHz32xjLeizRJiD/aEX70um/2EgVfG1bXt7f 21uSHxTXMxnT9nux6jfq1X62v/cXfLq93ureoQ7iMAiSgb/b6h+/UigmmUpCCTVdO0iQeWpBe30i naUalDNt02HE3VXFcY6XzIy95CimihJPYhiimXk139mEPMIJb34Aam2FYXwTmcsMzOBuPtJh67j1 7twCwEWHY98cPTJcoHXI9pTn65nIs0w+DRBoKZfkDACGktQsGmMNIiPIACCZH8RYK0Z81UcA6UUk 6UNY60N7MuYOFace7BfHFkc1DyRxMWaoDBanD5+oJWU0Zp0TJeXqVbUOT3bCZOnXbMUUcsJ6MEHO 7aPvDEKBFxtFfmtCuRIJYxqt8z4e+V1G2B1lnyw8Tgw1xc9IXFWqj4kfZNnVojUoNGVJL4RcYPki 9KXbEwVGAY7gp/BOGfaUoHpVoTn0Jom2Mh6iUOlCtGlzN/bMBPz8rGmXRiKhWO6Cw+kKa6yVBGGJ Ebbe3wRInIP2bCzDA+J1vBCUZDNoHVUlesUwUa1WJVxH1LXZVJ0nPE6QVC8KN7ynOIUJmZisjoKO 0pJsGQUJNdebFRQeFLhQ1X46quSyj4207W/uBwlMOdgjdGQh7xdwEt6yYvtGSgzyUONESXDQtX2F lav7rjGDMyM946D2/gjyI4tLluh98s8Vc7t6JL4UCAXTaNhezt7AHuce8R7lhR5qjqmo8/c79R16 tTFbecJ97H2ufUSLdA4xzaJMtfWyAo03HxI+iVcVNKvVqEaUveVyz+SbCMGX+1hcVfhtlsj6nfWt Tc1vt/Y369/IlNKmtWegRzK/037DXXmiY4TjhYxEWtETZhEgEVgRehLUpCuUEM7GEI5osPGwH6cf ZmBlOGrqMDYzfsFLHEP9Jv+N9hsxI6vGj/KS8o1yNM1LTahGwg9Ddb9qaU1O9d0fKHOpFZrlOSrH 6sTqVj8EyiLKrcocNUS+++s+RyHqDigMKEt8V82vM9rtHKZAMyNs8IJQ2mFUhLsHfRikrZtY/uLB 2JH+TNoRI1T+o4BPglOeU7tB7TLFUgRpN935LttNCeWQvjTt0j96A/eMYYeBuIRLoY3dQt+uPDUt VadCtkK1Qua4i7v6pDSnpEuqQ/cxCFNsRJ5On07GKmESIedQfVl9vRS1FF9RiP+6mqXcrpi4oEPS s8eh9DQ9t9swMFfBsXTDu+vwgcSP9Vnj+fND6bUMug5iAzo14ir6JIHg0AdnR0FS1pHVc1UJgwFR iWKTbyZp26GJbb8Ei+al46SxYm2G9Xepdz8Pfx4uokWkpaT9qPhH8TRTTbGthKFEQEFAYWRifKJw YiHjQ5YQ+yLHYdZh1mLWZovxJ69PkOay5lYLufq9TyW6p82dDmGf+HSLdTQ/GTRIV+VU0Sx0VHWU viltU0pWqshdsBqoPW/+2fyjhbB2fwlxtbr5c7OfiZXpO6uWoeqB0+SuZL92v7hnISSMiGXMCtJP pP6ujS64/Fr8G5mT2acifkNXqUesVjpT2FNLnLvNb6EMgzY56lmzMlbvrNpcQy4jY6I+BkNZ0oRZ soiH10e79uTdrp0Ntn1mrnOyGrL6T8pWa065T21OI9byxrnyGHJ1OV2nB0YltlT3f4mg5X1js9us 9FeWUA6yajp8z2rGmqhdtVjbUuxZfmty9k74Rtglb1d4V5Bng2ND12icp7dltAWqPOCYaxS2KRHw /tX81ef1F1AaSANjBRcVJBakB/t45/OFsidehH/V4qZ++UQsNr9NDE8sIbgqeL+vitJ69O/XKsrb w/TJxEkbzDAV/98L00gmByYxA/7odGERYdrhIvFOkTpEv9jTPnj+WpNw3ZR8kEyV3MiyYlfmq2GW /8AoXM5iK7jC087VLjLMYqk7Pq+ToyNn/5GEj4WxmtvBvnvz6eQyYidnB23X910666iBk3fBZP9V WCn+DxYcCH+qqEKkEm/KPzFsxjL8WJqLt5408aje/aT9VlSsv7h/RS5J6DOSKV3IMir6JP1N7Pzg TT3EWQzZbwul+/G1UwFDYT2LPxc/04PiM4cQfxd7SFLiV2fxckrF5GarP1YHlh7QSouw+pchh5gW VrJvSD/tGr5WIugyozaLl/zW+ES/yIhUb2WhbdkzETXGl1qRyzRxMt4wtvqVJ/ss7/UEpx9nO6m5 OPyjNjOpzkFXpS1fvaCz9yrqLh4CU7gWYqVga3Qz4+d55YoT7bRq/y5aH0W7IMO+seC+Jhi7YzRN 1P0wv+O1xRMDZR1W3c0mk4fwHGJhvKjoatzVd2l4AUv0a177xBGOYz0ZaymNPyoG28ynLGcsjw42 mHP1MFPL0w7SMB0wllcaMTa1Gk+ub6RWWdMm6Fc7Qpol2op6BFs7LUp/Tk4IiGh817jSeNDgPp1Z bzJZvHP4dUrs23nD9Xd+Va/z9ln4Z/eqJoym4rzlvMWj3GPDrUh1VPnRXerjZ4Mqpc/OJyNwM3DP SO8xmxfb5uZJZ4Q01BIWmI5lMEfIu19yzjyZMbgxNOOyh3feO0l67x3domzekmHwr3jE+lZuI82S e5BakbLaimdeZEpnqWc+6pnqtSuPC/9a+LPh8ySzSo6VzSWo6y1/qfHTZuUoeDbZ6mIqNudbjp8/ y/2a8Wphs++h2eSmLZbjWTu22/aM1GvMoUCnwKT+RavbRf3q/lvtVu3MGn6jz3/c/gi+JM8qj3Rk d2RWt+nbtZ36dDX5Wt4eEefyTK5fdXmf478uzOfyCa13Xfo43g7dXm/ir/u70D1PQjuWPelOqzqV O42uRE/EliXWI+ZF9F7Wls7WAmx7PfMvf63J3CKGhUy9JF4nYEkgJGQkQMMxwo9zeYX5+HZEXJ6P nkolpsynaHPfvRS+QKvf4bP6380fL07+Sv9VkHaRltzz+Uvx85/eY8Ka1eH+YbLun16c7IZXWo8n fZ5rnqRnRZtYyntfKALoX63v667Vp0sfkwmTk1/h4dIvn8w4mVdB3qQwk1VTBAAfGpBDQwDg/hVc HwGAOxsA/DEGAP50AHjnmGQ0JA0AtGxykmIaXit/DTyzNeavX7uF/ZpvUdE+OsLOSGKYokifv0uU VC0stJEWUyyk0rJX/bVEReRL1UygIaqBDQcLo1FOhdZK6vHOw9PS09Iw4Ox2ZJy4xMGsj5Tj8LIt j0dAwPWwy7f7ds11UuiWRHzRnfooIG10D5sa5ut3aiApBV40KQmQVcMB5ORhKeTkAPDrcOvdof4w NAE41PXS02zAaTyi+C18we/2anVchGCKfJvM+ePzg7LyTzMzM/oKOOYwrMhtSfQHkYXpJRItPitC sYX08ED6hOWEfXMk/xV3w0+3OXNucu4shQgNxQro6GhZIX19KvVP6UwT3MySAnpUN4NS8I0cLFrI P9c+poEReGW2UVZ9zIaYSSU3Gg2balJwzC8bDExdKrWsChV2dfl1Lh4Lre9eo183eqNqKjZVRKMq Lj61UkYa5+FKIRh9TApdWFjYckRIVE7KtyGCQLkzFUoYS2fbOOnoDnu0KtzRoM6PuFKJ9NHwUqrT cM/H7WQLEc5nfonIu9ICiQBd2vKLKhomkZQqW9aEU1N7Pj1qthJdJxmrbDTExtY+2+Pq96+85sbG 7MnHuy9SmsGxtHIc0UdCCHm/uBThxLCNdaTrdD9vlpZ8/5KY8a2J8T2n2Xs5tc/4MQhkCUjcWXhk Ce/43aa5MDTVYbrOgijpYegzUZOrMstzuOos0VnkkiiQYbnkgi10ZOvRxbB7p9Ehlpwr+g6AbEVV 1f1RwcNAg96nzKk9743qY376aNTJBcM8Gvg3iNLAtEacR5v+wCc9vQhNTe1N45ZVOw5OTu201NTu iQyFY2b8mB1b9yAKZF612d5LYfDo4ZYUP3G4FNGCt0fgJZ0aM4mIEiaEAcwIOPBSgfxy2zkFDsvF 86QSGwo5HMsGQhKomCqulyCZEwmSrupTQ5zJlkdHBDmv09af8PBw6Mrq6iWGpVvaNmLZ+5rhBvdv lPBiocyUJycnUixpxw9jY2PHszNqPWS8tU8fGpafmpftHQplcaZhYOFnZme3rh966yHwqoFThyEl NiZ+124k7+k0WlIDf72XCXseMOegZBYi5ePjk52SRQJG69GTqMGkWWPxBFgMCCG9+y5iuJLYe3VT 0PrgiwuXWBI+HH63yGBkMezphYVw3NK3nRRb/7YHkv8c/3Eor6nJY4imshu/qkaPLC9XwUUoL0ek QVecNSZA5FJEKg6nXNHUDIIwfbXg+yuAOI2O0MAZSLjZ9Kkq4uH6+HZsizIkTY1aE4oGC4a299Hr +3deE46sXzMz0aQCzbv/CgbyrVHbq0NMdL4XFxcyCc4VhFAxgPsEBb701ZEQpKKwhGSWINAoPdFQ wAaBg+iuxrx2d6/9VDUNM5qEq+Firit8uswqh4N6eQnGmloXFZN3gM2pbX3X42zDT0/R/VpBSlU2 xJkUoxMC/UrdU23bsPw1Jyeuv18cFx+voLYWW1c3Y3R0NHTgCBhxNoZVmIdVSLYg7JyKWNB6F0/C Fzw8LK2hrV1U3Yx/fX1tbWMDZkO+LCpkZDhFGF4MfvMdPKXj7ek6GhMTU8BsjVHP4eP12S0utF4R llV01t06iCa/q4q0qanJZErbQHPePSwKIAhHuRg25wja502foOUlUeJVz3biJWHLUBBjc7NFCBVG nrpU/SjqWNmUWRKEPC56h8f0T9Co1nUkPmx+fv5SUvU7YXgU1MtgkTKaylw3fMhUTFeXUDqeKIzd TNpsOr9vFDoVMm978q24vLgYQfPbt6UGK6zu+wPiLx2HeiT8bsjl5Q99uLtkcknwcklb3sJ+h5M5 aBXl5dVEAh5/BxAQEFhn0JllPhGNcJYUKFz7dP3zEgwqzuduHTw4OCj8bsTw10NAQ/M6oYcSfzie 7+Q7E0UStcnT3Tnv7vgPFU08Ac+/dyc3jt1tq/suVsSR8VLRhaLY8KCqoZNxwFvl+eGXZDsjjkGh voo8R7ErKyu1rodKX+pO0exXWt+Ao+sr5twQKitH1dRMzMvrcf4z9yOA5WTTcTex5P1Srb/Qzcnq YkHocdxPy8Cm2jJsmmJ/iqffQDqXN0c4Brmw1/phnkhdT+GOYHQhv0akm4GMhxJmiWJW+CUrL7lE BGacXf1Q/Yi7ARIMLP8VPQJbKo71E+mKqdBIKrNe7UUR3t1ZwGtMc3MzZhA9lIMgXJVWIfvfXW/Q /u3njcaft+uemzkqQGh8UCE9mzMJKiqaMgEgC0AA2r0xGDYYNmJtxdlx7/v7kNXV1TNrGwelXd7q 2axSelSJcEwpBwdMSVX4s90QeT1zzgUBxUUxeMsgk5GI6jBquWAKOYr8kK/UKOvY2zBguBhUvpNn a4h6/hMItAZVI2sCNYmizvPMkITXKV/dTnyWa7Q+tr+//zP6gKCOurprZCH9G4DcGgfg2ym/5cZu hwPQAnGAZ5gU2FHYr/jG9Gi9iL1kd6s4xvUMoUfoAGO+J3aBaSIg6jEACsLPgEvKEr3fsXGHT5Si braNg5x5VBPSv/0xBC8EIFAPO2ZtzQ9zNsUwKnCK8BTwvRShnTcw1nBbGIH9aPkIonBgh/YHVDBE jzC0ncgC0YBsLGNIYPDGxtvfUc/QeohOqhANtxlVbh46Fy2bc7bQ7yQkFRRjzsBvwAIiMFtHTiEO ewXnhBa4DMwkfpcskQdYYbwQAzuYgFaSXiMY86uh0iiNP/w4m137Y/H6SvwPHTUOD8Adfu880Oze CJ+NtkVu4HI2RLil88nr9wPhWRe9p/2oD+lD9yanTDA6vfmwr2Rkv3SoflwJHEXc4hvhu3x3Uexp N4x2sqU3UUvwM26kPpHazFdE5W7DrjwjLiKPefNnLHEWn92GUI+ofofD3iH3wgOa54CTq+CBXSai XFIwlm2rbFwWR6NVuEmvC0Qn/LrHUpcNApdqYsKR0ZvBm976lWnkXmTKZYu3qO7lVhcffSDYgpAt 6MU6E3FiWmwRInXf5MpRS2PX6inRZ9Lq+/xJCIV0pPG5dxiTywAORgSgYkYwmM0nUtB61tZUL/hR /uonFVY4CsotWERJFqDYtY59pX5TSC+GKxWZhKyKTSlHRf3zNZciEhsBJpgefn07ifpeKKse97cE KmPPJ3hkTc3rs1yy1fZ4pGh7YBFWBxcNwA0s1Im9vf/ixJ9DwP2F4OGWVUJMTFhNfXbLaSnEkK1h Gb1hCMc8O89b4Z9PZxbpq913xEBc+GBqMRwp5ELkvCmkoHf59BxeGz2eLle/VecWFkwDpBAwSXgB Mewdb7HHET3YbECNKmPKmeTgxZbFcjw9qB4GLimbT3cBRnFAYRZkg/WH3EL6pS47ng2/W9LKFy93 SISGkgwgG9fYgoRBFpp1MKXsQi0WWWITS8ZJ9Ax9MBt4wAA1KHGGh87Y1jZ83UZFuvcGXf33jNps UlFm/gtCxzpZsxxuPi4QrzISIpZRt/Da2EgN4CQq0L4lwZSBAEsSaQ+yKNgIgXONc3vWtGXwkXCA nSDK1zfQbgU5Obl4y+CkgaPI/QpKcfcuTBhRHOpsJSsX6rcpampqO94Ss2YwFGOHyknU2ZPf4WU1 XPg7kALnZNECKVCWSG/2ZGnpl09uvACnYdhAoVQqk9BwOobzUcuGYVeUEfhAZjXYWsjqZ2WNpAdM 3apmL5a7dKBWEjbQhMCpVHZr6JUqY+JgS1oHXZ0NFSgZVWZHh6NFCQylmRwcQlNqckMFDBbFfnHp gNskad5aRZfCDcIDxChAZqEop9t/F6jD4qAfyIAPFDYiUFFbU8lFyXApPt6/q4JjqwOwP8zLJcki ulCzEXbGbKMYt1PDyFIxwOAQAZJrs+MaEEDyM3EYWZ682k5i+Y+gIifV74OwgUT3DIqzZp/paBmC AOyIdkTAYDZkWSwpJRBApi4XveLIH4YVXdLleDWR/wAeKh1XN6ol9GMdQBG+GgiPKxUEkfuZqKCC I8W+AX9gBiOKfCjyRnDv6/dBajHcHTnAqBQAntgPZhggMJKqgsXuJKqRTDDBALL7i9d7mkBqOWyE 7RSiFIMOD5ORm5OlNlc6zL8iQPwum9codiwBO8lbOiVxQ/8NhstK4Tye9ACK4BVTmN63KIBual8e FVD240ffeyyhWAxiblEcqa9Bc47t1Wa31QZedssc5ubmWy09j8wH+t77DOfjZAk8ufBOI59g1+rG kWtGkv/BnZ+Xa7ZeRPPpdHgwE4QHTThEDJnCQB+wCj595l468fDqMoR0B7A7dO1reXVtqAMAGXbd 1MNwCsCciCO66N66uoLjg22o8wCb+2UHBUTUrImFbSubzPB52yEkaJrEljfEewelVn/HIaKlqe3N 4Z6XHQTFietCHYQajmBa2ufH59vzwSSavrfQAnNquWgNWJTgwNmjwdK8L9EyZP68BX1nLm3YSDAT dcA0yVvgdBC+V3u1ux8uhhzEFfaGfPmk0NhY9CV77rsv3fSuP09rdyd4NgfBM9NBAJD2OifyU3UA Xpx7hmCKYL6rt1HxZELRHfjMwRTIqoQdX4TJUVFRz9NkyIc6zroiLBy7woyeLt5hhzplLcuqPd+Z JRR1dwuzw2ALGBXA984X//dHNYLlNSdxDKoKvkhgCBJWvzlHGPvc8bXpXJmq1/18mnOfMquo7SdG sEU6La577q54XxbiiqRa/JqbC+0RfJIVf+BdMYPLF8rFm46MXILpgZeFAJ3nD8c6TS40fz1858ZZ WckjS3O9iUB2Vd3suD/Icn65Pxx4a3KtU6X15eGoFNzRQjwupJD+nCO1eSQZqIocpZOZ5QJJ9a00 /OlGE3GA30WNaoSuua60vDwqmd9fdnPzQTl/aQDt3XoU2DrZzkrrcJ+uo+jeyhOdrrQxkQHxFIPH H9ddp4TC277350lDvCRBM9a67lCpXwf77ds3FVwaAiNLaps/THMbG/FZDp1v4uLiTIh6qo/Btn3L 2k3+llbpby1WxXx05V9IpWETEZdihCaKja1tMEQu3edgTrhlTy4pNDUV/z+93B2Ou1ttmNoGYUMA VyoKIbAGphMP3ksyt9cRiI2nUQJBQS6pVgcCKjTIuLU6zEHUqpBK/JhXoZihTfuFr7eVtZ+YtxRK jC3ZJt0gTOmJjELp1xVZvTNcGGgHUyN0FSJR9dcghDjPa2qbbBcKdKxtBt+j/QNz3MSPcLnV0Otc Klt4+4qMnZEjrS6RRgH3OWPgiXAAHjkYqFlEy5tCq1l81+rT9+jl5WfoJWT4TxBLN5Hy30v5e9CI ObQQi/e6rootXLyD0SfHAhKAO3WrENLMksqiv2uJbaexY8QF6DyGSA0xCGrDdaCcfSWLtyB0giCl 7sIy7MLa7Oog0k6G+j3ebBdhicTiPAtOmf2eLvC6vl5sdx+uKylB7Ml+RgOLjI1JHEcKNy3FeMGi Ib/8xXxKWt2c0JQiOY2AjXNfNJxVN5UScaZfrhA39WLiLEYln952dNMWm6tgcbqOGgWHAju1pIYJ TxEYh/1nKAjwwnRibdHRyetLGe7SJMZeZ3yN72/ULCrX+T63w4tZ03nDGI5OOHPi63fStPhvawep P0XOLEAysk4EJyOSn/HebOv1PElsuD0X8MHvJRf/9qzzw5QZOIX/5xv6HUeOS11llmvN9ayr1etg SGihsKbr4JP9avsHE2XZiJSyCoRagl401RL/2LN7YO3NNMLprLtfCwTq0/l7a3Q/n/UrBErPwtDX er7rs4akOKvjdSjOSxJv+CNhbnWLO7NAeZAqujATV6pNdbaRvS6dXBk29JlALsmtx/95sryq6lkF V8rV3NQ8s6TAJlPki0JqXFJ5EGb8/hyWQY9pk0pVB4/sLJcOBD8lNdV0ekus1f7GHv23WCF0H5q2 eyOyia+h1VE/eF1CcOgtXFkbpioIlY06Zq2S6D/GuDUqK4+urKzEUlNTt1pYnEcweEi8xbuNik2y o6GhGftaIPHqevdvuyiYApeIFMT10tmo/wpWqwJPOA9LuAZLcGrg8DLMum6j1+71Fd4Q2bY9mEIU W4q+PhYsgsjCJTs4mSTqTkPWZsfN+IksHrMhGabaq7BiAnYX85aHDD5X2pW1zzoXd2ZOHesjSSoR lUd1vVU2K87Fox8Gb90XjRJ2dqjSmfF1YI+8mPHRxmk5rTODh2yHSuKynB4/t4QdiZWXU3FyNrj/ yVD/tfKnv0Di2qljtfAmPBjARmBns3mmV5KTE5OzYQBNNOhCKznQZyuPfUzo+/2EycFNoPPY3I3g SlLeFAyAEX59ujQ0cMRInrHnLmpqwlOG6Ab39Dy5tGS4zK9VsNnOyT55QvFVqAll9RVDSp7xUSam ja1prYd0xi01tLR2EiPIYFj/CbE4UEI5JkVeBdGJBFvVySMLJd8hmxCUGOLS1EJiXp6hpqzuwzHn pQlYlr8mp6Zk5ORQQHjR0EQYYFvZHojAIudzMU9h94IXCz70nluYuyEACoqPLi4ulXS+5Kp+rGlv RxTOYx1TXMMj+i+A30d+s5t9zcqKQLg8HFlL+TO2BDqGh4cHJRgVdXVSddh6Ej2/q9E39KSxdv+q pePI3MfzKYGW1bgFLW1j9VCYkQzM0E0Kio8c5RUVbCY7obuWM3s/1f8N2zlmqwyxCQg8fre9ekun OMjcshHccdbeIfMBxl0xZGTW21zNXdN9/AUKhWLdHC/WSNBwWyOkK5SIXvoeVmox/fXQy19OTM55 9ovVG3yJfu7i0EmmVe5Pnx3OrlWBSKmyHddDldoffYbD3lExMaGVF7qRAwBusRmaJDY8KJvhMKKz XKCNzLVYZvTUIxYkU17Df9LK6qgaaErhzOiOq7ST4URnZGT0KabmH2Br9dsmbfU5raFANa0JNz0X TPipXnK1ROck2NNhMoB490U4QLedLB4NAD0V8E1Ew/R3bcEoVAC/TV9bO2J4eJjN+LgdM2ZIDQnb 3TChReJS0EvbI+K9Q8PyyejY2IoVR4a+ibuVDZGmpqa1sE+jNqL9Uq2J19X5SG+GYp9LU9c+eoaA 56lcznMwxVa796UTNBjhnNui/fvgwd4BLCCs8wJTncWD9V8LyVt+XAdFNrf2RVNTykd6uAUyIJtZ Lr7Y1BoneGNmvib9FT5bScbMx+ehW0UqrsSGi4fH7Gyjq4vbySrHHd/Q554GnYCNwtbxxQu39yn4 KZn/ESVY9KMrWiTZns6/+y/WDg7Sr/X1xhZz4QMY9IyM/fliIf3Cj//xQSVtRmpqs6YWLn050ZJV 5PpvFZTpNxm8yYY6Go33EeT+vEJuDKDx0Gu2HyJU93XMchuMfOO1m/faS9qyGe+eV4MW8/cI5b25 rvCRycZwUPLYmLyb1psPM4lXl3mvJKDlHfr+MznYwoiICNOsPxTFpLkbAl2q8/PV+akLQu0sFyrq BU8DBC7pr5cNJqzo8SB8Mh4hAsK9kQruB5Erv1SLOncofln5SgXVgkPL7sbj6cAk6buiqqqqkW9N cfU8P37QiZw8g2eo3SlKaHev8eggDWg/mTIp+QNij9nqQpXXS1ixSV7NknbQ2NgYm0hIiY0TbzZG m7f3KmE4zGqUb65guI56RFyBDS4enmPz6omJDwCtX+oJEPkvg/wAP2Ejnail08kvcknTa2uxcsm0 hWVl/77DGY+lMIyOjv+tDwgBMailpai8nlEhzevhR2liVl7EeWnECGWlUQ+Z4FREXEIhPRjwtZTc 1NMDHZbB9pj1UZWO7qISG/4ZNfUs+egYnrCVb/BivYfX25dNebcPABExqRBMIH00AZsx4PC8S173 +7Tj15BaZJf88vIyr9VKIwtTI7xY4KF3YxN5w6g+y7vMzPit4EtW5EsTUKtZF6pivDdMenthKp7Y 33IRtO/iwwfO2x5/1zVpWGUDySxAJhMPMmI6kfmhyz+JV0CAOEM3g309O2f5qbzc9DwEa2s4y0HS 5yFB+/lmBmrZZ8/t2a4ujw0vSIaFiE5CgN8D2iJ7biaWdt610HXQu3VvPvql3Hny5+ReXIjl4UhS zNrRxQnSOnTlcKdB44KDgzWVShuY7UFvrvF3NaJKr/nmz1aGAu3V2WYPFGDDFQbr/azdm2AtCu3a XMX7XnRvf9/9AR3Egbnfql6njgFos9q8byPu0Q7MprecTl4HQFpYEUy2IBwaGnplz5poEs7Fwq34 2/qrgNZAm56J6XK6NwhOZL3582pxuIdSb8/jdU1AVwSDlcLRPMz4xERYiQezYhy13OjU1Ijf7WZC 3e5wIavKR1emldVVAS4ETc2wIdM+IzI/3LAwBJNXV/4csFX+rb3NTOA2m7DK6zOPb44fjiO/VY6Y /bVv9mz+e8u9OKowcvu9dKd84jZRz7L98MNr5UgQHJKPAerv35/yo8C2DoH85RxHClxZIeLi4nKz pmxqaTEDtQ1MpLKPfiyvxnxExET9tFWF9KPj45VgWF7l5XB3h+2km42xuPUWPUZlk3r/bQ8s4OLi /62qqCC2X8oPR3xM4HkJ8j3tKl1EZJt5XsuYWVq03uoNSueywQGHjt9wI66nR4yIRRZL7NBa8PHZ W5Cs83ZSYIvqy/sk6u0rlLcb46wQ+0vvLgOt7BCgVqddi0AcyZg9a4jBKrvuKm7RKIAjRQzs4RVa 1YNrRhNSNjY2NejG2mm0Uqpbbvznj8T+m6XAFjqJbJv5gQJIxyq4eJDBqmN+WqXsyCEs4cCr2GqG 8rOeV2H/uwKOceX8Y357bmIj7vy/Q+0rKw4ME25lyGeMibhhnFPZDw+ZU8IdVW1tfCPGOHJJ1nZ2 AzEfjXpDsETmmOCWvhmjGixun3s53Qp5egrsHJqrWGrRsrTfmXY22ROSYCKBzlxFSyvctUR5pO0w Z9VCuw66339c042/T4gHD2OV633Qg4hF1Jh3iAD43MJML83NkSIJKFi6HtuU5tz8jcZjOsXKKcaP QWDSruythFqaXKWzdxXCuUkHUxj3TfDspqUZxXfLt6z5oB3o8yGRjPRtO7nZ2XD+/pZmKK2oiEFC 6eBrsNhXl/3lRtn/In3ZFmFeMcHm3yymzC1VL0N0PwKn/0NLTc/Lg+fExERbgZqoCq5g3R5K58Wf ivzfu65YelULmFaRYIrQQ3o7vRdvlgy+cEQWltV+XjtVk5JC2Hg6zFKl2fbOHWm0md0gFDa9UxLD bu88w0YIHB8fd9hMGTK1yT1vQHu92Jl5lxuZAgtqdF+9BZTtcIxv5IOTR0SRBPBPMCGGpZtXnoH/ G4iAgg+jRlvnPv0AduSXg9IczhN/VWyflLlkYTKMlcPL7WzXSd32m4YPi/MLC5goBjUTlpy5fgeE Crkc96244nqisIOo/Cjpwq1XMx+dGWf1oBv9D9fHRaaECGK9sYScuT/Hx9c890YTcxmWCwwbcylm j9wuXKuzL0dAe1jLkSLHpTiw77Ld7Xs2EBMQUDeyk0FtluepronHkobW8zLh+DUnx2+Mo6ewEBZ0 bT/paQySqEExmNf3m/iAVfy9xG3VgGwfmQpu36R9lVquP1QuWUsxg8NCd68kAvTXw6w9g65Tgqu7 MGU/fgRRy5lQsE1SQ/yN2as7XtrVU+9BGo051Q+9ubkpQ/lOj8qSJfSoBRbxn2ufhByOU8siiW8/ cpOqj1tNJ/FaTtc7bu3ToN/elm+v2HN31roxIGIRBh8dKp15bHZ4nsXf+0KgOzO6DQ+NO56dV/9C sEweeTYlGX4bJrybnRK4zfloZfBQTYE8gnf0ZQ0HtMmgKhfuJEm4VndeDBvHP3z1Z8gcNojBv6Z3 U1DdK4MYtldL4A4ZxhD/jbkag9Zs5YB99dOtha60tKX0M562vnOycycIO2Q8D+nilGSLGDGFoSDE dHaYjcQXHy/4v9jFzOMcjNuAhTNxebhbrG14lMrntqvIE2Si8/T4fDuQGnhc3fMiE3XBR3h1Xp5Q t3c+YtKs05/a3dun8vxgZPSa1fnUGf4fXN46hi1pPzh/MFjMp+cAM/7w4MXiuQdTSIIcWV1aU/PD waaXeovLibm5ufEvgxg274s946uY6mMKlJ0U85fhOGLnw/6wvlCsRNozmjT9i2m+2dIatvGT3c99 s30iT/8kagSBEWewt772dpzqXxWvnkzGXnmB0hRLzIM5JXgLyVaySnvs8n6BrXzx+gDRQvsiTD6c QJ6gbRRwkmL6ksqs57SLEQBMReTRcamtxRHwme7vfkzVN+fOasoNHxv6Bkh35+5+hhczBo0kXgNL 1gek6WLZ1+AJy4HmKr3zjs0XTJkOr1zRVAsNDQ34ivQ/7Rv7E8mhGfdG/oTJ98Pj/03FnUp5rrvj jThnmly4u0NbiqTD2xl9ixKpbdLp815Ow4n9K+jlNcZcstbja3oeUBeNPEltbW3b96SZmZhCKJD7 ihYHIhJ4ixeesl6c3nvlJrjsyyfBZ01ovVZU1HQ3gnpf3GLHm/O1/Ku0eBkoWQHew7GELWE0cWz6 nR4CN/4t7xYN/TBAJ7QRxRk5+NlzLT4pCeefcF5AjxjvB0igtpE3QsDDLjklE6pOKgYxnW6qScuq nuVEprucg6FD2BvYhSzmdLppV9g/sK0GLOnMqXHtJW+QsMgEmThiwDFZf/489CdBRFpLyzJiJP8z N7F0LEnUPHlNeHn5f/NCHidrCBXz0TuFhW8zdI5Cv/Zetr6+DOk8PF2yNyTIJsGnWug5GnmNjDMd 6Nctv1l6e3Ti0Q8Y07NEEBMZtyx3GxMiuru7W0m159hkhhfS310lK2+7dLB7/F1q1Svm4+OjZyUP /WC30AuqzFZfiNdtjifbQeOsippaksrpOsLlu61Z5SdvjPc8HbAZinrVmb7zNLsjcavGdZ+Ude67 3xoR6CxSIsOCrdfzDNR5T7vK79Kgmtiz7zZpGw2tVRyYjsSTOndk+RxoCD6uuYItF14Mm9fqwQ2d kJ/aJP11od4i3Xdfc6nODIkEUyZlL4kahTb5x0vSa8decOJ7meqEb2Pd2nTlv7wlJCTOqGKo/7tB JeR7s/WvQMJYF18XxK7W197sgK7bmnfUuhwjmUfhVFyep/r6vuudrZrCaw485eXlbCD7h5Y5dQe8 +kO8n5y/dN+4pk8cpLPYq+L+N4Nlt3bn3ICGitrNysoaMXskCdL4W/wzL7WPITGFkapFMq+7a5eT zzafPn2Ko5f2JY+TwJ0SfoSwvA6x1j3sJpjd7Lvxs7vsjQpPfQENWmmeD5ErSw+sUeUjrEeHUPR9 uJ1vbyAMU033wR8b7qxnX10tpbHnnKuVf4JT73lYz2LQAjy2M2iKi+3isA9kG6a2nfLr63F4WFsJ L6vb8LN4PsvT00wdXposZzma3lRC/12pK4slfS+zJhuoqqJHnRR+/BtRmifwbBq/G3/jZtdCd1Vk X8xQjx5uUm45me3ddzCRdfr8UFHjOMsVF6mPg57lNVuiuFRvgUarmBsLZX2lmUznb5JBeaeE2NTc aLo/huf1eNKiWldDRQBHEEMtFzx7VJYn6D2GMRCOZXR63ph3NX1D/pkVFJzf7SxYpNx/45+egt31 Wuy966urHZCh9arRiUOmQdXH/00Edq/VwoC/G5F694x+qJC3YRUs1v18o1epX4eT9+UC5Y+tXV+d /pq9+TA6K7HKzWlZYpHDz9Hxsm/U5zI/Gx+KHEpAg6eKDSbik673fe/PvMqXvcmz7sdTrCTqusQM CZRg5L++x3ceU2R/l7pJp15PvLP9H47SPSb5CAQc7BN2iBIiMEl4AxWJeUxOVpqWuuzedRqyKtCq 6w+Dx9VNTUxN1eL0GU8XQLu43PxqHiDtPJuAO7tEV8gAe1xkNzj6piU1JbWzxR3a7EtMhJlnZ94R yaYfbLUqRIalVa0f9Y1Jm4pJgHgAdFFLzrtsLnOyV8eLWAwMDF0eCZklBXV1b/Aeb7aPa3qG/7sR /PL8GPKTiTf+OOXcskEXQsy2df+rSFrIumk5U5BsCqXh7C0qMAzaG/SsiZvNhknh4873Mpks602u h58VFWWUOL8gJEqqaqjU2bp21Dms81fqNiDSJqMQEhLSM7JI29igyH98pGRlxVrmW3ZaIq3VYWbL UHjJRKBGIPWpDQzpfYz50Nj+yflumnJucXEXLPKy8kpxPz8h0KDgSsEM3HGGeKS4zRgLQuv/BMLA bTAxsvhTv39vJgBbmv4Ie93mxRjwR4fU2/v+bLmyGcvWzi5AVU/7NqJkeNg5XDJy/6aYnpGAqPiY PzMr69f0dOBEFrfYojvoS1P3jTnuGyzvmHLuXO+JGOxo5FAP2PCl3tRDJGYbjZMsM0s63ILHZHMn 6iv1DQ1fzoL24dL2Dg4cflRUhUCgfvJBJw0w7kHUWC4eSjfECGEIjqjBvEWmm93Hf46PS2IKBwm7 /+oYs6lND9e3YNAet5NpeVG5JA+mb6//spoTleeOGFjW+fTpIX7dIeCH0euLAAqsGNv1hz+Xb74F 9SI1tbbeDTQ2zmEZsNzWlo+NLWNgYLwuLixwDyLHRiKFyxoT5J0w0yxEBnFlKIg7OJymgRXqaGVp 6f81M9Pne0k7URJ1UMkPYeLOTDbKJukT0or59tLaWqep/L6Qs1JZl6d4UgHLxsZGhVsvwQEaVXN3 eFj9KIDCxyaZjeNrMAVyukLJwtzcXE10TIz2/t5BdYnNg2/FPGzCyKqV1Zd+WAo1XWqAgZPzErqT tMPqeulyVrtKuu/QYmdqJrFrngb9ACLhN/mkFNtxlfepbFoTZCF9ibHcwcqwfSztP835B53XbkFf YGRhZeU44/7M1LHuT8/JhFZe3n94KZa6L6zOt58CL/r9ThMXIbP8iDvr5DEF9/AWzN2ar/AoLas5 YCvt0UqrQG83XFhZ+ST5yy239/JHIT0/l6LraD3JGroZQXg+vTwtQ/m8PUeTM8liZbmtgZaHR1Pv 5WNDfT2mbL2+Ik4o1KFlwLQuv6npgw4DjWqULHUj7Y/bKAVMsf8/jcWAEkhN8/9HtV6sfr8iAZQf M1tno3KjAfAlJ6UkWS1uHPw/UEsDBAoAAAAAAK51fkwAAAAAAAAAAAAAAAAEACQAc3JjLwoAIAAA AAAAAQAYAA0v0PskyNMBDS/Q+yTI0wHNAVHM74nTAVBLAwQUAAAACACgZGlMGOci3B4DAABWCQAA EQAkAHNyYy9iYWNrZ3JvdW5kLmpzCgAgAAAAAAABABgA3KQ18Jq30wENMQ/nz5nTAaGG38MxhtMB xVVNTxsxEL0j8R9GOdAQBW/VI20qUSpUJJAQ9FJVlfB6J1mXXXtrexPx9d879n7EC2mKyqE5YGHP vHnz5tmbTOBrLi1c6doIhGOdIZxoUwLt2Tr9icKB0+ByBIemtKDn4Z9zfSeLgsNFnRZS7O7ABM6k QGVxCksG79hbBqdz4CB0ddsnXZzBiltQ2kEmrTMyrR1msJIupwBpA85cFgTyTdcguAKdOi5pUQjc Qe5cdZgkZVOdabNICDShcgmDSbK7s7szqi2CxxZu9N5vJJNJy886z0RopagtqZWFuTZwTRsOlYMP Bx8hw6XTurDXFFaWtZKC+0jmEQh+yc0gfwb3j4MiQUwrjKycl9CgrShOpgWGWgYLfivVAkq0li/Q QopuhahCckeEq6wnQmC6QhsIhKBP3EoBBTWDCk1AbRn1oCS9ezMo3gjrQw1SQ54ArypabShGJytu slcS0y6fhjn/ORzKmoZgUWUhheYrlXSSF/Iu6NyVBz9yk1F/ZL6KS9MNQORGl8hMrZykVavjpnfG s+ys1WQ8r1WYz3g9qn24JwRIEtqyukBW6MV4lHJxszC6VtlBM7ND6BFH02jS+2HIAH7+nfTtVpLA Fbq6gkobB4OSXvBezzkRf6KGbwrkPE5iipcIs9kMRl3sqOUO66HPoG+xxZ8GUdE062UzeewzX9L5 gCB1/wS5k8D/2mtAPKLL8L1NYI6np9kPOhxvOX14oKvjMRtEimS9ODFsVJWUvsRSL7GROvD1dvOy wCpHBaLQFjPWJXR6sbAfqxYJA9RzgQ4HFHpej+vylp4pkUOnOOOxsdouOJUZLdB9ObocHT7dN/ir RutOyPI2xywOaARo7w2rtHXnTZWu2lop/0vpFt/EHPvl8R9M2V3XF3iyDX2hJX3x1xqyqxj50cP+ 3Y0+ijXW9Z5j8rklN4Vs8GWnz/+zZctgkyvbUa0vz94ebHNoFLrdZ5tddZRFT3rz6Qna9D35uMg6 Wp13ZKIHuoveX7+hzVn8OfOfg6DQc8zP0opt737fsldnKHekxvBg3LXeNr6xCRMGvbEPSvRL+PMb UEsDBBQAAAAIAK51fkw+ZTakSAUAAP4OAAAOACQAc3JjL2NvbnRlbnQuanMKACAAAAAAAAEAGAAN L9D7JMjTAQ0v0PskyNMBh8RfqzGG0wGtV0tvGzcQvgfIf5jsxStDpYoe5aZFHjbswo4DJTn0Jmp3 JNGhyC3JlaME/u8dvlZStH4kyMXykpxvXt/MkKNj+LgUFj7o1lQIb3SNcKbNCmjNtrMbrBw4DW6J 4NCsLOh5+LjSX4WUHN63Mymq58/gGC5FhcriENYM/mC/M7iYA4dKN5tO6P0l3HILSjuohXVGzFqH NdwKt6QDwgacuZAE8q9uoeIK9MxxQT8KgTtYOteMR6NV1M60WYwIdETqRgyOR8+fPX9WtBbBY1eu OPELo+PjgPtGK7XjzoxXnxdGt6oGWxnROOD+X6Q/QgknuBRfuRNawQqt5QtkASVEay6MdXndh0oh 1uQIQTdGN2jkBhouDHkfdHoU76RQAaNfvUHJNyD1QlRBFXmz5gYabRy8hGpp9AqZaZUT9JuAS9VK OYRvoPgKx1DQskPlCrgbkO9elDXauqtoafktLG9DcklJQAVzbcjnSq+EWmSvyElSGEytce20lkQI SiZm2+atio5pleGT6AC+0RkQcyhf7C+BD4jVEhkao01ZnE4m15NxF0kdCVdrtOrIAX4h+14U3mYv a9C1RoWPO+8FgKWgVkvIihkPFm2VcaJCsUB3/mpSjOMakL3n3EywQrHGurP5JG/PDPLPJ7sABv9r 0bozooVdYr2LNNnfegiNTL4LwQ9Z6YLGeF3HNKApu9V700Rlk1If6FpJpBppG5AJwub0JC1vu/N7 inLyyhSr72wyuNJr7DcLMiM99U5iHmpdtSviXRI8XdP/nXRB0ffKU6zyejGk+L06WB7CnEubND2O S3W+WKA5/eKNCpAfd1d+EC1u9Bk66dvZQc+FNYJzyovEp5TRNsGJjUA2+AYQZd7i+mM67/vDEKJ1 oU9Q1wyC1G1WgjgaBKYYfLYX6r3RC0MGTGHFm0AU+tKShIXrqd++eoi8kOhyMcXauqiH8XPJTWA1 ESGJJC5QCK74Z2rBrUHfEIWyjlPTIptDGKY1UkeX01zsqfljkqXsLR35JdY0Bnz8ytb6tuS4oUKm rkpt4laoWt8OWLYwInpK4m3aZNcBvUyZDwdYdoFO5n93t8mlSwr+S+8b/A3/fLh+x/wgUQsx35S0 OIDxLuu9atzX+qa1Tq8Cs3r4+dsEbUMNEIlSKapR9Tj9hoh+R1aq+IZTl4ugkWy73Il8szA96EbT rq/ew8Eg79nRcGsJwtOv5o5DmTiWZ+U8IUJqhYMwvuIsiydCXu6bD/d1yS3HnprBpH9LurzyAyk5 sOeXZOOBwt/GBsq311eAXtYO2GEWc3ztKJ4JLd9npQeL0VXEC9hWughB57qakhJmfjdWdZTfT/72 unN/Lql4Pa92+sO0J717HbcMlj/aPnTjP+0w95AgxWLcUy4Pby/7SOPvMePnuJv4wy6Bd/vNduH7 t+mtmDxHD708HFV7rj5sLRnVNwH7DfykzM+Z2DukfszKeybgvqFdNZBPv2jWPwL59DH/CNDPT/ht fi5UKDIaCLwR7MaGFwuVym758JaaDj0fKi7lhsE7HaYfj5U6rSS9Zy5IYOrfOHTTpVqlZwvVI13D /ZA+ooeWokeEbRvvH+3MNnAmDM71l2HXtq32FUZnRbSIw5/xrvAXjcsFJZtsCrMzGrZvcC7k7V2w HoKoB4ER/uFxY8l7OvwSarZEHkelv9TXjArsVKIP8+vNRV2SFPEr38/9TRcgCVZ0/3WYDpdH0byj 2FRvLBN+Fos6f1pTHT51SNmnyWVZjKSYjTof0quADGQNNwT+jp6ujC4baNxrCpPBMjkQaFtmXgyh iBjFYHDyP1BLAwQUAAAACADscyVMrFXb3OoAAABaAQAAEQAkAHNyYy9kZXZ0b29scy5odG1sCgAg AAAAAAABABgA5y0beymG0wHWcFtQJYbTAdZwW1AlhtMBPY5PS8QwEMXvhX6HZ882EU8i3V4EQVhh QS8e02RqsrSdkkx3WT+9cVv3NP/e+71p7uoanz4kfPASLeGFHeGV44i8S0t3JCsQhniCUBwTuL8O 7/wThsHgsHRDsGUBoMY+WJoS3eOk8KgeFN56GFieLzfbYY+zSZhY4EKSGLpFyOEcxGdBSBupD0PG fPECayZwJybkMhGMwIvMz1qP6weK47fOWJ0DtUJdt2VRFo2XcWj/YI0n465d7keS/I83MZHsqkX6 +qnSq0zfdE3H7vLvSDaGWZCi3VWOTsI8JHVMVdvo9bS5N0/GrMG/UEsDBBQAAAAIANN2tkyJso/d qwQAAJ4MAAAPACQAc3JjL2RldnRvb2xzLmpzCgAgAAAAAAABABgA9l84C8zx0wFDfw/nz5nTAR3m W1AlhtMBrVbbbhs3EH034H+Y6KUrw95t89AHGS5gOEmTwrlADlAURQFTu6MVG4pUSa5kJfC/d4Zc UhdLRgPUgLW7JOfM7cxwqjP4PJMO7kxna4Qb0yC8MXYOtOa6yd9Ye/AG/AzBo507MNPw8d58lUoJ +NRNlKxPT+AMbmWN2uE5LEt4Wf5YwrspCKjNYp2FPt3CSjjQxkMjnbdy0nlsYCX9jA5IF3CmUhHI H6aDWmgwEy8kPTSC8DDzfjGqqnnUXhrbVgRakbqqhLPq9OT0ZNA5BMau/eCSF6qzs4B7Y7Tecmci 6i+tNZ1uwNVWLjwIfkX6kVp6KZT8Krw0GubonGixDCghWlNpnU/rHCqN2JAjBL2wZoFWrWEhpCXv g05GYSelDhiH1VtUYg3KtLIOqsibpbCwMNbDFdQza+ZY2k57Sc8euPgGWsxxBIMGl94Y5QbwOCS3 WapcGOffRyOLbwQJXkzeNaOElURKqd2CwLD5XerGrMpw7PQkAG3iN8aWcoYW7o0e4z8dOn/LCxrt PUzW0OBUdMpvhWlumk6F+CgjOD6rGWp4hcvPrBb4d2IeeJ9iprEJkoUssYwn77OFMz9XG5zhecxV TOQH9Ctjv5A2SpuxAYSjy5lcEU1ggpuMkhEsWhulOC+6hUZ4AU5qYj+jLdGuIwS5qzWfKGhTdQ2/ 8omY/MbU3Rw1py2EYhj8DpJ3SFxVzozCF1PWEWdbyn83ocTNq+TWBbtVzYS9wAdO2AWxtm3RVtI5 wqx++rnngdHXTbMX82I3O3EZpsZmXjo2b0qpDmYTYzwvOKpIhIKW1iAsRtr1sacsPuHlMJFx2unI ZKMTqXpNQwjsklMoXuwuAat1RmGJ1hpbDF6Pxx/Ho2yiiR2mMej0Dx7wgbx4MWDPWNai76wOH4/s K4CjKqpnkBSXIli0USao9gct+rfX48EorgHZ+2tYyeZepp2JRfHlcltWPInzNs6RLBwDszg3S3wG b3zowEFI8j+EINR1TkBJ5maxvHqUGNRz+74Ri0AhNdhuAaqHcCnVvZZX+fyOokSEoo/7nk3R68Nm QWpnulPqMreYLWrtpSpq2G9YOpZ8GTO9MYhK6da0mQ6HmyD/fU8jPE9ChD6CqCKvRQKOMun2NljJ Llcz3mOMR3iE1HKzT5kgolGnuoIp9RHcD9EhFm5V4A5GjsU+srcdJrIeiW5u82+ofboZNnt029sd 7hB1y94jLN+Y/N8sTrH4PpOfsPFZq1PRvKX6UOi2rrp0/h5wyW00DQsOKO1QhBGmvwoCArdVnm34 ehmmmWOnBx/oq3u6inS3xJD0X8z6mwiUmQ9FD30OqGvDd1WOI0d4b3pQwvnX3JDzoU2jpgHk+PHc mbZac27OQCOAQo/ZUItuQahhWmHj+PLjOzO39yPHPD6EkSd+Xib0/7mcc+na3ahvarjfGMFvdx8/ lDxT6lZO1zktByqZX6oKrhUNSpoIQPPuGjqXZocYNqIFjYlzSXcEEYPS2UvFEzEYyfsy7vUnDlBg WJKYLoo/nzDgryFc/RITTKK7GSanHQ1rmZGD8/QanTkqkMBZIlMtiTwOy1rQBV3QbX9c9/PsCjDc E+n/X1BLAwQUAAAACABzWzVJBmJ0WgIVAADKQgAABwAkAExJQ0VOU0UKACAAAAAAAAEAGACA29tk 6hPSAQVAAj+i3NMBBUACP6Lc0wHNXG2P20aS/i5A/4EwcMjMQR47njjZzeI+eBMna8AOgnVywX1s kS2pY4rUssmRtb/+6qmqbnaTlF8O/nCDIJmR2NVd709VF/Om/bera1P8OmxrVxavXWkbb4v/tp13 bVM8u3u6Xv3XR3/Wq/Xq67viR7tzjetpoV+vHmc/8gQ98+iHtuk7tx36tnu0XhX0c7Sm8YU15aFw TeUeXDWYumi7orZ7+sU2vesvRX8wfVF21vTWb4oyULG+6Fuh0x+sPICTt7sNSLRnIv1D+2A7WxVv 211/Np29k9M8y08TmM5OxTTb49Y1gSx/FJeBV3zY0qedL27crjDN5bYYvK2EzvZSmCLdxjSVMHMy Xe/KoTZd+v1XPqOuZ71Pzzo95JQ/HMhcIa/0vmF6+bKM5tt26EowWtnip7Y7kpSL88GRjsA/q5l0 k/J1ML4wfU9qDKzjyaalQ2BB8fL9wW1dX7zY8Bcv39ty6M22Vvp0Zj8Q+WRjoYJvNyy1N23ldq40 UerTBfosbcbWVBqvNFxT1kPlmn1xajtZDoXZdqfyeE7yeNWQpk9EHWf6w/VE2pKdVaa7BMfwqYyw En/cmFtR6KdIJhVKZX1JzwRxyUGjnP4OkYux5Xr6Gxl23Hub7D2zgzNtfDQkF/NgXM2yHprKdvx0 b7sjpDhu/qBeT44K37Gmqx09rDavItgUxBQ4KEzt25HeSCYQJhucCVDF/S2Je2IAmfWRDxU7tQrs fW67d+Jj4LaZ6VzJfkdkX5tuT4/9QStykkJEwgh7tF3wnDPULhsdKdJ0pErYkxAyhbfkVfR5sXN0 bJIR/us3QtV5FsuVaPMXnE2EMIkwtK5qy+FIgU6f/Wt8FuLJHj+YB5gxhNK5/aGHkew70/SbYC5H 894dh2Nh3/dEkQzee1j0RqicD5a5U5Pp3dEGIQfTZXJgzg9bb/81EJX6smGdkBeqKOo6LONjeETk B3shvrcX4SnX+NdPiafMgWcKV3q7tq7bMzH5fepgbBEQuptrX8TfWT/UdJBd1x7p8cJUFecikstm NM7K1pY/xWOcIo7JocIZkF6IbbZi1edI4pof4oiNPX/kmCBtnLJ8lXaQGjLmr4YV+UNt3NE/EsdK wstUjCd5vMTjN/52kwQ/WHdL3lu7o+uZ4Q0tpM8qldCpa0vrvYRbc2JjH7zQ8hxYkx1GC4XOEYuF SBr6mOVzO9RkFxDKrqNz2EqCyK6VSKQGtxvDyBhrLmrT72jdhtIq/8fbuuZf2t3OgiKT8oasXB1E hUKxj059RNBnXfe0k99JTLOOHcH1fprO+cM5L4oPgmoAH2bxLYc0TiOWLX7+5ffiZ9vYjvwrx1qb FGxtwsMqBtIFEfj4wq/jwuIFRNKGNULn2sJ77MiQ5FLUCHchBXjRRhvyZ63pL/AOODI17xlumgXw U2fpbIiNUJfoNHO/SB/w5H/a4VFxQw/it+7RbW7nE6hocrBo39uudDCWEJ5CnnI+s7A7HD1d6xDO ZW/xGytelaBQIcAItK3hFT78UXP4Y5G6sCXlmiOJWp+QBEPkeWN1uqGjIG1V5sgGEUfTUXThI+Vc wIalsH623aaoXGdLPRF2beQDTgalIRTKD8uHHN8g98bsLbJNhFDC3CRDkOvx3qbkZMBJ8exgPvQX 4h1ha7KWgzuBzrHtLOdmIbJzO5LWibSAfW6eP/2P22AKFIB8T/EFyvEHinXscVuyVzIDF0w2I54c ki2EXC9UKj8jdniOV+Sr1XL1Mf8RKhRdZT3+fAnImME2Cs0khL3sQDoTDFFXj88OcaVrL6buL493 nUV2bdrmsX1PJuPdgw0ewzkMOhNjcJRWyErKHmZLwZYERGJSE71JAI6GWAlZlT2a7t1tarhp1M0D bovagw5niXw1lPQrOVqCAFXL7HYXGJA/1YZ+oaPsGD3TJ1paSRqIiievOtUtIdN5dITQNyHetcgS xLB4NlzCeEdfs+lnEGBTBB8goIp6RZJbAuH+JpCDZLgNMszSYbSOiQzA80YkgWShmUJtM80VdpIk co5jvhiTxQLnmjGuJotnKDVf0gHIB2nDH4kBfPzbIZqJFysjWQE3qK+SfYrMyEVO9Bn4EtAw7r1e bSkFEYazkfyOoXsqEnb8RiIBwGtWwTKI7TzFkFHxfiLTkZF7eF4ADx5U2YGKtyXZ8oeY4sAWOStM J8domzraf3ia1bxeZSCy+KWNgI5cR1cgdIe9zo4gKTDG8VTD6BgGMqCIfGkAlCUIPyO6GyuAMVEk CPaXtocmYtxKVMTIb0tUNoT+g9/qqZAFAlfoA6xXidhjaNhpBi6RSRkwmVn52NkjbaFczQrCEP+D o+w4FQjWOjKO5VyAQ3xf3LhbBDNpRDDu1rDjuoqd8PKVT2JEWm9PN5ZU4NztUp/kiq8kFZYPMr+h uGlPfRoFJv4klNSpbgPyvimXg0LEmZD6TMNOXMFQcdOUNlbAMyB4J/Y8GgKVaVZqPEGsEJ5aotKM wdoj7nQPqPLlz/UKltfuW6mMUzeGDEcBHM0FZtxYAHEgS2Tyloz6wqKDX8QGQkfVGaV2UXESOe7v vrlVhyUk9TaWcbGNge/Io9IjIGL61Mc076F3obUVTs6WUx5abI/G2xg0llsQoh6T1JKxzaBwZ70K 4r3x1kYeqGB8dgv7mncslhoLaL2tV5TEKDjFIJKvGqVzH6Tz/K74JyVKS5YgIW0RCHThEZ/2Wsbv t5YizgNJj00+N3c2OA5Vbu8g1tChpNJM8gb7tx92QD8Qj1gU6VkL+7TIps/m9coHK+9n6LX8ZFxX /O7tzKC1ZQFYQvKqQJ9rw9S0AXw4UWpcprKQwhsDEAr6F+lCVG1JJ2oExO6wHede/q2yRqs1RZHr Fez2gVIwiVRP+d1dht/WK9WWJ3VRdXN/9wz/upcETfbNgi3jigAup6lnvcoTKu92D637Ey1zW1cz 5l9EjPIsGttZBtmFyh5VD555QZmnmjyyFHWWOpWhNCeBr1d5g5NtDdKXrnchyEaasPxx7IETxht8 j7ARc2di93kixUp+moIkCjSqDNzJ5cYtB4UpV2N3dtG9keDwWRPMb7JvdG5R3KE94xnKdYQQ2y0a IeTMMKMrB6VgSAZKRkdOfTwJBKpRqLJbQuSlOkhk46tJTM4ayrFZeA9MNtXrpCWJ515xyEtA8aJm p81s2reJ+V2x1GQV64BbqFs7bdLmtpK1wf3YNc4j/tci4Q+oNyS6pe77hxVzvTEPpZN1esoYoCU1 Kq9FZ5FyFlWbDQpVRjXlAcgeOAnlYjiNaRQ+eM4wmSdpTzNykdUDwUIS5fBJp8wt5IJZI2CjDc+I 3HpdRh5JBQACMy/eoGx7cBws1V2yFklsak0PEbFDYsoSawH/YNOjdj7JmHPeJmH/Hkh9at5ZZYWn ggA1wMB+Ellmj4eNggATHEDYJpcJl8qw7AS3KF8JXpk4PAQngWvWQC9e6Q1Echzn2T4zxBm7qXk7 Xzv/aZxqGyut384uXPJs9I6OI+A84EGJn3JXtJnAGwEnklD7tJyp5yY83Xe9WjBiccvpvtzz9e0Y zaM1cQSYCvKI4l+epFzdnqTttBs6hujL8C6R8MKxYtszUS47V3raKBY6rlrsN6jygGt9apuQtlQ/ 0VF4OwBKKsdKG/B0cMAm0LgZs+sIVPTbTajU4t/EKbe5cY1LFIk5IIiLAve05t3Rn4aBw+U2tPNJ VbC1a14aBD8voBT4R78By8LkAk+MuEFH73bUnxi3wa2qS/Guac8N4S5pMZFzlOXQGUKWXqVMkPeF ILjgNi9GzP8bdJhFhkPbog/SSgNFPaMNkdwUO8ueuxkFRpo5cS8F2IuqoQZ9U5ahCo3yS+32AeS0 mTNm6WpubnfFP9ozQW3aLwb/FsbOfQRih8MSJLC1B1Pv5LwwIWmV4LOF+itBRdwsAwDe+rYmsyey ZW2N3mQwSmNDnrNbRG4319hF9GA5chQSKZua+B9ztzYdzb6zLHYlursUYDuvGrVxsF6Ne5HJD52Q n/fFJqXcZ2hMfBvn4+OP4Ex75Ekw45JwyZOYxWuOVKDDBeyrTa716s+hc75yZWw+UXh41cQDoXjl 5PLjwHJ6S1QHAcn/tPuh1mruY33gK6j/FRoQ0BbagnJ/ytLW0J3mteTmchn5znp4Hv06jmX1VVxd DVJee2FrU/xJgQzdcVpYwfwZ/wY+GW9GG/6eLwlmPYPFeuAjl8YMt9DsJ60q7NQya1Sj5srQviIM abgXicYDGaCs47TCp4MbFqfalAJgCSjSnuHOlI2pUsFOqio/EdZCEkrLh5caWIW/wFfX8nCFOogI lyP8KMyNOM703IDpY5WOlG0p8Nd6mWXWqzTNQksUe8lf/Tv0JXtG+QwGe52Z4F4iWRnbNgVlhF7F MkvDU89Rh/42dgKyZmmuUu6E9kqOdiXfPxLdEkCDPFwKGqrM6yumDIzCxpJEW10lzeb1ilc5mTqA 6fWfdDC5mU8Gk7J+KFf0nXUN1EIEYMSMK72CpA3RrK0XixuanhiQS+eUCO4oSEVQEB7bOcFXURxe UoQ0tzbBvOXOggyh2bfAC3pj4RYuFyA4TlvIyRSWJSgKWsQVUBBNKfcz8PVZgXTqnBTz3z4tKnMh jnbI+KHTwvKl5aZ8h9aMKolp3hVvKE22IQMGRj5TtHJDkzG7xCsY6nGF4xMui09kUtAvYiiLhu8Y ZNwkNpTIY6xDV1vbmbTBhLhWDqMNESFwOj3pmD/1OiQa6Cjr+1TWnHs7OcGpH1nDOdQjn3HdAZoy GNMj7PUhk5s9hjj69G4YaRv39T33cxRgIqWOjXiZpOB2ryLTytJHnSEeLojxe37KSOtrA0MgQ7fd 4zCBATbLjqKzfnKLMGn3MhI0uznQfjlSMi6A60t6R1xfYp88HRy57tHIRyziS5gF4hidbOhj9Xul D6ztCr57naUifzBp4FI13CPtSwx/0ODaj6Eyp+uL5zK6RtoLlzJEc72yiBgYpQiAmvGV1KGpMkK6 ASvgD21fkm9Hcua+23rFhru1JKEHU7uqvqQXPOwlnThmRmseDFEzq2Gm7IgM/NA94Cov+Yal8Z9f 6IcoFV/kRyh9y60GxXxQ0B8B8n0upaug7LMpfYkfobTUAoj9joUsJ8H1kUHse5RRSu7BMYUVgbEW JO/IM+MlOuWxDtM/1SbcaDLmU0qCXChmJC3kzeJ4l26CGB4bAh/jDgMN3ImzwHEeM2IdFXwNd7M2 lFNldMvkckrmjXWiBQdGQNcwQ/8IhEG8REJy/h1qEoVoI6V/UenqtGDQsYRQ6F9rQo9jNdmZ3h54 9CwZtRtXQYdWeYSz6XCbAnUpMEdKN9y6y8vGW8T74Wiz9qVMAYbLO7kEZO0ESp09GScIvmw7HcyB YDD5k7lRNJCSQhulGL6ZJ9sKlGAfDY9shlvT2Z05JoBivTsXW6BEGPHQdu7fyxatPYrkm/GcUeBf 2O++WKT7fxs1v0tHKaCk17EY/kxK12vZz6X0JX6E0u9sLA1hV9eVw1FadQG661cydkfOw4EszJ2R Y/UppaSN19h97fa4tb/dxOG08V5RptMkhcLip2fKAKP0TtB7Oh/aIp19mTmK8VNK4z2zooyt5V6G 1HdcX+lQR5jHGyfzckrc95Ah85IyCsXYWkNDo/fl8O/KHM3extEBdN+IcxJWQukjiSCQ2PEcguda mCI5RXf6iylHSvu2rVBEbqRx7vv2dKK1G0bVA7ZFETR00rYz9W5oStlEmY6UAkaUeQ+MQmIuM2WI D8PNcsC7WIXwHEomJ9bqiL7kegu1zE7nIb3e5o43VrqJxtZAqc6cLulA8Q4c5k8oifl+JnwJqVXW YHSlS63Ac9vUNX8O3UVbazyOHIsUHahJTDfNdzGSS3siuVqvzTl2K3Qcazz4Hdnm0Rb5j1BK22Z4 r0Df1Kjlfq/QAcUwCJVKYkppNMir9qj3DCTckS5UPqGU7BM6+lHEac7+eFT5Ej9fOLv8BWE8lIWT zg0PB3AbP9aNnUWXCeVamydZnfvZdu2AewpuaMt1AleBXa+TL6mCETN1kA44pqlQ8B6pMJU3DGA5 VGWQHk/oy3LTDUS2GKMPjRQ1rXhAcQM6SXpfRtao7TeMP6dHGIMND3cHA3fyRsiOzLl/3O4eqzlL M8fLRN0BYpiOBsr2hH252DPRf5LG77bjAf+0DBYLTUtlrpf+ele8IaRCJZxpbDssTHnMJmKyWR/t I9g+KRjBVkmCCW/h+GH7J49gH3GxK1PD7Y57B1xUB6Zn5S79erB1Jb1B3O9ZhLTSCsiW4BEXR62Q lDXuNeJAY3dzvcqGxsJNRkL2rmBrJF3k7WOZKdGiBqNHoVIgue0H8nQxvTgOHk/DwLQbkAe1H8K2 2GmPZQypGE3xclEV1uTSiP2U+cuKT+9CIyO2gGMR/ZE+Pq39mnCwPUcK+PSNvvT6E1lMFS9mkvEh SnmWMn8Vu8gmKfnGqQumf89zn3yHNY5vL9DiMa/stS0ZweZRxhMGD/yBXx/KX8HIWtoYTAvTc2He de+QN41M4pFRDkQHthmea4bj1kZJxkFkUJ/KZWGe4gO9nMmdgg07rle5kvTxcW4pjMJh8jd0ABcv R7kxvzT+11yW5glViOMkUh2sJKhThXB/p2Po9GjKP7nsZRy3itOpPPCZDTmNSpGOHFadeVSvRVyS YQpWZjoVwi7t483vZT58gWqNNBkH5yeTkmPokINStDVHmxmbNK1wW75eSTWrQTmW2bwkn5aLJhrG UKW7bTXc87njmcIamYvxYc56PN1tEPM36QQKhqYX34ejhR8Ypoi3aD4ZxhovpRNL/XTqPFs4mdW4 NiOUqyB5CVb7/fl7vPnru1OthVuk8Cowyym+FV08nrMQVspYxGfdZsqLiZzaZmSdD1krGEXmx2+y /ycAyESuH+7wlpokt2Rsj9b8+prfNoavjEqpxgY+yOCST/oqyZQZIicp6tD3p++fPDnK3ndtt39C NJ/Qbk/ushtZ7BDvZBmVe9eFyouqk6SRL/eL6Z2InGC8LU1usKfL6lYnJG7Y/vnu/PWrH17+8val 0JHHCM/ZB7i+lHdUyd4qKjPJ4E18A7J276zWFm37jgiFuGDSy4c4CFJV6US2zHL04zgICX+cbIlv TGV29Xeyq097s/3/YmWfZXOfdgzMOIIMvwQ3xvI3i/+jimiR69X/AlBLAwQUAAAACACjcLZMeZt+ dHMDAADyBgAAFAAAAE1FVEEtSU5GL21hbmlmZXN0Lm1mnZTHjqNKGIX3foret6bJwS3NggwmuNsE AzvAmBxcBSY8/e2+wdIMd+PZlFSqU6Xz6Zy/zKgtrikcfngpgEXXvr9gb+huZ0VN+v7S/Hv4VsKu 3YlF9i3k6qwDxZA38P3FFKkXW+Ww7wWn6N3X/sc/svcXgivptq9OVGJJY5EwpkNKcvbz5+77wkPF ZK98dNH6WYp5jUxVaoKvJ0aS6dv0t/Lr1Yf2aGAqVWHDrdkfALFgclWkFUk3ay8oCLd6F9XgqHQw JsL8+R/CSeJEU3prLs/az/LOO7UQ8aPcDApSdJeDs06/22eLW79Qnn4+f8hXvbtm7KiF05lLSXdj P+RxfJCqlcP6OTQyrTwKvaird/Ozj4BN7Eck55tPNLAL6WG/LmIkj0DUF18ZPIvA5VNfuQf06Bc5 gO7+sMQG+fk7Qs8hRrc/aobJaZEyVrL9Gttp151scoPQ6EjWzEEZ0+MZvwRnw+wWGUcC3DYntJ+z SS5ZRa2gRLgPBJBCpEi69q1vs2cJrKxk6qJGeiZR0TW5H+qZMDchkA7jqTKGzVHeygbrQyUhl/YI VMXcEAixYs4phXwsrH6e1lXra8IhbrQXaXoEiQZMwMsj1oPj54MAggSJo6TKQDe2lz8IAvYigV9N sdSvVjbTe8/qyW2X6hWw3TLR4ocbxnNl0cUMh8lZ1ZzbYLjMnUWAHiYJBMoM2/0RnPgANRlfxAol 8nGsVEkjdAUH/QXjK4chbYc/YAiWnuYi9LZyLrxYRXYLLeGyKVPoZ8y5cWWEJb3cvXy4yGEMBSyd 8u04H16N1gvANDgKvbRjXKY8nljGB48foCki2S3uadb19GNJ/sJwSe9D19XwLR+a+lkMv7rzn63d KISAT6a1Okl+t7lNo85xNyI2HgdGkMScnRwE3tFPknDoNhi8pIjsNZPbk6ZMmMFcYVF48YVDZdgF QFz0/g6YOjvdYPL/GM9nMTC84/dXZq0bMVCABNH9K7X5WoE1mrjHHCSkVgWzGnRLwU3JHxAy2EDU +2kazf7Sg71/YDn1WABmiAhZbU536jr3LdWWRCTbzJI9IAxNkCxbeta8F+laXjASekWz1zBAHU+K 1O1MAwzgqEKaxH1uJ6sM0SboHCVmPG1bpM5J5ZU7CpWl5HcVXkmA87Km9sbJ7xBSgqA9hh7F+t7X IO3+AlBLAwQUAAAACACjcLZMkp6TQ5kAAAC+AAAAEwAAAE1FVEEtSU5GL21vemlsbGEuc2Zly7sO gjAAQNGdr2BvMJVYRBIGHsGqdEBIwbFowWooCW01+PUaV7Y73FOKXjJtJu5QPikxysBer6BFUuSk oudKO4RJ0f0isMFjxq2K8cxc9YoBIPVwLPowtEocrZd71laKeU+fNLfDHupNBlxOt0Zy6v+Ji7wl onlMVZXUZQXxuDPJ8Ead4nq8nxm4YgQieDmZQuTNR4WW9QVQSwECFAMUAAAACACjcLZMIDcP9yIN AAB4EAAAFAAAAAAAAAAAAAAAgAEAAAAATUVUQS1JTkYvbW96aWxsYS5yc2FQSwECHwAUAAAACAC6 frZMEnNugnIBAAAWAwAADQAkAAAAAAAAACAAAABUDQAAbWFuaWZlc3QuanNvbgoAIAAAAAAAAQAY ACZaLVLU8dMBoAfLvveq0wHFOVBMJYbTAVBLAQIfABQAAAAIAJuLsExtTJjleQYAAAUPAAAJACQA AAAAAAAAIAAAABUPAABSRUFETUUubWQKACAAAAAAAAEAGACl4OGZKu3TARTMj1rEr9MBsQ8kTiWG 0wFQSwECHwAKAAAAAADRVFVMAAAAAAAAAAAAAAAABAAkAAAAAAAAABAAAADZFQAAbGliLwoAIAAA AAAAAQAYAGe5yr73qtMBZ7nKvveq0wGOMa5D8onTAVBLAQIfABQAAAAIAAR8aUwEnPrIHAUAAAcO AAANACQAAAAAAAAAIAAAAB8WAABsaWIvaGFyYXBpLmpzCgAgAAAAAAABABgAOrnjZ7O30wFnucq+ 96rTAXJf4kjyidMBUEsBAh8ACgAAAAAAZFAqTAAAAAAAAAAAAAAAAAQAJAAAAAAAAAAQAAAAihsA AHJlcy8KACAAAAAAAAEAGAB/mdzV8YnTAX+Z3NXxidMBWJkx0vGJ0wFQSwECHwAUAAAACADDTYRK mHd+NMUwAAAFMQAADAAkAAAAAAAAACAAAADQGwAAcmVzL2ljb24ucG5nCgAgAAAAAAABABgAPHYZ hRet0gE36zhNJYbTATfrOE0lhtMBUEsBAh8ACgAAAAAArnV+TAAAAAAAAAAAAAAAAAQAJAAAAAAA AAAQAAAA40wAAHNyYy8KACAAAAAAAAEAGAANL9D7JMjTAQ0v0PskyNMBzQFRzO+J0wFQSwECHwAU AAAACACgZGlMGOci3B4DAABWCQAAEQAkAAAAAAAAACAAAAApTQAAc3JjL2JhY2tncm91bmQuanMK ACAAAAAAAAEAGADcpDXwmrfTAQ0xD+fPmdMBoYbfwzGG0wFQSwECHwAUAAAACACudX5MPmU2pEgF AAD+DgAADgAkAAAAAAAAACAAAACaUAAAc3JjL2NvbnRlbnQuanMKACAAAAAAAAEAGAANL9D7JMjT AQ0v0PskyNMBh8RfqzGG0wFQSwECHwAUAAAACADscyVMrFXb3OoAAABaAQAAEQAkAAAAAAAAACAA AAAyVgAAc3JjL2RldnRvb2xzLmh0bWwKACAAAAAAAAEAGADnLRt7KYbTAdZwW1AlhtMB1nBbUCWG 0wFQSwECHwAUAAAACADTdrZMibKP3asEAACeDAAADwAkAAAAAAAAACAAAABvVwAAc3JjL2RldnRv b2xzLmpzCgAgAAAAAAABABgA9l84C8zx0wFDfw/nz5nTAR3mW1AlhtMBUEsBAh8AFAAAAAgAc1s1 SQZidFoCFQAAykIAAAcAJAAAAAAAAAAgAAAAa1wAAExJQ0VOU0UKACAAAAAAAAEAGACA29tk6hPS AQVAAj+i3NMBBUACP6Lc0wFQSwECFAMUAAAACACjcLZMeZt+dHMDAADyBgAAFAAAAAAAAAAAAAAA gAG2cQAATUVUQS1JTkYvbWFuaWZlc3QubWZQSwECFAMUAAAACACjcLZMkp6TQ5kAAAC+AAAAEwAA AAAAAAAAAAAAgAFbdQAATUVUQS1JTkYvbW96aWxsYS5zZlBLBQYAAAAADwAPAB4FAAAldgAAAAA= _BASE64_ } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Extension::HarExportTrigger - Contains the HAR Export Trigger extension =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new(har => 1, debug => 1); $firefox->go("https://fastapi.metacpan.org/v1/download_url/Firefox::Marionette"); my $har = Archive::Har->new(); $har->hashref($firefox->har()); say $har->creator()->name(); =head1 DESCRIPTION This module contains the L extension. This module should not be used directly. It is required when the 'har' parameter is supplied to the L method in L. =head1 SUBROUTINES/METHODS =head2 as_string Returns a base64 encoded copy of the L extension. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Extension::HarExportTrigger requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 ACKNOWLEDGEMENTS Thanks to L for creating the L extension for Firefox, which this module contains. =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. This module also contains software written by L that is licensed under the L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/UpdateStatus.pm0000644000175000017500000003025414175143706023044 0ustar davedavepackage Firefox::Marionette::UpdateStatus; use strict; use warnings; use URI(); our $VERSION = '1.22'; sub _NUMBER_OF_MILLISECONDS_IN_A_SECOND { return 1000 } sub new { my ( $class, %parameters ) = @_; my $self = bless \%parameters, $class; return $self; } sub _convert_time_to_seconds { my ( $self, $milliseconds ) = @_; if ( defined $milliseconds ) { my $seconds = $milliseconds / _NUMBER_OF_MILLISECONDS_IN_A_SECOND(); return int $seconds; } else { return; } } sub _resolve_to_boolean { my ( $self, $key ) = @_; if ( defined $self->{$key} ) { return $self->{$key} ? 1 : 0; } else { return; } } sub successful { my ($self) = @_; return ( ( defined $self->{update_status_code} ) && ( $self->{update_status_code} eq 'SUCCESSFUL_UPDATE' ) ); } sub update_status_code { my ($self) = @_; return $self->{update_status_code}; } sub type { my ($self) = @_; return $self->{type}; } sub service_url { my ($self) = @_; return URI->new( $self->{service_url} ); } sub details_url { my ($self) = @_; return URI->new( $self->{details_url} ); } sub selected_patch { my ($self) = @_; return $self->{selected_patch}; } sub build_id { my ($self) = @_; return $self->{build_id}; } sub channel { my ($self) = @_; return $self->{channel}; } sub unsupported { my ($self) = @_; return $self->_resolve_to_boolean('unsupported'); } sub status_text { my ($self) = @_; return $self->{status_text}; } sub elevation_failure { my ($self) = @_; return $self->_resolve_to_boolean('elevation_failure'); } sub display_version { my ($self) = @_; return $self->{display_version}; } sub update_state { my ($self) = @_; return $self->{update_state}; } sub name { my ($self) = @_; return $self->{name}; } sub app_version { my ($self) = @_; return $self->{app_version}; } sub error_code { my ($self) = @_; return $self->{error_code}; } sub install_date { my ($self) = @_; return $self->_convert_time_to_seconds( $self->{install_date} ); } sub patch_count { my ($self) = @_; return $self->{patch_count}; } sub number_of_updates { my ($self) = @_; return $self->{number_of_updates}; } sub is_complete_update { my ($self) = @_; return $self->_resolve_to_boolean('is_complete_update'); } sub prompt_wait_time { my ($self) = @_; return $self->{prompt_wait_time}; } sub previous_app_version { my ($self) = @_; return $self->{previous_app_version}; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::UpdateStatus - Represents the resulting status of an Firefox update =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new(); my $status = $firefox->update(); while($status->successful()) { $status = $firefox->update(); } say "Firefox has been upgraded to " . $status->display_version(); =head1 DESCRIPTION This module handles the implementation of the status of a Firefox update using the Marionette protocol =head1 SUBROUTINES/METHODS =head2 new accepts a hash as a parameter. Allowed keys are below; =over 4 =item * app_version - application version of this update. =item * build_id - L of this update. Used to determine a particular build, down to the hour, minute and second of its creation. This allows the system to differentiate between several nightly builds with the same |version|. =item * channel - L used to retrieve this update from the Update Service. =item * details_url - L offering details about the content of this update. This page is intended to summarise the differences between this update and the previous, which also links to the release notes. =item * display_version - string to display in the user interface for the version. If you want a real version number use app_version. =item * elevation_failure - has an elevation failure has been encountered for this update. =item * error_code - L that conveys additional information about the state of a failed update. If the update is not in the "failed" state the value is zero. =item * install_date - when the update was installed. =item * is_complete_update - is the update a complete replacement of the user's existing installation or a patch representing the difference between the new version and the previous version. =item * name - name of the update, which should look like " " =item * number_of_updates - the number of updates available. =item * patch_count - number of patches supplied by this update. =item * previous_app_version - application version prior to the application being updated. =item * prompt_wait_time - allows overriding the default amount of time in seconds before prompting the user to apply an update. If not specified, the value of L will be used. =item * selected_patch - currently selected patch for this update. =item * service_url - the Update Service that supplied this update. =item * status_text - message associated with this update, if any. =item * type - either 'minor', 'partial', which means a binary difference between two application versions or 'complete' which is a complete patch containing all of the replacement files to update to the new version =item * unsupported - is the update no longer supported on this system. =item * update_state - state of the selected patch; =over 4 =item + downloading - the update is being downloaded. =item + pending - the update is ready to be applied. =item + pending-service - the update is ready to be applied with the service. =item + pending-elevate - the update is ready to be applied but requires elevation. =item + applying - the update is being applied. =item + applied - the update is ready to be switched to. =item + applied-os - the update is OS update and to be installed. =item + applied-service - the update is ready to be switched to with the service. =item + succeeded - the update was successfully applied. =item + download-failed - the update failed to be downloaded. =item + failed - the update failed to be applied. =back =item * update_status_code - a code describing the state of the patch. =back This method returns a new L object. =head2 app_version returns application version of this update. =head2 build_id returns the L of this update. Used to determine a particular build, down to the hour, minute and second of its creation. This allows the system to differentiate between several nightly builds with the same |version|. =head2 channel returns the L used to retrieve this update from the Update Service. =head2 details_url returns a L offering details about the content of this update. This page is intended to summarise the differences between this update and the previous, which also links to the release notes. =head2 display_version returns a string to display in the user interface for the version. If you want a real version number use app_version. =head2 elevation_failure returns a boolean to indicate if an elevation failure has been encountered for this update. =head2 error_code returns a L that conveys additional information about the state of a failed update. If the update is not in the "failed" state the value is zero. =head2 install_date returns when the update was installed. =head2 is_complete_update returns a boolean to indicate if the update is a complete replacement of the user's existing installation or a patch representing the difference between the new version and the previous version. =head2 name returns name of the update, which should look like " " =head2 number_of_updates returns the number of updates available (seems to always be 1). =head2 patch_count returns the number of patches supplied by this update. =head2 previous_app_version returns application version prior to the application being updated. =head2 prompt_wait_time returns the amount of time in seconds before prompting the user to apply an update. If not specified, the value of L will be used. =head2 selected_patch returns the currently selected patch for this update. =head2 service_url returns a L for the Update Service that supplied this update. =head2 status_text returns the message associated with this update, if any. =head2 successful returns a boolean to indicate if an update has been successfully applied. =head2 type returns either 'minor', 'partial', which means a binary difference between two application versions or 'complete' which is a complete patch containing all of the replacement files to update to the new version =head2 unsupported returns a boolean to show if the update is supported on this system. =head2 update_state returns the state of the selected patch; =over 4 =item + downloading - the update is being downloaded. =item + pending - the update is ready to be applied. =item + pending-service - the update is ready to be applied with the service. =item + pending-elevate - the update is ready to be applied but requires elevation. =item + applying - the update is being applied. =item + applied - the update is ready to be switched to. =item + applied-os - the update is OS update and to be installed. =item + applied-service - the update is ready to be switched to with the service. =item + succeeded - the update was successfully applied. =item + download-failed - the update failed to be downloaded. =item + failed - the update failed to be applied. =back The most usual state is "pending" =head2 update_status_code returns a code describing the state of the patch. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::UpdateStatus requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/Link.pm0000644000175000017500000001146414175143706021315 0ustar davedavepackage Firefox::Marionette::Link; use strict; use warnings; use URI::URL(); use base qw(Firefox::Marionette::Element); our $VERSION = '1.22'; sub new { my ( $class, $element ) = @_; my $self = $element; bless $self, $class; return $self; } sub url { my ($self) = @_; my %attributes = $self->attrs(); return $attributes{href}; } sub text { my ($self) = @_; my $text = $self->browser() ->script( 'return arguments[0].innerText;', args => [$self] ); if ( defined $text ) { $text =~ s/^\s*//smx; $text =~ s/\s*$//smx; } return $text; } sub name { my ($self) = @_; my %attributes = $self->attrs(); return $attributes{name}; } sub tag { my ($self) = @_; return $self->tag_name(); } sub base { my ($self) = @_; return $self->browser()->uri(); } sub attrs { my ($self) = @_; return %{ $self->browser()->script( 'let namedNodeMap = arguments[0].attributes; let attributes = {}; for(let i = 0; i < namedNodeMap.length; i++) { var attr = namedNodeMap.item(i); if (attr.specified) { attributes[attr.name] = attr.value } }; return attributes;', args => [$self] ) }; } sub URI { my ($self) = @_; my %attributes = $self->attrs(); return URI::URL->new_abs( $attributes{href}, $self->base() ); } sub url_abs { my ($self) = @_; return $self->URI()->abs(); } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Link - Represents a link from the links method =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('http://metacpan.org');; foreach my $link ($firefox->links()) { if ($link->type() eq 'a') { say "Link to " . $link->URI(); } elsif ($line->type() eq 'meta') { say "Meta name is " . $link->name(); } } =head1 DESCRIPTION This module is a super class of L designed to be compatible with L. =head1 SUBROUTINES/METHODS =head2 attrs returns the attributes for the link as a hash. =head2 base returns the base url to which all links are relative. =head2 name returns the name attribute, if any. =head2 new accepts an L as a parameter and returns a L object =head2 tag returns the tag (one of: "a", "area", "frame", "iframe" or "meta"). =head2 text returns the text of the link, specifically the L. =head2 url returns the URL of the link. =head2 URI returns the URL as a URI::URL object. =head2 url_abs returns the URL as an absolute URL string. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Link requires no configuration files or environment variables. =head1 DEPENDENCIES Firefox::Marionette::Link requires the following non-core Perl modules =over =item * L =back =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette/ShadowRoot.pm0000644000175000017500000000671714175143706022516 0ustar davedavepackage Firefox::Marionette::ShadowRoot; use strict; use warnings; our $VERSION = '1.22'; sub IDENTIFIER { return 'shadow-6066-11e4-a52e-4f735466cecf' } sub new { my ( $class, $browser, %parameters ) = @_; my $shadow_root = bless { browser => $browser, %parameters }, $class; return $shadow_root; } sub TO_JSON { my ($self) = @_; my $json = { IDENTIFIER() => $self->uuid() }; return $json; } sub uuid { my ($self) = @_; return $self->{ IDENTIFIER() }; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::ShadowRoot - Represents a Firefox shadow root retrieved using the Marionette protocol =head1 VERSION Version 1.22 =head1 SYNOPSIS use Firefox::Marionette(); use Cwd(); my $firefox = Firefox::Marionette->new()->go('file://' . Cwd::cwd() . '/t/data/elements.html'); $firefox->find_class('add')->click(); my $shadow_root = $firefox->find_tag('custom-square')->shadow_root(); foreach my $element (@{$firefox->script('return arguments[0].children', args => [ $shadow_root ])}) { warn $element->tag_name(); } =head1 DESCRIPTION This module handles the implementation of a Firefox Shadow Root using the Marionette protocol =head1 SUBROUTINES/METHODS =head2 IDENTIFIER returns the L =head2 new returns a new L. =head2 uuid returns the browser generated UUID connected with this L. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::ShadowRoot requires no configuration files or environment variables. =head1 DEPENDENCIES None. =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/lib/Firefox/Marionette.pm0000644000175000017500000140263014175143706020420 0ustar davedavepackage Firefox::Marionette; use warnings; use strict; use Firefox::Marionette::Response(); use Firefox::Marionette::Element(); use Firefox::Marionette::Cookie(); use Firefox::Marionette::Window::Rect(); use Firefox::Marionette::Element::Rect(); use Firefox::Marionette::Timeouts(); use Firefox::Marionette::Image(); use Firefox::Marionette::Link(); use Firefox::Marionette::Login(); use Firefox::Marionette::Capabilities(); use Firefox::Marionette::Certificate(); use Firefox::Marionette::Profile(); use Firefox::Marionette::Proxy(); use Firefox::Marionette::Exception(); use Firefox::Marionette::Exception::Response(); use Firefox::Marionette::UpdateStatus(); use Firefox::Marionette::ShadowRoot(); use Waterfox::Marionette::Profile(); use Compress::Zlib(); use Config::INI::Reader(); use Archive::Zip(); use Symbol(); use JSON(); use IPC::Open3(); use Socket(); use English qw( -no_match_vars ); use POSIX(); use Scalar::Util(); use File::Find(); use File::Path(); use File::Spec(); use URI(); use URI::Escape(); use Time::HiRes(); use File::Temp(); use File::stat(); use File::Spec::Unix(); use File::Spec::Win32(); use FileHandle(); use MIME::Base64(); use DirHandle(); use XML::Parser(); use Text::CSV_XS(); use Carp(); use Config; use base qw(Exporter); BEGIN { if ( $OSNAME eq 'MSWin32' ) { require Win32; require Win32::Process; require Win32API::Registry; } } our @EXPORT_OK = qw(BY_XPATH BY_ID BY_NAME BY_TAG BY_CLASS BY_SELECTOR BY_LINK BY_PARTIAL); our %EXPORT_TAGS = ( all => \@EXPORT_OK ); our $VERSION = '1.22'; sub _ANYPROCESS { return -1 } sub _COMMAND { return 0 } sub _DEFAULT_HOST { return 'localhost' } sub _DEFAULT_PORT { return 2828 } sub _MARIONETTE_PROTOCOL_VERSION_3 { return 3 } sub _WIN32_ERROR_SHARING_VIOLATION { return 0x20 } sub _NUMBER_OF_MCOOKIE_BYTES { return 16 } sub _MAX_DISPLAY_LENGTH { return 10 } sub _NUMBER_OF_TERM_ATTEMPTS { return 4 } sub _MAX_VERSION_FOR_ANCIENT_CMDS { return 31 } sub _MAX_VERSION_FOR_NEW_CMDS { return 61 } sub _MIN_VERSION_FOR_NEW_SENDKEYS { return 55 } sub _MIN_VERSION_FOR_HEADLESS { return 55 } sub _MIN_VERSION_FOR_WD_HEADLESS { return 56 } sub _MIN_VERSION_FOR_SAFE_MODE { return 55 } sub _MIN_VERSION_FOR_MODERN_EXIT { return 40 } sub _MIN_VERSION_FOR_AUTO_LISTEN { return 55 } sub _MIN_VERSION_FOR_HOSTPORT_PROXY { return 57 } sub _MIN_VERSION_FOR_XVFB { return 12 } sub _MIN_VERSION_FOR_WEBDRIVER_IDS { return 63 } sub _MIN_VERSION_FOR_LINUX_SANDBOX { return 90 } sub _DEFAULT_SOCKS_VERSION { return 5 } sub _MILLISECONDS_IN_ONE_SECOND { return 1_000 } sub _DEFAULT_PAGE_LOAD_TIMEOUT { return 300_000 } sub _DEFAULT_SCRIPT_TIMEOUT { return 30_000 } sub _DEFAULT_IMPLICIT_TIMEOUT { return 0 } sub _WIN32_CONNECTION_REFUSED { return 10_061 } sub _OLD_PROTOCOL_NAME_INDEX { return 2 } sub _OLD_PROTOCOL_PARAMETERS_INDEX { return 3 } sub _OLD_INITIAL_PACKET_SIZE { return 66 } sub _READ_LENGTH_OF_OPEN3_OUTPUT { return 50 } sub _DEFAULT_WINDOW_WIDTH { return 1920 } sub _DEFAULT_WINDOW_HEIGHT { return 1080 } sub _DEFAULT_DEPTH { return 24 } sub _LOCAL_READ_BUFFER_SIZE { return 8192 } sub _WIN32_PROCESS_INHERIT_FLAGS { return 0 } sub _DEFAULT_CERT_TRUST { return 'C,,' } sub _PALEMOON_VERSION_EQUIV { return 52 } # very approx guess sub _MAX_VERSION_FOR_FTP_PROXY { return 89 } sub _DEFAULT_UPDATE_TIMEOUT { return 300 } # 5 minutes sub _MIN_VERSION_NO_CHROME_CALLS { return 94 } sub _MIN_VERSION_FOR_SCRIPT_SCRIPT { return 31 } sub _MIN_VERSION_FOR_SCRIPT_WO_ARGS { return 60 } # sub _MAGIC_NUMBER_MOZL4Z { return "mozLz40\0" } sub _WATERFOX_CURRENT_VERSION_EQUIV { return 68; } # https://github.com/MrAlex94/Waterfox/wiki/Versioning-Guidelines sub _WATERFOX_CLASSIC_VERSION_EQUIV { return 56; } # https://github.com/MrAlex94/Waterfox/wiki/Versioning-Guidelines my $proxy_name_regex = qr/perl_ff_m_\w+/smx; my $local_name_regex = qr/firefox_marionette_local_\w+/smx; my $tmp_name_regex = qr/firefox_marionette_(?:remote|local)_\w+/smx; my @sig_nums = split q[ ], $Config{sig_num}; my @sig_names = split q[ ], $Config{sig_name}; sub BY_XPATH { Carp::carp( '**** DEPRECATED METHOD - using find(..., BY_XPATH()) HAS BEEN REPLACED BY find ****' ); return 'xpath'; } sub BY_ID { Carp::carp( '**** DEPRECATED METHOD - using find(..., BY_ID()) HAS BEEN REPLACED BY find_id ****' ); return 'id'; } sub BY_NAME { Carp::carp( '**** DEPRECATED METHOD - using find(..., BY_NAME()) HAS BEEN REPLACED BY find_name ****' ); return 'name'; } sub BY_TAG { Carp::carp( '**** DEPRECATED METHOD - using find(..., BY_TAG()) HAS BEEN REPLACED BY find_tag ****' ); return 'tag name'; } sub BY_CLASS { Carp::carp( '**** DEPRECATED METHOD - using find(..., BY_CLASS()) HAS BEEN REPLACED BY find_class ****' ); return 'class name'; } sub BY_SELECTOR { Carp::carp( '**** DEPRECATED METHOD - using find(..., BY_SELECTOR()) HAS BEEN REPLACED BY find_selector ****' ); return 'css selector'; } sub BY_LINK { Carp::carp( '**** DEPRECATED METHOD - using find(..., BY_LINK()) HAS BEEN REPLACED BY find_link ****' ); return 'link text'; } sub BY_PARTIAL { Carp::carp( '**** DEPRECATED METHOD - using find(..., BY_PARTIAL()) HAS BEEN REPLACED BY find_partial ****' ); return 'partial link text'; } sub _download_directory { my ($self) = @_; my $directory; eval { my $context = $self->_context('chrome'); $directory = $self->script( 'let branch = Components.classes["' . q[@] . 'mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefService).getBranch(""); return branch.getStringPref ? branch.getStringPref("browser.download.downloadDir") : branch.getComplexValue("browser.download.downloadDir", Components.interfaces.nsISupportsString).data;' ); $self->_context($context); } or do { chomp $EVAL_ERROR; Carp::carp( "Firefox does not support dynamically determining download directory:$EVAL_ERROR" ); my $profile = Firefox::Marionette::Profile->parse( File::Spec->catfile( $self->{_profile_directory}, 'prefs.js' ) ); $directory = $profile->get_value('browser.download.downloadDir'); }; if ( $OSNAME eq 'cygwin' ) { $directory = $self->execute( 'cygpath', '-s', '-m', $directory ); } return $directory; } sub mime_types { my ($self) = @_; return @{ $self->{mime_types} }; } sub download { my ( $self, $path ) = @_; my $handle; if ( my $ssh = $self->_ssh() ) { $handle = $self->_get_file_via_scp( {}, $path, 'downloaded file' ); } else { $handle = FileHandle->new( $path, Fcntl::O_RDONLY() ) or Firefox::Marionette::Exception->throw( "Failed to open '$path' for reading:$EXTENDED_OS_ERROR"); } return $handle; } sub _directory_listing_via_ssh { my ( $self, $parameters, $directory, $short ) = @_; my $binary = 'ls'; my @arguments = ( '-1', "\"$directory\"" ); if ( $self->_remote_uname() eq 'MSWin32' ) { $binary = 'dir'; @arguments = ( '/B', "\"$directory\"" ); } my $ssh_parameters = {}; if ( $parameters->{ignore_missing_directory} ) { $ssh_parameters->{ignore_exit_status} = 1; } my @entries; my $entries = $self->_execute_via_ssh( $ssh_parameters, $binary, @arguments ); if ( defined $entries ) { foreach my $entry ( split /\r?\n/smx, $entries ) { if ($short) { push @entries, $entry; } else { push @entries, $self->_remote_catfile( $directory, $entry ); } } } return @entries; } sub _directory_listing { my ( $self, $parameters, $directory, $short ) = @_; my @entries; if ( my $ssh = $self->_ssh() ) { @entries = $self->_directory_listing_via_ssh( $parameters, $directory, $short ); } else { my $handle = DirHandle->new($directory); if ($handle) { while ( length( my $entry = $handle->read() ) ) { next if ( $entry eq File::Spec->updir() ); next if ( $entry eq File::Spec->curdir() ); if ($short) { push @entries, $entry; } else { push @entries, File::Spec->catfile( $directory, $entry ); } } $handle->close() or Firefox::Marionette::Exception->throw( "Failed to close directory '$directory':$EXTENDED_OS_ERROR"); } elsif ( $parameters->{ignore_missing_directory} ) { } else { Firefox::Marionette::Exception->throw( "Failed to open directory '$directory':$EXTENDED_OS_ERROR"); } } return @entries; } sub downloading { my ($self) = @_; my $downloading = 0; foreach my $entry ( $self->_directory_listing( {}, $self->_download_directory() ) ) { if ( $entry =~ /[.]part$/smx ) { $downloading = 1; Carp::carp("Waiting for $entry to download"); } } return $downloading; } sub downloads { my ($self) = @_; return $self->_directory_listing( {}, $self->_download_directory() ); } sub _setup_adb { my ( $self, $host ) = @_; $self->{_adb} = { host => $host, }; return; } sub _read_possible_proxy_path { my ( $self, $path ) = @_; my $local_proxy_handle = FileHandle->new( $path, Fcntl::O_RDONLY() ) or return; my $result; my $search_contents = $self->_read_and_close_handle( $local_proxy_handle, $path ); my $local_proxy = JSON::decode_json($search_contents); return $local_proxy; } sub _matching_remote_proxy { my ( $self, $ssh_local_directory, $search_local_proxy ) = @_; my $local_proxy = $self->_read_possible_proxy_path( File::Spec->catfile( $ssh_local_directory, 'reconnect' ) ); my $matched = 1; if ( !defined $local_proxy->{ssh} ) { return; } foreach my $key ( sort { $a cmp $b } keys %{$search_local_proxy} ) { if ( !defined $local_proxy->{ssh}->{$key} ) { $matched = 0; } elsif ( $key eq 'port' ) { if ( $local_proxy->{ssh}->{$key} != $search_local_proxy->{$key} ) { $matched = 0; } } else { if ( $local_proxy->{ssh}->{$key} ne $search_local_proxy->{$key} ) { $matched = 0; } } } if ($matched) { return $local_proxy; } return; } sub _get_max_scp_file_index { my ( $self, $directory_path ) = @_; my $directory_handle = DirHandle->new($directory_path) or Firefox::Marionette::Exception->throw( "Failed to open directory '$directory_path':$EXTENDED_OS_ERROR"); my $maximum_index; while ( my $entry = $directory_handle->read() ) { if ( $entry =~ /^file_(\d+)[.]dat/smx ) { my ($index) = ($1); if ( ( defined $maximum_index ) && ( $maximum_index > $index ) ) { } else { $maximum_index = $index; } } } $directory_handle->close() or Firefox::Marionette::Exception->throw( "Failed to close directory '$directory_path':$EXTENDED_OS_ERROR"); return $maximum_index; } sub _setup_ssh_with_reconnect { my ( $self, $host, $port, $user ) = @_; my $search_local_proxy = { user => $user, host => $host, port => $port }; my $temp_directory = File::Spec->tmpdir(); my $temp_handle = DirHandle->new($temp_directory) or Firefox::Marionette::Exception->throw( "Failed to open directory '$temp_directory':$EXTENDED_OS_ERROR"); POSSIBLE_REMOTE_PROXY: while ( my $tainted_entry = $temp_handle->read() ) { next if ( $tainted_entry eq File::Spec->curdir() ); next if ( $tainted_entry eq File::Spec->updir() ); if ( $tainted_entry =~ /^($proxy_name_regex)$/smx ) { my ($untainted_entry) = ($1); my $ssh_local_directory = File::Spec->catfile( $temp_directory, $untainted_entry ); if ( my $proxy = $self->_matching_remote_proxy( $ssh_local_directory, $search_local_proxy ) ) { $self->{_ssh} = { port => $port, host => $host, user => $user, pid => $proxy->{ssh}->{pid}, }; if ( ( defined $proxy->{firefox} ) && ( defined $proxy->{firefox}->{pid} ) ) { $self->{_firefox_pid} = $proxy->{firefox}->{pid}; } if ( ( defined $proxy->{xvfb} ) && ( defined $proxy->{xvfb}->{pid} ) ) { $self->{_xvfb_pid} = $proxy->{xvfb}->{pid}; } if ( ( $OSNAME eq 'MSWin32' ) || ( $OSNAME eq 'cygwin' ) ) { $self->{_ssh}->{use_control_path} = 0; $self->{_ssh}->{use_unix_sockets} = 0; } else { $self->{_ssh}->{use_control_path} = 1; $self->{_ssh}->{use_unix_sockets} = 1; $self->{_ssh}->{control_path} = File::Spec->catfile( $ssh_local_directory, 'control.sock' ); } $self->{_remote_uname} = $proxy->{ssh}->{uname}; $self->{marionette_binary} = $proxy->{ssh}->{binary}; $self->{_initial_version} = $proxy->{firefox}->{version}; $self->_initialise_version(); $self->{_ssh_local_directory} = $ssh_local_directory; $self->{_root_directory} = $proxy->{ssh}->{root}; if ( defined $proxy->{ssh}->{tmp} ) { $self->{_original_remote_tmp_directory} = $proxy->{ssh}->{tmp}; } $self->{profile_path} = $self->_remote_catfile( $self->{_root_directory}, 'profile', 'prefs.js' ); my $local_scp_directory = File::Spec->catdir( $self->ssh_local_directory(), 'scp' ); $self->{_local_scp_get_directory} = File::Spec->catdir( $local_scp_directory, 'get' ); $self->{_scp_get_file_index} = $self->_get_max_scp_file_index( $self->{_local_scp_get_directory} ); $self->{_local_scp_put_directory} = File::Spec->catdir( $local_scp_directory, 'put' ); $self->{_scp_put_file_index} = $self->_get_max_scp_file_index( $self->{_local_scp_put_directory} ); last POSSIBLE_REMOTE_PROXY; } } } $temp_handle->close() or Firefox::Marionette::Exception->throw( "Failed to close directory '$temp_directory':$EXTENDED_OS_ERROR"); if ( $self->_ssh() ) { } else { Firefox::Marionette::Exception->throw( "Failed to detect existing local ssh tunnel to $user\@$host"); } return; } sub ssh_local_directory { my ($self) = @_; return $self->{_ssh_local_directory}; } sub _setup_ssh { my ( $self, $host, $port, $user, $reconnect ) = @_; if ($reconnect) { $self->_setup_ssh_with_reconnect( $host, $port, $user ); } else { my $ssh_local_directory = File::Temp->newdir( CLEANUP => 0, TEMPLATE => File::Spec->catdir( File::Spec->tmpdir(), 'perl_ff_m_XXXXXXXXXXX' ) ) or Firefox::Marionette::Exception->throw( "Failed to create temporary directory:$EXTENDED_OS_ERROR"); $self->{_ssh_local_directory} = $ssh_local_directory->dirname(); my $local_scp_directory = File::Spec->catdir( $self->ssh_local_directory(), 'scp' ); mkdir $local_scp_directory, Fcntl::S_IRWXU() or Firefox::Marionette::Exception->throw( "Failed to create directory $local_scp_directory:$EXTENDED_OS_ERROR" ); $self->{_local_scp_get_directory} = File::Spec->catdir( $local_scp_directory, 'get' ); mkdir $self->{_local_scp_get_directory}, Fcntl::S_IRWXU() or Firefox::Marionette::Exception->throw( "Failed to create directory $self->{_local_scp_get_directory}:$EXTENDED_OS_ERROR" ); $self->{_local_scp_put_directory} = File::Spec->catdir( $local_scp_directory, 'put' ); mkdir $self->{_local_scp_put_directory}, Fcntl::S_IRWXU() or Firefox::Marionette::Exception->throw( "Failed to create directory $self->{_local_scp_put_directory}:$EXTENDED_OS_ERROR" ); $self->{_ssh} = { host => $host, port => $port, user => $user, }; if ( ( $OSNAME eq 'MSWin32' ) || ( $OSNAME eq 'cygwin' ) ) { $self->{_ssh}->{use_control_path} = 0; } else { $self->{_ssh}->{use_control_path} = 1; $self->{_ssh}->{control_path} = File::Spec->catfile( $self->ssh_local_directory(), 'control.sock' ); } } $self->_initialise_remote_uname(); return; } sub _control_path { my ($self) = @_; if ( my $ssh = $self->_ssh() ) { if ( $ssh->{use_control_path} ) { return $ssh->{control_path}; } } return; } sub _ssh { my ($self) = @_; return $self->{_ssh}; } sub _adb { my ($self) = @_; return $self->{_adb}; } sub images { my ( $self, $from ) = @_; return grep { $_->url() } map { Firefox::Marionette::Image->new($_) } $self->has( '//*[self::img or self::input]', undef, $from ); } sub links { my ( $self, $from ) = @_; return map { Firefox::Marionette::Link->new($_) } $self->has( '//*[self::a or self::area or self::frame or self::iframe or self::meta]', undef, $from ); } sub _get_marionette_parameter { my ( $self, %parameters ) = @_; foreach my $deprecated_key (qw(firefox_binary firefox marionette)) { if ( $parameters{$deprecated_key} ) { Carp::carp( "**** DEPRECATED - $deprecated_key HAS BEEN REPLACED BY binary ****" ); $self->{marionette_binary} = $parameters{$deprecated_key}; } } if ( $parameters{binary} ) { $self->{marionette_binary} = $parameters{binary}; } return; } sub _store_restart_parameters { my ( $self, %parameters ) = @_; $self->{_restart_parameters} = { restart => 1 }; foreach my $key ( sort { $a cmp $b } keys %parameters ) { next if ( $key eq 'profile' ); next if ( $key eq 'capabilities' ); next if ( $key eq 'timeout' ); $self->{_restart_parameters}->{$key} = $parameters{$key}; } return; } sub _init { my ( $class, %parameters ) = @_; my $self = bless {}, $class; $self->_store_restart_parameters(%parameters); $self->{last_message_id} = 0; $self->{creation_pid} = $PROCESS_ID; $self->{sleep_time_in_ms} = $parameters{sleep_time_in_ms}; foreach my $type (qw(nightly developer waterfox)) { if ( defined $parameters{$type} ) { $self->{requested_version}->{$type} = $parameters{$type}; } } if ( defined $parameters{survive} ) { $self->{survive} = $parameters{survive}; } $self->{extension_index} = 0; $self->{debug} = $parameters{debug}; $self->_get_marionette_parameter(%parameters); if ( $parameters{console} ) { $self->{console} = 1; } if ( defined $parameters{adb} ) { $self->_setup_adb( $parameters{adb} ); } if ( defined $parameters{host} ) { if ( $OSNAME eq 'MSWin32' ) { $parameters{user} ||= Win32::LoginName(); } else { $parameters{user} ||= getpwuid $EFFECTIVE_USER_ID; } $parameters{port} ||= scalar getservbyname 'ssh', 'tcp'; $self->_setup_ssh( $parameters{host}, $parameters{port}, $parameters{user}, $parameters{reconnect} ); } if ( defined $parameters{width} ) { $self->{window_width} = $parameters{width}; } if ( defined $parameters{height} ) { $self->{window_height} = $parameters{height}; } if ( defined $parameters{har} ) { $self->{_har} = $parameters{har}; require Firefox::Marionette::Extension::HarExportTrigger; } $self->{mime_types} = [ qw( application/x-gzip application/gzip application/zip application/pdf application/octet-stream application/msword application/vnd.openxmlformats-officedocument.wordprocessingml.document application/vnd.openxmlformats-officedocument.wordprocessingml.template application/vnd.ms-word.document.macroEnabled.12 application/vnd.ms-word.template.macroEnabled.12 application/vnd.ms-excel application/vnd.openxmlformats-officedocument.spreadsheetml.sheet application/vnd.openxmlformats-officedocument.spreadsheetml.template application/vnd.ms-excel.sheet.macroEnabled.12 application/vnd.ms-excel.template.macroEnabled.12 application/vnd.ms-excel.addin.macroEnabled.12 application/vnd.ms-excel.sheet.binary.macroEnabled.12 application/vnd.ms-powerpoint application/vnd.openxmlformats-officedocument.presentationml.presentation application/vnd.openxmlformats-officedocument.presentationml.template application/vnd.openxmlformats-officedocument.presentationml.slideshow application/vnd.ms-powerpoint.addin.macroEnabled.12 application/vnd.ms-powerpoint.presentation.macroEnabled.12 application/vnd.ms-powerpoint.template.macroEnabled.12 application/vnd.ms-powerpoint.slideshow.macroEnabled.12 application/vnd.ms-access ) ]; my %known_mime_types; foreach my $mime_type ( @{ $self->{mime_types} } ) { $known_mime_types{$mime_type} = 1; } foreach my $mime_type ( @{ $parameters{mime_types} } ) { if ( !$known_mime_types{$mime_type} ) { push @{ $self->{mime_types} }, $mime_type; $known_mime_types{$mime_type} = 1; } } return $self; } sub _check_for_existing_local_firefox_process { my ($self) = @_; my $profile_path = File::Spec->catfile( $self->{_profile_directory}, 'prefs.js' ); my $profile_handle = FileHandle->new($profile_path); my $port; if ($profile_handle) { while ( my $line = <$profile_handle> ) { if ( $line =~ /^user_pref[(]"marionette[.]port",[ ](\d+)[)];$/smx ) { ($port) = ($1); } } } return $port || _DEFAULT_PORT(); } sub _reconnected { my ($self) = @_; return $self->{_reconnected}; } sub _check_reconnecting_firefox_process_is_alive { my ( $self, $pid ) = @_; if ( $OSNAME eq 'MSWin32' ) { if ( Win32::Process::Open( my $process, $pid, _WIN32_PROCESS_INHERIT_FLAGS() ) ) { $self->{_win32_firefox_process} = $process; return $pid; } } elsif ( kill 0, $pid ) { return $pid; } return; } sub _get_local_reconnect_pid { my ($self) = @_; my $temp_directory = File::Spec->tmpdir(); my $temp_handle = DirHandle->new($temp_directory) or Firefox::Marionette::Exception->throw( "Failed to open directory '$temp_directory':$EXTENDED_OS_ERROR"); my $alive_pid; TEMP_DIR_LISTING: while ( my $tainted_entry = $temp_handle->read() ) { next if ( $tainted_entry eq File::Spec->curdir() ); next if ( $tainted_entry eq File::Spec->updir() ); if ( $tainted_entry =~ /^($local_name_regex)$/smx ) { my ($untainted_entry) = ($1); my $possible_root_directory = File::Spec->catfile( $temp_directory, $untainted_entry ); my $local_proxy = $self->_read_possible_proxy_path( File::Spec->catfile( $possible_root_directory, 'reconnect' ) ); if ( ( defined $local_proxy->{firefox} ) && ( defined $local_proxy->{firefox}->{binary} ) ) { if ( $self->_binary() ne $local_proxy->{firefox}->{binary} ) { next TEMP_DIR_LISTING; } } elsif ( $self->_binary() ) { next TEMP_DIR_LISTING; } if ( ( defined $local_proxy->{firefox} ) && ( $local_proxy->{firefox}->{pid} ) ) { if ( my $check_pid = $self->_check_reconnecting_firefox_process_is_alive( $local_proxy->{firefox}->{pid} ) ) { $alive_pid = $check_pid; } else { next TEMP_DIR_LISTING; } } else { next TEMP_DIR_LISTING; } if ( ( defined $local_proxy->{xvfb} ) && ( defined $local_proxy->{xvfb}->{pid} ) && ( kill 0, $local_proxy->{xvfb}->{pid} ) ) { $self->{_xvfb_pid} = $local_proxy->{xvfb}->{pid}; } $self->{_initial_version} = $local_proxy->{firefox}->{version}; $self->{_root_directory} = $possible_root_directory; if ( $self->{profile_name} ) { $self->{_profile_directory} = Firefox::Marionette::Profile->directory( $self->{profile_name} ); $self->{profile_path} = File::Spec->catfile( $self->{_profile_directory}, 'prefs.js' ); } else { $self->{_profile_directory} = File::Spec->catfile( $self->{_root_directory}, 'profile' ); $self->{_download_directory} = File::Spec->catfile( $self->{_root_directory}, 'downloads' ); $self->{profile_path} = File::Spec->catfile( $self->{_profile_directory}, 'prefs.js' ); } } } $temp_handle->close(); return $alive_pid; } sub _reconnect { my ( $self, %parameters ) = @_; if ( $parameters{profile_name} ) { $self->{profile_name} = $parameters{profile_name}; } $self->{_reconnected} = 1; if ( my $ssh = $self->_ssh() ) { if ( my $pid = $self->_firefox_pid() ) { if ( $self->_remote_process_running($pid) ) { $self->{_firefox_pid} = $pid; } } } else { if ( my $pid = $self->_get_local_reconnect_pid() ) { if ( ( kill 0, $pid ) && ( my $port = $self->_check_for_existing_local_firefox_process() ) ) { $self->{_firefox_pid} = $pid; } } } my ( $host, $user ); if ( my $ssh = $self->_ssh() ) { $host = $self->_ssh()->{host}; $user = $self->_ssh()->{user}; } elsif (( $OSNAME eq 'MSWin32' ) || ( $OSNAME eq 'cygwin' ) ) { $user = Win32::LoginName(); $host = 'localhost'; } else { $user = getpwuid $EFFECTIVE_USER_ID; $host = 'localhost'; } my $quoted_user = defined $user ? quotemeta $user : q[]; if ( $self->_ssh() ) { $self->_initialise_remote_uname(); } $self->_check_visible(%parameters); my $port = $self->_get_marionette_port(); defined $port or Firefox::Marionette::Exception->throw( "Existing firefox process could not be found at $user\@$host"); my $socket; socket $socket, $self->_using_unix_sockets_for_ssh_connection() ? Socket::PF_UNIX() : Socket::PF_INET(), Socket::SOCK_STREAM(), 0 or Firefox::Marionette::Exception->throw( "Failed to create a socket:$EXTENDED_OS_ERROR"); binmode $socket; my $sock_addr = $self->_get_sock_addr( $host, $port ); connect $socket, $sock_addr or Firefox::Marionette::Exception->throw( "Failed to re-connect to Firefox process at '$host:$port':$EXTENDED_OS_ERROR" ); $self->{_socket} = $socket; my $initial_response = $self->_read_from_socket(); $self->{marionette_protocol} = $initial_response->{marionetteProtocol}; $self->{application_type} = $initial_response->{applicationType}; $self->_compatibility_checks_for_older_marionette(); return $self->new_session( $parameters{capabilities} ); } sub _compatibility_checks_for_older_marionette { my ($self) = @_; if ( !$self->marionette_protocol() ) { if ( $self->{_initial_packet_size} == _OLD_INITIAL_PACKET_SIZE() ) { $self->{_old_protocols_key} = 'type'; } else { $self->{_old_protocols_key} = 'name'; } my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, 'getMarionetteID', 'to' => 'root' ] ); my $next_message = $self->_read_from_socket(); $self->{marionette_id} = $next_message->{id}; } return; } sub profile_directory { my ($self) = @_; return $self->{_profile_directory}; } sub _pk11_tokendb_interface_preamble { my ($self) = @_; return <<'_JS_'; # security/manager/ssl/nsIPK11Token.idl let pk11db = Components.classes["@mozilla.org/security/pk11tokendb;1"].getService(Components.interfaces.nsIPK11TokenDB); let token = pk11db.getInternalKeyToken(); _JS_ } sub pwd_mgr_needs_login { my ($self) = @_; my $script = <<'_JS_'; if (('hasPassword' in token) && (!token.hasPassword)) { return false; } else if (('needsLogin' in token) && (!token.needsLogin())) { return false; } else if (token.isLoggedIn()) { return false; } else { return true; } _JS_ $self->chrome(); my $result = $self->script( $self->_compress_script( $self->_pk11_tokendb_interface_preamble() . $script ) ); $self->content(); return $result; } sub pwd_mgr_logout { my ($self) = @_; my $script = <<'_JS_'; token.logoutAndDropAuthenticatedResources(); _JS_ $self->chrome(); $self->script( $self->_compress_script( $self->_pk11_tokendb_interface_preamble() . $script ) ); $self->content(); return $self; } sub pwd_mgr_lock { my ( $self, $password ) = @_; if ( !defined $password ) { Firefox::Marionette::Exception->throw( 'Primary Password has not been provided'); } my $script = <<'_JS_'; if (token.needsUserInit) { token.initPassword(arguments[0]); } else { token.changePassword("",arguments[0]); } _JS_ $self->chrome(); $self->script( $self->_compress_script( $self->_pk11_tokendb_interface_preamble() . $script ), args => [$password] ); $self->content(); return $self; } sub pwd_mgr_login { my ( $self, $password ) = @_; if ( !defined $password ) { Firefox::Marionette::Exception->throw( 'Primary Password has not been provided'); } my $script = <<'_JS_'; if (token.checkPassword(arguments[0])) { return true; } else { return false; } _JS_ $self->chrome(); if ( $self->script( $self->_compress_script( $self->_pk11_tokendb_interface_preamble() . $script ), args => [$password] ) ) { $self->content(); } else { $self->content(); Firefox::Marionette::Exception->throw('Incorrect Primary Password'); } return $self; } sub _import_profile_paths { my ( $self, %parameters ) = @_; if ( $parameters{import_profile_paths} ) { foreach my $path ( @{ $parameters{import_profile_paths} } ) { my ( $volume, $directories, $name ) = File::Spec->splitpath($path); my $read_handle = FileHandle->new( $path, Fcntl::O_RDONLY() ) or Firefox::Marionette::Exception->throw( "Failed to open '$path' for reading:$EXTENDED_OS_ERROR"); binmode $read_handle; if ( $self->_ssh() ) { $self->_put_file_via_scp( $read_handle, $self->_remote_catfile( $self->{_profile_directory}, $name ), $name ); } else { my $write_path = File::Spec->catfile( $self->{_profile_directory}, $name ); my $write_handle = FileHandle->new( $write_path, Fcntl::O_WRONLY() | Fcntl::O_CREAT() | Fcntl::O_EXCL(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR() ) or Firefox::Marionette::Exception->throw( "Failed to open '$write_path' for writing:$EXTENDED_OS_ERROR" ); binmode $write_handle; my $result; while ( $result = $read_handle->read( my $buffer, _LOCAL_READ_BUFFER_SIZE() ) ) { $write_handle->print($buffer) or Firefox::Marionette::Exception->throw( "Failed to write to '$write_path':$EXTENDED_OS_ERROR"); } defined $result or Firefox::Marionette::Exception->throw( "Failed to read from '$path':$EXTENDED_OS_ERROR"); $write_handle->close() or Firefox::Marionette::Exception->throw( "Failed to close '$write_path':$EXTENDED_OS_ERROR"); } $read_handle->close() or Firefox::Marionette::Exception->throw( "Failed to close '$path':$EXTENDED_OS_ERROR"); } } return; } sub _login_interface_preamble { my ($self) = @_; return <<'_JS_'; # toolkit/components/passwordmgr/nsILoginManager.idl let loginManager = Components.classes["@mozilla.org/login-manager;1"].getService(Components.interfaces.nsILoginManager); _JS_ } sub fill_login { my ($self) = @_; my $found; my $browser_uri = URI->new( $self->uri() ); FORM: foreach my $form ( $self->find_tag('form') ) { my $action = $form->attribute('action'); my $action_uri = URI->new_abs( $action, $browser_uri ); my $old = $self->_context('chrome'); my @logins = $self->_translate_firefox_logins( @{ $self->script( $self->_compress_script( $self->_login_interface_preamble() . <<"_JS_"), args => [ $browser_uri->scheme() . '://' . $browser_uri->host(), $action_uri->scheme() . '://' . $action_uri->host() ] ) } ); try { return loginManager.findLogins(arguments[0], arguments[1], null); } catch (e) { console.log("Unable to use modern loginManager.findLogins methods:" + e); return loginManager.findLogins({}, arguments[0], arguments[1], null); } _JS_ $self->_context($old); foreach my $login (@logins) { if ( ( my $user_field = $form->has_name( $login->user_field ) ) && ( my $password_field = $form->has_name( $login->password_field ) ) ) { $user_field->clear(); $password_field->clear(); $user_field->type( $login->user() ); $password_field->type( $login->password() ); $found = 1; last FORM; } } } if ( !$found ) { Firefox::Marionette::Exception->throw( "Unable to fill in form on $browser_uri"); } return $self; } sub delete_login { my ( $self, $login ) = @_; my $old = $self->_context('chrome'); $self->script( $self->_compress_script( $self->_login_interface_preamble() . $self->_define_login_info_from_blessed_user( 'loginInfo', $login ) . <<"_JS_"), args => [$login] ); loginManager.removeLogin(loginInfo); _JS_ $self->_context($old); return $self; } sub delete_logins { my ($self) = @_; my $old = $self->_context('chrome'); $self->script( $self->_compress_script( $self->_login_interface_preamble() . <<"_JS_") ); loginManager.removeAllLogins(); _JS_ $self->_context($old); return $self; } sub _define_login_info_from_blessed_user { my ( $self, $variable_name, $login ) = @_; return <<"_JS_"; let $variable_name = Components.classes["\@mozilla.org/login-manager/loginInfo;1"].createInstance(Components.interfaces.nsILoginInfo); $variable_name.init(arguments[0].host, ("realm" in arguments[0] && arguments[0].realm !== null ? null : arguments[0].origin || ""), arguments[0].realm, arguments[0].user, arguments[0].password, "user_field" in arguments[0] && arguments[0].user_field !== null ? arguments[0].user_field : "", "password_field" in arguments[0] && arguments[0].password_field !== null ? arguments[0].password_field : ""); _JS_ } sub _get_1password_login_items { my ( $class, $json ) = @_; my @items; foreach my $account ( @{ $json->{accounts} } ) { foreach my $vault ( @{ $account->{vaults} } ) { foreach my $item ( @{ $vault->{items} } ) { if ( ( $item->{item}->{categoryUuid} eq '001' ) && ( $item->{item}->{overview}->{url} ) ) { # Login push @items, $item->{item}; } } } } return @items; } sub logins_from_csv { my ( $class, $import_handle ) = @_; binmode $import_handle, ':encoding(utf8)'; my $parameters = $class->_csv_parameters( $class->_get_extra_parameters($import_handle) ); $parameters->{auto_diag} = 1; my $csv = Text::CSV_XS->new($parameters); my @logins; my $count = 0; my %import_headers; foreach my $key ( $csv->header($import_handle) ) { $import_headers{$key} = $count; $count += 1; } my %mapping = ( 'web site' => 'host', 'login name' => 'user', login_uri => 'host', login_username => 'user', login_password => 'password', url => 'host', username => 'user', password => 'password', httprealm => 'realm', formactionorigin => 'origin', guid => 'guid', timecreated => 'creation_in_ms', timelastused => 'last_used_in_ms', timepasswordchanged => 'password_changed_in_ms', ); while ( my $row = $csv->getline($import_handle) ) { my %parameters; foreach my $key ( sort { $a cmp $b } keys %import_headers ) { if ( ( exists $row->[ $import_headers{$key} ] ) && ( defined $mapping{$key} ) ) { $parameters{ $mapping{$key} } = $row->[ $import_headers{$key} ]; } } foreach my $key (qw(host origin)) { if ( defined $parameters{$key} ) { my $uri = URI->new( $parameters{$key} )->canonical(); if ( !$uri->has_recognized_scheme() ) { my $default_scheme = 'https://'; warn "$parameters{$key} does not have a recognised scheme. Prepending '$default_scheme'\n"; $uri = URI->new( $default_scheme . $parameters{$key} ); } $parameters{$key} = $uri->scheme() . q[://] . $uri->host(); if ( $uri->default_port() != $uri->port() ) { $parameters{$key} .= q[:] . $uri->port(); } } } if ( my $login = $class->_csv_record_is_a_login( $row, \%parameters, \%import_headers ) ) { push @logins, $login; } } return @logins; } sub _csv_record_is_a_login { my ( $class, $row, $parameters, $import_headers ) = @_; if ( ( $parameters->{host} ) && ( $parameters->{host} eq 'http://sn' ) && ( $import_headers->{extra} ) && ( $row->[ $import_headers->{extra} ] ) && ( $row->[ $import_headers->{extra} ] =~ /^NoteType:/smx ) ) { warn "Skipping non-web login for '$parameters->{user}' (probably from a LastPass export)\n"; return; } elsif (( defined $import_headers->{'first one-time password'} ) && ( $import_headers->{type} ) && ( $row->[ $import_headers->{type} ] ne 'Login' ) ) # See 001 reference for v8 { warn "Skipping $row->[ $import_headers->{type} ] record (probably from a 1Password export)\n"; return; } elsif (( $parameters->{host} ) && ( $parameters->{user} ) && ( $parameters->{password} ) ) { return Firefox::Marionette::Login->new( %{$parameters} ); } return; } sub _csv_parameters { my ( $class, $extra ) = @_; return { binary => 1, empty_is_undef => 1, %{$extra}, }; } sub _get_extra_parameters { my ( $class, $import_handle ) = @_; my @extra_parameter_sets = ( {}, # normal { escape_char => q[\\], allow_loose_escapes => 1 }, # KeePass { escape_char => q[\\], allow_loose_escapes => 1, eol => ",$INPUT_RECORD_SEPARATOR", }, # 1Password v7 ); if ( $OSNAME eq 'MSWin32' or $OSNAME eq 'cygwin' ) { push @extra_parameter_sets, { escape_char => q[\\], allow_loose_escapes => 1, eol => ",\r\n", } # 1Password v7 } my $extra_parameters = {}; SET: foreach my $parameter_set (@extra_parameter_sets) { seek $import_handle, Fcntl::SEEK_SET(), 0 or die "Failed to seek to start of file:$EXTENDED_OS_ERROR\n"; my $parameters = $class->_csv_parameters($parameter_set); $parameters->{auto_diag} = 2; my $csv = Text::CSV_XS->new($parameters); eval { foreach my $key ( $csv->header( $import_handle, { munge_column_names => sub { defined $_ ? lc : q[] } } ) ) { } while ( my $row = $csv->getline($import_handle) ) { } $extra_parameters = $parameter_set; } or do { next SET; }; last SET; } seek $import_handle, Fcntl::SEEK_SET(), 0 or die "Failed to seek to start of file:$EXTENDED_OS_ERROR\n"; return $extra_parameters; } sub logins_from_zip { my ( $class, $import_handle ) = @_; my @logins; my $zip = Archive::Zip->new($import_handle); if ( $zip->memberNamed('export.data') && ( $zip->memberNamed('export.attributes') ) ) { # 1Password v8 my $json = JSON::decode_json( $zip->contents('export.data') ); foreach my $item ( $class->_get_1password_login_items($json) ) { my ( $username, $password ); foreach my $login_field ( @{ $item->{details}->{loginFields} } ) { if ( $login_field->{designation} eq 'username' ) { $username = $login_field->{value}; } elsif ( $login_field->{designation} eq 'password' ) { $password = $login_field->{value}; } } if ( ( defined $username ) && ( defined $password ) ) { push @logins, Firefox::Marionette::Login->new( guid => $item->{uuid}, host => $item->{overview}->{url}, user => $username, password => $password, creation => $item->{createdAt}, ); } } } return @logins; } sub add_login { my ( $self, @parameters ) = @_; my $login; if ( scalar @parameters == 1 ) { $login = $parameters[0]; } else { $login = Firefox::Marionette::Login->new(@parameters); } my $old = $self->_context('chrome'); $self->script( $self->_compress_script( $self->_login_interface_preamble() . $self->_define_login_info_from_blessed_user( 'loginInfo', $login ) . <<"_JS_"), args => [$login] ); # xpcom/ds/nsIWritablePropertyBag2.idl loginManager.addLogin(loginInfo); let loginMetaInfo = Components.classes["\@mozilla.org/hash-property-bag;1"].createInstance(Components.interfaces.nsIWritablePropertyBag2); if ("guid" in arguments[0] && arguments[0].guid !== null) { loginMetaInfo.setPropertyAsAUTF8String("guid", arguments[0].guid); } if ("creation_in_ms" in arguments[0] && arguments[0].creation_in_ms !== null) { loginMetaInfo.setPropertyAsUint64("timeCreated", arguments[0].creation_in_ms); } if ("last_used_in_ms" in arguments[0] && arguments[0].last_used_in_ms !== null) { loginMetaInfo.setPropertyAsUint64("timeLastUsed", arguments[0].last_used_in_ms); } if ("password_changed_in_ms" in arguments[0] && arguments[0].password_changed_in_ms !== null) { loginMetaInfo.setPropertyAsUint64("timePasswordChanged", arguments[0].password_changed_in_ms); } if ("times_used" in arguments[0] && arguments[0].times_used !== null) { loginMetaInfo.setPropertyAsUint64("timesUsed", arguments[0].times_used); } loginManager.modifyLogin(loginInfo, loginMetaInfo); _JS_ $self->_context($old); return $self; } sub _translate_firefox_logins { my ( $self, @results ) = @_; return map { Firefox::Marionette::Login->new( host => $_->{hostname}, user => $_->{username}, password => $_->{password}, user_field => $_->{usernameField} eq q[] ? undef : $_->{usernameField}, password_field => $_->{passwordField} eq q[] ? undef : $_->{passwordField}, realm => $_->{httpRealm}, origin => exists $_->{formActionOrigin} ? ( defined $_->{formActionOrigin} && $_->{formActionOrigin} ne q[] ? $_->{formActionOrigin} : undef ) : ( defined $_->{formSubmitURL} && $_->{formSubmitURL} ne q[] ? $_->{formSubmitURL} : undef ), guid => $_->{guid}, times_used => $_->{timesUsed}, creation_in_ms => $_->{timeCreated}, last_used_in_ms => $_->{timeLastUsed}, password_changed_in_ms => $_->{timePasswordChanged} ) } @results; } sub logins { my ($self) = @_; my $old = $self->_context('chrome'); my $result = $self->script( $self->_compress_script( $self->_login_interface_preamble() . <<"_JS_") ); return loginManager.getAllLogins({}); _JS_ $self->_context($old); return $self->_translate_firefox_logins( @{$result} ); } sub _binary_directory { my ($self) = @_; if ( exists $self->{_binary_directory} ) { } else { my $binary = $self->_binary(); my $binary_directory; if ( $self->_ssh() ) { if ( $self->_remote_uname() eq 'MSWin32' ) { my ( $volume, $directories ) = File::Spec::Win32->splitpath($binary); $binary_directory = File::Spec::Win32->catdir( $volume, $directories ); } elsif ( $self->_remote_uname() eq 'cygwin' ) { my ( $volume, $directories ) = File::Spec::Unix->splitpath($binary); $binary_directory = File::Spec::Unix->catdir( $volume, $directories ); } else { my $remote_path_to_binary = $self->_execute_via_ssh( { ignore_exit_status => 1 }, 'which', $binary ); if ( defined $remote_path_to_binary ) { chomp $remote_path_to_binary; if ( my $symlinked_path_to_binary = $self->_execute_via_ssh( { ignore_exit_status => 1 }, 'readlink', '-f', $remote_path_to_binary ) ) { my ( $volume, $directories ) = File::Spec::Unix->splitpath( $symlinked_path_to_binary); $binary_directory = File::Spec::Unix->catdir( $volume, $directories ); } else { my ( $volume, $directories ) = File::Spec::Unix->splitpath($remote_path_to_binary); $binary_directory = File::Spec::Unix->catdir( $volume, $directories ); } } } } elsif ( $OSNAME eq 'cygwin' ) { my ( $volume, $directories ) = File::Spec::Unix->splitpath($binary); $binary_directory = File::Spec::Unix->catdir( $volume, $directories ); } else { my ( $volume, $directories ) = File::Spec->splitpath($binary); $binary_directory = File::Spec->catdir( $volume, $directories ); } if ( defined $binary_directory ) { if ( $binary_directory eq '/usr/bin' ) { $binary_directory = undef; } } $self->{_binary_directory} = $binary_directory; } return $self->{_binary_directory}; } sub _most_recent_updates_index { my ($self) = @_; if ( defined $self->{_cached_per_instance}->{_most_recent_updates_index} ) { } else { my $binary_directory = $self->_binary_directory(); if ( defined $binary_directory ) { my $found_updates_directory; foreach my $entry ( $self->_directory_listing( { ignore_missing_directory => 1 }, $binary_directory, 1 ) ) { if ( $entry eq 'updates' ) { $found_updates_directory = 1; } } if ($found_updates_directory) { my $updates_path = File::Spec->catfile( $binary_directory, 'updates' ); my @entries; foreach my $entry ( $self->_directory_listing( { ignore_missing_directory => 1 }, $updates_path, 1 ) ) { if ( $entry =~ /^(\d{1,10})$/smx ) { push @entries, $1; } } my @sorted_entries = reverse sort { $a <=> $b } @entries; $self->{_cached_per_instance}->{_most_recent_updates_index} = shift @sorted_entries; } } } return $self->{_cached_per_instance}->{_most_recent_updates_index}; } sub _most_recent_updates_status_path { my ( $self, $index ) = @_; if ( defined( my $most_recent_updates_index = $self->_most_recent_updates_index() ) ) { my $binary_directory = $self->_binary_directory(); return File::Spec->catfile( $binary_directory, 'updates', $most_recent_updates_index, 'update.status' ); } return; } sub _wait_for_updating_to_finish { my ($self) = @_; delete $self->{_cached_per_instance}->{_most_recent_updates_index}; my $count = 1; my $updating; while ($count) { $count = 0; if ( defined( my $most_recent_updates_index = $self->_most_recent_updates_index() ) ) { my $binary_directory = $self->_binary_directory(); my $most_recent_update_directory = File::Spec->catfile( $binary_directory, 'updates', $most_recent_updates_index ); foreach my $entry ( $self->_directory_listing( { ignore_missing_directory => 1 }, $most_recent_update_directory, 1 ) ) { $count += 1; } } if ($count) { $updating = 1; sleep 1; } } if ($updating) { sleep 1; } return; } sub _get_update_status { my ($self) = @_; my $updates_status_path = $self->_most_recent_updates_status_path(); if ($updates_status_path) { my $updates_status_handle; if ( $self->_ssh() ) { $updates_status_handle = $self->_get_file_via_scp( {}, $updates_status_path, 'update.status file' ); } else { $updates_status_handle = FileHandle->new( $updates_status_path, Fcntl::O_RDONLY() ); } if ($updates_status_handle) { my $status = $self->_read_and_close_handle( $updates_status_handle, $updates_status_path ); chomp $status; return $status; } elsif ( $OS_ERROR == POSIX::ENOENT() ) { } else { Firefox::Marionette::Exception->throw( "Failed to open $updates_status_path for reading:$EXTENDED_OS_ERROR" ); } } return; } sub _wait_for_any_background_update_status { my ($self) = @_; my $update_status = $self->_get_update_status(); while ( ( defined $update_status ) && ( $update_status eq 'applying' ) ) { sleep 1; $update_status = $self->_get_update_status(); } return; } sub restart { my ($self) = @_; my $capabilities = $self->capabilities(); my $timeouts = $self->timeouts(); if ( $self->_session_id() ) { $self->_quit_over_marionette(); delete $self->{session_id}; } else { $self->_terminate_marionette_process(); } $self->_wait_for_any_background_update_status(); foreach my $key ( qw(marionette_protocol application_type _firefox_pid last_message_id _child_error) ) { delete $self->{$key}; } if ( my $ssh = $self->_ssh() ) { delete $ssh->{ssh_local_tcp_socket}; } delete $self->{_cached_per_instance}; $self->_reset_marionette_port(); $self->_get_version(); my @arguments = $self->_setup_arguments( %{ $self->{_restart_parameters} } ); $self->_launch(@arguments); my $socket = $self->_setup_local_connection_to_firefox(@arguments); my $session_id; ( $session_id, $capabilities ) = $self->_initial_socket_setup( $socket, $capabilities ); $self->_check_protocol_version_and_pid( $session_id, $capabilities ); $self->_post_launch_checks_and_setup($timeouts); return $self; } sub _reset_marionette_port { my ($self) = @_; my $handle; if ( $self->_ssh() ) { $handle = $self->_get_file_via_scp( {}, $self->{profile_path}, 'profile path' ); } else { $handle = FileHandle->new( $self->{profile_path}, Fcntl::O_RDONLY() ) or Firefox::Marionette::Exception->throw( "Failed to open '$self->{profile_path}' for reading:$EXTENDED_OS_ERROR" ); } my $profile = Firefox::Marionette::Profile->parse_by_handle($handle); close $handle or Firefox::Marionette::Exception->throw( "Failed to close '$self->{profile_path}':$EXTENDED_OS_ERROR"); if ( $self->_is_auto_listen_okay() ) { $profile->set_value( 'marionette.port', Firefox::Marionette::Profile::ANY_PORT() ); } else { my $port = $self->_get_empty_port(); $profile->set_value( 'marionette.defaultPrefs.port', $port ); $profile->set_value( 'marionette.port', $port ); } if ( $self->_ssh() ) { $self->_save_profile_via_ssh($profile); } else { $profile->save( $self->{profile_path} ); } return; } sub update { my ( $self, $update_timeout ) = @_; my $timeouts = $self->timeouts(); my $script_timeout = $timeouts->script(); my $update_timeouts = Firefox::Marionette::Timeouts->new( script => ( $update_timeout || _DEFAULT_UPDATE_TIMEOUT() ) * _MILLISECONDS_IN_ONE_SECOND(), implicit => $timeouts->implicit(), page_load => $timeouts->page_load() ); $self->timeouts($update_timeouts); my $old = $self->_context('chrome'); # toolkit/mozapps/update/nsIUpdateService.idl my $update_parameters = $self->script( $self->_compress_script(<<'_JS_') ); let branch = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefService).getBranch("app.update."); let disabledForTesting = branch.getBoolPref("disabledForTesting"); branch.setBoolPref("disabledForTesting", false); let updateManager = new Promise((resolve, reject) => { var updateStatus = {}; if ("@mozilla.org/updates/update-manager;1" in Components.classes) { let PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx"; let PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never"; if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) { Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX); } if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) { Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER); } let updateService = Components.classes["@mozilla.org/updates/update-service;1"].getService(Components.interfaces.nsIApplicationUpdateService); let latestUpdate = null; if (!updateService.canCheckForUpdates) { updateStatus["updateStatusCode"] = 'CANNOT_CHECK_FOR_UPDATES'; reject(updateStatus); } if (!updateService.canApplyUpdates) { updateStatus["updateStatusCode"] = 'CANNOT_APPLY_UPDATES'; reject(updateStatus); } if (updateService.canUsuallyStageUpdates) { if (!updateService.canStageUpdates) { updateStatus["updateStatusCode"] = 'CANNOT_STAGE_UPDATES'; reject(updateStatus); } } if ((updateService.isOtherInstanceHandlingUpdates) && (updateService.isOtherInstanceHandlingUpdates())) { updateStatus["updateStatusCode"] = 'ANOTHER_INSTANCE_IS_HANDLING_UPDATES'; reject(updateStatus); } let updateChecker = Components.classes["@mozilla.org/updates/update-checker;1"].createInstance(Components.interfaces.nsIUpdateChecker); if (updateChecker.stopCurrentCheck) { updateChecker.stopCurrentCheck(); } let updateServiceListener = { onCheckComplete: (request, updates) => { latestUpdate = updateService.selectUpdate(updates, true); updateStatus["numberOfUpdates"] = updates.length; if (latestUpdate === null) { updateStatus["updateStatusCode"] = 'NO_UPDATES_AVAILABLE'; reject(updateStatus); } else { for (key in latestUpdate) { if (typeof latestUpdate[key] !== 'function') { updateStatus[key] = latestUpdate[key]; } } let result = updateService.downloadUpdate(latestUpdate, false); let updateProcessor = Components.classes["@mozilla.org/updates/update-processor;1"].createInstance(Components.interfaces.nsIUpdateProcessor); if (updateProcessor.fixUpdateDirectoryPermissions) { updateProcessor.fixUpdateDirectoryPermissions(true); } updateProcessor.processUpdate(latestUpdate); let previousState = null; function nowPending() { if ((latestUpdate.state) && ((previousState == null) || (previousState != latestUpdate.state))) { console.log("Update status is now " + latestUpdate.state); } previousState = latestUpdate.state; updateStatus["state"] = latestUpdate.state; updateStatus["statusText"] = latestUpdate.statusText; if ((latestUpdate.state == 'pending') || (latestUpdate.state == 'pending-service')) { updateStatus["updateStatusCode"] = 'SUCCESSFUL_UPDATE'; resolve(updateStatus); } else { setTimeout(function() { nowPending() }, 500); } } setTimeout(function() { nowPending() }, 500); } }, onError: (request, update) => { updateStatus["updateStatusCode"] = 'UPDATE_SERVER_ERROR'; reject(updateStatus); }, QueryInterface: (ChromeUtils.generateQI ? ChromeUtils.generateQI([Components.interfaces.nsIUpdateCheckListener]) : XPCOMUtils.generateQI([Components.interfaces.nsIUpdateCheckListener])), }; updateChecker.checkForUpdates(updateServiceListener, true); } else { updateStatus["updateStatusCode"] = 'UPDATE_MANAGER_DISABLED'; reject(updateStatus); } }); let updateStatus = (async function() { return await updateManager.then(function(updateStatus) { return updateStatus }, function(updateStatus) { return updateStatus }); })(); branch.setBoolPref("disabledForTesting", disabledForTesting); return updateStatus; _JS_ $self->_context($old); $self->timeouts($timeouts); my %mapping = ( updateStatusCode => 'update_status_code', installDate => 'install_date', statusText => 'status_text', appVersion => 'app_version', displayVersion => 'display_version', promptWaitTime => 'prompt_wait_time', buildID => 'build_id', previousAppVersion => 'previous_app_version', patchCount => 'patch_count', serviceURL => 'service_url', selectedPatch => 'selected_patch', numberOfUpdates => 'number_of_updates', detailsURL => 'details_url', elevationFailure => 'elevation_failure', isCompleteUpdate => 'is_complete_update', errorCode => 'error_code', state => 'update_state', ); foreach my $key ( sort { $a cmp $b } keys %{$update_parameters} ) { if ( defined $mapping{$key} ) { $update_parameters->{ $mapping{$key} } = delete $update_parameters->{$key}; } } my $update_status = Firefox::Marionette::UpdateStatus->new( %{$update_parameters} ); if ( $update_status->successful() ) { $self->restart(); } return $update_status; } sub _strip_pem_prefix_whitespace_and_postfix { my ( $self, $pem_encoded_string ) = @_; my $stripped_certificate; if ( ( $pem_encoded_string =~ s/^\-{5}BEGIN[ ]CERTIFICATE\-{5}\s*//smx ) && ( $pem_encoded_string =~ s/\s*\-{5}END[ ]CERTIFICATE\-{5}\s*//smx ) ) { $stripped_certificate = join q[], split /\s+/smx, $pem_encoded_string; } else { Firefox::Marionette::Exception->throw( 'Certificate must be PEM encoded'); } return $stripped_certificate; } sub add_certificate { my ( $self, %parameters ) = @_; my $trust = $parameters{trust} ? $parameters{trust} : _DEFAULT_CERT_TRUST(); my $import_certificate; if ( $parameters{string} ) { $import_certificate = $self->_strip_pem_prefix_whitespace_and_postfix( $parameters{string} ); } elsif ( $parameters{path} ) { my $pem_encoded_certificate = $self->_read_certificate_from_disk( $parameters{path} ); $import_certificate = $self->_strip_pem_prefix_whitespace_and_postfix( $pem_encoded_certificate); } else { Firefox::Marionette::Exception->throw( 'No certificate has been supplied. Please use the string or path parameters' ); } $self->_import_certificate( $import_certificate, $trust ); return $self; } sub _certificate_interface_preamble { my ($self) = @_; return <<'_JS_'; let certificateNew = Components.classes["@mozilla.org/security/x509certdb;1"].getService(Components.interfaces.nsIX509CertDB); let certificateDatabase = certificateNew; try { certificateDatabase = Components.classes["@mozilla.org/security/x509certdb;1"].getService(Components.interfaces.nsIX509CertDB2); } catch (e) { } _JS_ } sub _import_certificate { my ( $self, $certificate, $trust ) = @_; # security/manager/ssl/nsIX509CertDB.idl my $old = $self->_context('chrome'); my $encoded_certificate = URI::Escape::uri_escape($certificate); my $encoded_trust = URI::Escape::uri_escape($trust); my $result = $self->script( $self->_compress_script( $self->_certificate_interface_preamble() . <<"_JS_") ); certificateDatabase.addCertFromBase64(decodeURIComponent("$encoded_certificate"), decodeURIComponent("$encoded_trust"), ""); _JS_ $self->_context($old); return $result; } sub certificate_as_pem { my ( $self, $certificate ) = @_; # security/manager/ssl/nsIX509CertDB.idl # security/manager/ssl/nsIX509Cert.idl my $encoded_db_key = URI::Escape::uri_escape( $certificate->db_key() ); my $old = $self->_context('chrome'); my $certificate_base64_string = MIME::Base64::encode_base64( ( pack 'C*', @{ $self->script( $self->_compress_script( $self->_certificate_interface_preamble() . <<"_JS_") ) } ), q[] ); return certificateDatabase.findCertByDBKey(decodeURIComponent("$encoded_db_key"), {}).getRawDER({}); _JS_ $self->_context($old); my $certificate_in_pem_form = "-----BEGIN CERTIFICATE-----\n" . ( join "\n", unpack '(A64)*', $certificate_base64_string ) . "\n-----END CERTIFICATE-----\n"; return $certificate_in_pem_form; } sub delete_certificate { my ( $self, $certificate ) = @_; # security/manager/ssl/nsIX509CertDB.idl my $encoded_db_key = URI::Escape::uri_escape( $certificate->db_key() ); my $old = $self->_context('chrome'); my $certificate_base64_string = $self->script( $self->_compress_script( $self->_certificate_interface_preamble() . <<"_JS_") ); let certificate = certificateDatabase.findCertByDBKey(decodeURIComponent("$encoded_db_key"), {}); return certificateDatabase.deleteCertificate(certificate); _JS_ $self->_context($old); return $self; } sub certificates { my ($self) = @_; my $old = $self->_context('chrome'); my $certificates = $self->script( $self->_compress_script( $self->_certificate_interface_preamble() . <<'_JS_') ); let result = certificateDatabase.getCerts(); if (Array.isArray(result)) { return result; } else { let certEnum = result.getEnumerator(); let certificates = new Array(); while(certEnum.hasMoreElements()) { certificates.push(certEnum.getNext().QueryInterface(Components.interfaces.nsIX509Cert)); } return certificates; } _JS_ $self->_context($old); my @certificates; foreach my $certificate ( @{$certificates} ) { push @certificates, Firefox::Marionette::Certificate->new($certificate); } return @certificates; } sub _read_certificate_from_disk { my ( $self, $path ) = @_; my $handle = FileHandle->new( $path, Fcntl::O_RDONLY() ) or Firefox::Marionette::Exception->throw( "Failed to open certificate '$path' for reading:$EXTENDED_OS_ERROR"); my $certificate = $self->_read_and_close_handle( $handle, $path ); return $certificate; } sub _read_certificates_from_disk { my ( $self, $trust ) = @_; my @certificates; if ($trust) { if ( ref $trust ) { foreach my $path ( @{$trust} ) { my $certificate = $self->_read_certificate_from_disk($path); push @certificates, $certificate; } } else { my $certificate = $self->_read_certificate_from_disk($trust); push @certificates, $certificate; } } return @certificates; } sub _launch_and_connect { my ( $self, %parameters ) = @_; my ( $session_id, $capabilities ); if ( $parameters{reconnect} ) { ( $session_id, $capabilities ) = $self->_reconnect(%parameters); } else { my @certificates = $self->_read_certificates_from_disk( $parameters{trust} ); my @arguments = $self->_setup_arguments(%parameters); $self->_import_profile_paths(%parameters); $self->_launch(@arguments); my $socket = $self->_setup_local_connection_to_firefox(@arguments); ( $session_id, $capabilities ) = $self->_initial_socket_setup( $socket, $parameters{capabilities} ); foreach my $certificate (@certificates) { $self->add_certificate( string => $certificate, trust => _DEFAULT_CERT_TRUST() ); } } return ( $session_id, $capabilities ); } sub _check_protocol_version_and_pid { my ( $self, $session_id, $capabilities ) = @_; if ( ($session_id) && ($capabilities) && ( ref $capabilities ) ) { } elsif (( $self->marionette_protocol() <= _MARIONETTE_PROTOCOL_VERSION_3() ) && ($capabilities) && ( ref $capabilities ) ) { } else { Firefox::Marionette::Exception->throw( 'Failed to correctly setup the Firefox process'); } if ( $self->marionette_protocol() < _MARIONETTE_PROTOCOL_VERSION_3() ) { } else { $self->_check_initial_firefox_pid($capabilities); } return; } sub _post_launch_checks_and_setup { my ( $self, $timeouts ) = @_; $self->_write_local_proxy( $self->_ssh() ); if ( defined $timeouts ) { $self->timeouts($timeouts); } if ( $self->{_har} ) { $self->_build_local_extension_directory(); my $path = File::Spec->catfile( $self->{_local_extension_directory}, 'har_export_trigger-0.6.1-an+fx.xpi' ); my $handle = FileHandle->new( $path, Fcntl::O_WRONLY() | Fcntl::O_CREAT() | Fcntl::O_EXCL(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR() ) or Firefox::Marionette::Exception->throw( "Failed to open '$path' for writing:$EXTENDED_OS_ERROR"); binmode $handle; $handle->print( MIME::Base64::decode_base64( Firefox::Marionette::Extension::HarExportTrigger->as_string() ) ) or Firefox::Marionette::Exception->throw( "Failed to write to '$path':$EXTENDED_OS_ERROR"); $handle->close() or Firefox::Marionette::Exception->throw( "Failed to close '$path':$EXTENDED_OS_ERROR"); $self->install( $path, 0 ); } return; } sub new { my ( $class, %parameters ) = @_; my $self = $class->_init(%parameters); my ( $session_id, $capabilities ) = $self->_launch_and_connect(%parameters); $self->_check_protocol_version_and_pid( $session_id, $capabilities ); my $timeouts = $self->_build_timeout_from_parameters(%parameters); $self->_post_launch_checks_and_setup($timeouts); return $self; } sub _check_initial_firefox_pid { my ( $self, $capabilities ) = @_; my $firefox_pid = $capabilities->moz_process_id(); if ( $self->_ssh() ) { } elsif ( ( $OSNAME eq 'cygwin' ) || ( $OSNAME eq 'MSWin32' ) ) { } elsif ( defined $firefox_pid ) { if ( $self->_firefox_pid() != $firefox_pid ) { Firefox::Marionette::Exception->throw( 'Failed to correctly determine the Firefox process id through the initial connection capabilities' ); } } if ( defined $firefox_pid ) { $self->{_firefox_pid} = $firefox_pid; } return; } sub _build_local_extension_directory { my ($self) = @_; if ( !$self->{_local_extension_directory} ) { my $root_directory; if ( $self->_ssh() ) { $root_directory = $self->ssh_local_directory(); } else { $root_directory = $self->_root_directory(); } $self->{_local_extension_directory} = File::Spec->catdir( $root_directory, 'extension' ); mkdir $self->{_local_extension_directory}, Fcntl::S_IRWXU() or ( $OS_ERROR == POSIX::EEXIST() ) or Firefox::Marionette::Exception->throw( "Failed to create directory $self->{_local_extension_directory}:$EXTENDED_OS_ERROR" ); } return; } sub _clean_local_extension_directory { my ($self) = @_; if ( $self->{_local_extension_directory} ) { # manual clearing of the directory to aid with win32 idiocy my $handle = DirHandle->new( $self->{_local_extension_directory} ) or Firefox::Marionette::Exception->throw( "Failed to open directory '$self->{_local_extension_directory}':$EXTENDED_OS_ERROR" ); my $cleaned = 1; while ( my $entry = $handle->read() ) { next if ( $entry eq File::Spec->updir() ); next if ( $entry eq File::Spec->curdir() ); my $path = File::Spec->catfile( $self->{_local_extension_directory}, $entry ); unlink $path or $cleaned = 0; } $handle->close() or Firefox::Marionette::Exception->throw( "Failed to close directory '$self->{_local_extension_directory}':$EXTENDED_OS_ERROR" ); if ($cleaned) { delete $self->{_local_extension_directory}; } } return; } sub har { my ($self) = @_; my $context = $self->_context('content'); my $log = $self->script(<<'_JS_'); return (async function() { return await HAR.triggerExport() })(); _JS_ $self->_context($context); return { log => $log }; } sub _build_timeout_from_parameters { my ( $self, %parameters ) = @_; my $timeouts; if ( ( defined $parameters{implicit} ) || ( defined $parameters{page_load} ) || ( defined $parameters{script} ) ) { my $page_load = defined $parameters{page_load} ? $parameters{page_load} : _DEFAULT_PAGE_LOAD_TIMEOUT(); my $script = defined $parameters{script} ? $parameters{script} : _DEFAULT_SCRIPT_TIMEOUT(); my $implicit = defined $parameters{implicit} ? $parameters{implicit} : _DEFAULT_IMPLICIT_TIMEOUT(); $timeouts = Firefox::Marionette::Timeouts->new( page_load => $page_load, script => $script, implicit => $implicit, ); } elsif ( $parameters{timeouts} ) { $timeouts = $parameters{timeouts}; } return $timeouts; } sub _check_addons { my ( $self, %parameters ) = @_; $self->{addons} = 1; my @arguments = (); if ( $self->{_har} ) { } elsif ( $parameters{nightly} ) { # safe-mode will disable loading extensions in nightly } elsif ( !$parameters{addons} ) { if ( $self->_is_safe_mode_okay() ) { push @arguments, '-safe-mode'; $self->{addons} = 0; } } return @arguments; } sub _check_visible { my ( $self, %parameters ) = @_; my @arguments = (); if ( ( defined $parameters{capabilities} ) && ( defined $parameters{capabilities}->moz_headless() ) && ( !$parameters{capabilities}->moz_headless() ) ) { if ( !$parameters{visible} ) { Carp::carp('Unable to launch firefox with -headless option'); } $self->{visible} = 1; } elsif ( $parameters{visible} ) { $self->{visible} = 1; } else { if ( $self->_is_headless_okay() ) { push @arguments, '-headless'; $self->{visible} = 0; } elsif (( $OSNAME eq 'MSWin32' ) || ( $OSNAME eq 'darwin' ) || ( $OSNAME eq 'cygwin' ) || ( $self->_ssh() ) ) { } else { if ( $self->_is_xvfb_okay() && $self->_xvfb_exists() && $self->_launch_xvfb_if_not_present() ) { $self->{_launched_xvfb_anyway} = 1; $self->{visible} = 0; } else { Carp::carp('Unable to launch firefox with -headless option'); $self->{visible} = 1; } } } $self->_launch_xvfb_if_required(); return @arguments; } sub _launch_xvfb_if_required { my ($self) = @_; if ( $self->{visible} ) { if ( ( $OSNAME eq 'MSWin32' ) || ( $OSNAME eq 'darwin' ) || ( $OSNAME eq 'cygwin' ) || ( $self->_ssh() ) || ( $ENV{DISPLAY} ) || ( $self->{_launched_xvfb_anyway} ) ) { } elsif ( $self->_xvfb_exists() && $self->_launch_xvfb_if_not_present() ) { $self->{_launched_xvfb_anyway} = 1; } } return; } sub _setup_arguments { my ( $self, %parameters ) = @_; my @arguments = qw(-marionette); if ( defined $self->{window_width} ) { push @arguments, '-width', $self->{window_width}; } if ( defined $self->{window_height} ) { push @arguments, '-height', $self->{window_height}; } if ( defined $self->{console} ) { push @arguments, '--jsconsole'; } push @arguments, $self->_check_addons(%parameters); push @arguments, $self->_check_visible(%parameters); if ( $parameters{restart} ) { my $profile_directory = $self->{_profile_directory}; if ( $OSNAME eq 'cygwin' ) { $profile_directory = $self->execute( 'cygpath', '-s', '-m', $profile_directory ); } push @arguments, ( '-profile', $profile_directory, '--no-remote', '--new-instance' ); } elsif ( $parameters{profile_name} ) { $self->{profile_name} = $parameters{profile_name}; $self->{_profile_directory} = Firefox::Marionette::Profile->directory( $parameters{profile_name} ); $self->{profile_path} = File::Spec->catfile( $self->{_profile_directory}, 'prefs.js' ); push @arguments, ( '-P', $self->{profile_name} ); } else { my $profile_directory = $self->_setup_new_profile( $parameters{profile}, %parameters ); if ( $self->_ssh() ) { if ( $self->_remote_uname() eq 'cygwin' ) { $profile_directory = $self->_execute_via_ssh( {}, 'cygpath', '-s', '-m', $profile_directory ); chomp $profile_directory; } } elsif ( $OSNAME eq 'cygwin' ) { $profile_directory = $self->execute( 'cygpath', '-s', '-m', $profile_directory ); } my $mime_types_content = $self->_mime_types_content(); if ( $self->_ssh() ) { $self->_write_mime_types_via_ssh($mime_types_content); } else { my $path = File::Spec->catfile( $profile_directory, 'mimeTypes.rdf' ); my $handle = FileHandle->new( $path, Fcntl::O_WRONLY() | Fcntl::O_CREAT() | Fcntl::O_EXCL(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR() ) or Firefox::Marionette::Exception->throw( "Failed to open '$path' for writing:$EXTENDED_OS_ERROR"); $handle->print($mime_types_content) or Firefox::Marionette::Exception->throw( "Failed to write to '$path':$EXTENDED_OS_ERROR"); $handle->close or Firefox::Marionette::Exception->throw( "Failed to close '$path':$EXTENDED_OS_ERROR"); } push @arguments, ( '-profile', $profile_directory, '--no-remote', '--new-instance' ); } if ( ( $self->{_har} ) || ( $parameters{devtools} ) ) { push @arguments, '--devtools'; } if ( $parameters{kiosk} ) { push @arguments, '--kiosk'; } return @arguments; } sub _mime_types_content { my ($self) = @_; my $mime_types_content = <<'_RDF_'; _RDF_ foreach my $mime_type ( @{ $self->{mime_types} } ) { $mime_types_content .= <<'_RDF_'; _RDF_ } $mime_types_content .= <<'_RDF_'; _RDF_ foreach my $mime_type ( @{ $self->{mime_types} } ) { $mime_types_content .= <<'_RDF_'; _RDF_ } $mime_types_content .= <<'_RDF_'; _RDF_ return $mime_types_content; } sub _write_mime_types_via_ssh { my ( $self, $mime_types_content ) = @_; my $handle = File::Temp::tempfile( File::Spec->catfile( File::Spec->tmpdir(), 'firefox_marionette_mime_type_data_XXXXXXXXXXX' ) ) or Firefox::Marionette::Exception->throw( "Failed to open temporary file for writing:$EXTENDED_OS_ERROR"); print {$handle} $mime_types_content or Firefox::Marionette::Exception->throw( "Failed to write to temporary file:$EXTENDED_OS_ERROR"); seek $handle, 0, Fcntl::SEEK_SET() or Firefox::Marionette::Exception->throw( "Failed to seek to start of temporary file:$EXTENDED_OS_ERROR"); $self->_put_file_via_scp( $handle, $self->_remote_catfile( $self->{_profile_directory}, 'mimeTypes.rdf' ), 'mime type data' ); return; } sub _is_firefox_major_version_at_least { my ( $self, $minimum_version ) = @_; $self->_initialise_version(); if ( ( defined $self->{_initial_version} ) && ( $self->{_initial_version}->{major} ) && ( $self->{_initial_version}->{major} >= $minimum_version ) ) { return 1; } elsif ( defined $self->{_initial_version} ) { return 0; } else { return 1; # assume modern non-firefox branded browser } } sub _is_xvfb_okay { my ($self) = @_; if ( $self->_is_firefox_major_version_at_least( _MIN_VERSION_FOR_XVFB() ) ) { return 1; } else { return 0; } } sub _is_script_missing_args_okay { my ($self) = @_; if ( $self->_is_firefox_major_version_at_least( _MIN_VERSION_FOR_SCRIPT_WO_ARGS() ) ) { return 1; } else { return 0; } } sub _is_script_script_parameter_okay { my ($self) = @_; if ( $self->_is_firefox_major_version_at_least( _MIN_VERSION_FOR_SCRIPT_SCRIPT() ) ) { return 1; } else { return 0; } } sub _is_using_webdriver_ids_exclusively { my ($self) = @_; if ( $self->_is_firefox_major_version_at_least( _MIN_VERSION_FOR_WEBDRIVER_IDS() ) ) { return 1; } else { return 0; } } sub _is_new_hostport_okay { my ($self) = @_; if ( $self->_is_firefox_major_version_at_least( _MIN_VERSION_FOR_HOSTPORT_PROXY() ) ) { return 1; } else { return 0; } } sub _is_new_sendkeys_okay { my ($self) = @_; if ( $self->_is_firefox_major_version_at_least( _MIN_VERSION_FOR_NEW_SENDKEYS() ) ) { return 1; } else { return 0; } } sub _is_safe_mode_okay { my ($self) = @_; if ( $self->_is_firefox_major_version_at_least( _MIN_VERSION_FOR_SAFE_MODE() ) ) { if ( $self->{pale_moon} ) { return 0; } else { return 1; } } else { return 0; } } sub _is_headless_okay { my ($self) = @_; my $min_version = _MIN_VERSION_FOR_HEADLESS(); if ( ( $OSNAME eq 'MSWin32' ) || ( $OSNAME eq 'darwin' ) ) { $min_version = _MIN_VERSION_FOR_WD_HEADLESS(); } if ( $self->_is_firefox_major_version_at_least($min_version) ) { return 1; } else { return 0; } } sub _is_auto_listen_okay { my ($self) = @_; if ( $self->_is_firefox_major_version_at_least( _MIN_VERSION_FOR_AUTO_LISTEN() ) ) { return 1; } else { return 0; } } sub execute { my ( $self, $binary, @arguments ) = @_; if ( my $ssh = $self->_ssh() ) { my $parameters = {}; if ( !defined $ssh->{ssh_connections_to_host} ) { $parameters->{accept_new} = 1; } if ( !$ssh->{control_established} ) { $parameters->{master} = 1; } if ( !defined $ssh->{first_ssh_connection_to_host} ) { $ssh->{ssh_connections_to_host} = 1; } else { $ssh->{ssh_connections_to_host} += 1; } my $return_code = $self->_execute_via_ssh( $parameters, $binary, @arguments ); if ( ($return_code) && ( $ssh->{use_control_path} ) ) { $ssh->{control_established} = 1; } return $return_code; } else { if ( $self->debug() ) { warn q[** ] . ( join q[ ], $binary, @arguments ) . "\n"; } my ( $writer, $reader, $error ); my $pid; eval { $pid = IPC::Open3::open3( $writer, $reader, $error, $binary, @arguments ); } or do { Firefox::Marionette::Exception->throw( "Failed to execute '$binary':$EXTENDED_OS_ERROR"); }; my ( $result, $output ); while ( $result = read $reader, my $buffer, _READ_LENGTH_OF_OPEN3_OUTPUT() ) { $output .= $buffer; } defined $result or Firefox::Marionette::Exception->throw( q[Failed to read STDOUT from '] . ( join q[ ], $binary, @arguments ) . "':$EXTENDED_OS_ERROR" ); if ( defined $output ) { chomp $output; $output =~ s/\r$//smx; } waitpid $pid, 0; if ( $CHILD_ERROR == 0 ) { return $output; } else { Firefox::Marionette::Exception->throw( q[Failed to execute '] . ( join q[ ], $binary, @arguments ) . q[':] . $self->_error_message( $binary, $CHILD_ERROR ) ); } return; } } sub _adb_initialise { my ($self) = @_; $self->execute( 'adb', 'connect', $self->_adb()->{host} ); my $adb_regex = qr/package:(.*(firefox|fennec).*)/smx; my $binary = 'adb'; my @arguments = qw(shell pm list packages); my $package_name; foreach my $line ( split /\r?\n/smx, $self->execute( $binary, @arguments ) ) { if ( $line =~ /^$adb_regex$/smx ) { $package_name = $1; } } return $package_name; } sub _execute_via_ssh { my ( $self, $parameters, $binary, @arguments ) = @_; my $ssh_binary = 'ssh'; my @ssh_arguments = ( $self->_ssh_arguments( %{$parameters} ), $self->_ssh_address() ); my $output = $self->_get_local_command_output( $parameters, $ssh_binary, @ssh_arguments, $binary, @arguments ); return $output; } sub _read_and_close_handle { my ( $self, $handle, $path ) = @_; my $content; my $result; while ( $result = $handle->read( my $buffer, _LOCAL_READ_BUFFER_SIZE() ) ) { $content .= $buffer; } defined $result or Firefox::Marionette::Exception->throw( "Failed to read from '$path':$EXTENDED_OS_ERROR"); $handle->close() or Firefox::Marionette::Exception->throw( "Failed to close '$path':$EXTENDED_OS_ERROR"); return $content; } sub _search_for_version_in_application_ini { my ( $self, $binary ) = @_; my $binary_directory = $self->_binary_directory(); if ( defined $binary_directory ) { my $found_active_update; foreach my $entry ( $self->_directory_listing( {}, $binary_directory, 1 ) ) { if ( $entry eq 'active-update.xml' ) { $found_active_update = 1; } } my ( $active_update_handle, $active_update_path ); my $active_update_version; if ($found_active_update) { if ( $self->_ssh() ) { $active_update_path = $self->_remote_catfile( $binary_directory, 'active-update.xml' ); $active_update_handle = $self->_get_file_via_scp( { ignore_missing_file => 1 }, $active_update_path, 'active-update.xml' ); } else { $active_update_path = File::Spec->catdir( $binary_directory, 'active-update.xml' ); $active_update_handle = FileHandle->new( $active_update_path, Fcntl::O_RDONLY() ) or Firefox::Marionette::Exception->throw( "Failed to open $active_update_path for reading:$EXTENDED_OS_ERROR" ); } if ($active_update_handle) { my $active_update_contents = $self->_read_and_close_handle( $active_update_handle, $active_update_path ); my $parser = XML::Parser->new(); $parser->setHandlers( Start => sub { my ( $p, $element, %attributes ) = @_; if ( $element eq 'update' ) { $active_update_version = $attributes{appVersion}; } }, ); $parser->parse($active_update_contents); } } my $application_ini_path = File::Spec->catfile( $binary_directory, 'application.ini' ); my $application_ini_handle = FileHandle->new( $application_ini_path, Fcntl::O_RDONLY() ); if ($application_ini_handle) { my $config = Config::INI::Reader->read_handle($application_ini_handle); if ( my $app = $config->{App} ) { if ( ( $app->{SourceRepository} ) && ( $app->{SourceRepository} eq 'https://hg.mozilla.org/releases/mozilla-beta' ) ) { $self->{developer_edition} = 1; } return join q[ ], $app->{Vendor}, $app->{Name}, $active_update_version || $app->{Version}; } } } return; } sub _get_version_string { my ( $self, $binary ) = @_; my $version_string; if ( $version_string = $self->_search_for_version_in_application_ini($binary) ) { } elsif ( $self->_ssh() ) { $version_string = $self->execute( q["] . $binary . q["], '--version' ); $version_string =~ s/\r?\n$//smx; } else { $version_string = $self->execute( $binary, '--version' ); $version_string =~ s/\r?\n$//smx; } return $version_string; } sub _initialise_version { my ($self) = @_; if ( defined $self->{_initial_version} ) { } else { $self->_get_version(); } return; } sub _get_version { my ($self) = @_; my $binary = $self->_binary(); $self->{binary} = $binary; my $version_string; my $version_regex = qr/(\d+)[.](\d+(?:\w\d+)?)(?:[.](\d+))*/smx; if ( $self->_adb() ) { my $package_name = $self->_adb_initialise(); my $dumpsys = $self->execute( 'adb', 'shell', 'dumpsys', 'package', $package_name ); my $found; foreach my $line ( split /\r?\n/smx, $dumpsys ) { if ( $line =~ /^[ ]+versionName=$version_regex\s*$/smx ) { $found = 1; $self->{_initial_version}->{major} = $1; $self->{_initial_version}->{minor} = $2; $self->{_initial_version}->{patch} = $3; } } if ( !$found ) { Firefox::Marionette::Exception->throw( "'adb shell dumpsys package $package_name' did not produce output that looks like '^[ ]+versionName=\\d+[.]\\d+([.]\\d+)?\\s*\$':$version_string" ); } } else { $version_string = $self->_get_version_string($binary); my $browser_regex = join q[|], qr/Mozilla[ ]Firefox[ ]/smx, qr/Waterfox[ ]Waterfox[ ]/smx, qr/Moonchild[ ]Productions[ ]Basilisk[ ]/smx, qr/Moonchild[ ]Productions[ ]Pale[ ]Moon[ ]/smx; if ( $version_string =~ /(${browser_regex})${version_regex}[[:alpha:]]*\s*$/smx ) # not anchoring the start of the regex b/c of issues with # RHEL6 and dbus crashing with error messages like # 'Failed to open connection to "session" message bus: /bin/dbus-launch terminated abnormally without any error message' { if ( $1 eq 'Moonchild Productions Pale Moon ' ) { $self->{pale_moon} = 1; $self->{_initial_version}->{major} = _PALEMOON_VERSION_EQUIV(); } elsif ( $1 eq 'Waterfox Waterfox ' ) { $self->{waterfox} = 1; } else { $self->{_initial_version}->{major} = $2; $self->{_initial_version}->{minor} = $3; $self->{_initial_version}->{patch} = $4; } } elsif ( defined $self->{_initial_version} ) { } elsif ( $version_string =~ /^Waterfox[ ]/smx ) { $self->{waterfox} = 1; if ( $version_string =~ /^Waterfox Classic/smx ) { $self->{_initial_version}->{major} = _WATERFOX_CLASSIC_VERSION_EQUIV(); } else { $self->{_initial_version}->{major} = _WATERFOX_CURRENT_VERSION_EQUIV(); } } else { Carp::carp( "'$binary --version' did not produce output that could be parsed. Assuming modern Marionette is available" ); } } $self->_validate_any_requested_version( $binary, $version_string ); return; } sub _validate_any_requested_version { my ( $self, $binary, $version_string ) = @_; if ( $self->{requested_version}->{nightly} ) { if ( !$self->nightly() ) { Firefox::Marionette::Exception->throw( "$version_string is not a nightly firefox release"); } } elsif ( $self->{requested_version}->{developer} ) { if ( !$self->developer() ) { Firefox::Marionette::Exception->throw( "$version_string is not a developer firefox release"); } } elsif ( $self->{requested_version}->{waterfox} ) { if ( $self->{binary} !~ /waterfox(?:[.]exe)?$/smx ) { Firefox::Marionette::Exception->throw( "$binary is not a waterfox binary"); } } return; } sub debug { my ( $self, $new ) = @_; my $old = $self->{debug}; if ( defined $new ) { $self->{debug} = $new; } return $old; } sub _visible { my ($self) = @_; return $self->{visible}; } sub _firefox_pid { my ($self) = @_; return $self->{_firefox_pid}; } sub _local_ssh_pid { my ($self) = @_; return $self->{_local_ssh_pid}; } sub _get_full_short_path_for_win32_binary { my ( $self, $binary ) = @_; if ( File::Spec->file_name_is_absolute($binary) ) { return $binary; } else { foreach my $directory ( split /;/smx, $ENV{Path} ) { my $possible_path = File::Spec->catfile( $directory, $binary . q[.exe] ); if ( -e $possible_path ) { my $path = Win32::GetShortPathName($possible_path); return $path; } } } return; } sub _firefox_tmp_directory { my ($self) = @_; my $tmp_directory; if ( $self->_ssh() ) { $tmp_directory = $self->_remote_firefox_tmp_directory(); } else { $tmp_directory = $self->_local_firefox_tmp_directory(); } return $tmp_directory; } sub _quoting_for_cmd_exe { my (@unquoted_arguments) = @_; my @quoted_arguments; foreach my $unquoted_argument (@unquoted_arguments) { $unquoted_argument =~ s/\\"/\\\\"/smxg; $unquoted_argument =~ s/"/""/smxg; push @quoted_arguments, q["] . $unquoted_argument . q["]; } return join q[ ], @quoted_arguments; } sub _win32_process_create_wrapper { my ( $self, $full_path, $command_line ) = @_; open STDIN, q[<], File::Spec->devnull() or Firefox::Marionette::Exception->throw( "Failed to redirect STDIN to nul:$EXTENDED_OS_ERROR"); open STDOUT, q[>], File::Spec->devnull() or Firefox::Marionette::Exception->throw( "Failed to redirect STDOUT to nul:$EXTENDED_OS_ERROR"); local $ENV{TMPDIR} = $self->_firefox_tmp_directory(); my $result = Win32::Process::Create( my $process, $full_path, $command_line, _WIN32_PROCESS_INHERIT_FLAGS(), Win32::Process::NORMAL_PRIORITY_CLASS(), q[.] ); return ( $process, $result ); } sub _save_stdin { my ($self) = @_; open my $local_stdin, q[<&], fileno STDIN or Firefox::Marionette::Exception->throw( "Failed to save STDIN:$EXTENDED_OS_ERROR"); return $local_stdin; } sub _save_stdout { open my $local_stdout, q[>&], fileno STDOUT or Firefox::Marionette::Exception->throw( "Failed to save STDOUT:$EXTENDED_OS_ERROR"); return $local_stdout; } sub _restore_stdin_stdout { my ( $self, $local_stdin, $local_stdout ) = @_; open STDIN, q[<&], fileno $local_stdin or Firefox::Marionette::Exception->throw( "Failed to restore STDIN:$EXTENDED_OS_ERROR"); close $local_stdin or Firefox::Marionette::Exception->throw( "Failed to close saved STDIN handle:$EXTENDED_OS_ERROR"); open STDOUT, q[>&], fileno $local_stdout or Firefox::Marionette::Exception->throw( "Failed to restore STDOUT:$EXTENDED_OS_ERROR"); close $local_stdout or Firefox::Marionette::Exception->throw( "Failed to close saved STDOUT handle:$EXTENDED_OS_ERROR"); return; } sub _start_win32_process { my ( $self, $binary, @arguments ) = @_; my $full_path = $self->_get_full_short_path_for_win32_binary($binary); my $command_line = _quoting_for_cmd_exe( $binary, @arguments ); if ( $self->debug() ) { warn q[** ] . $command_line . "\n"; } my $local_stdout = $self->_save_stdout(); my $local_stdin = $self->_save_stdin(); my ( $process, $result ) = $self->_win32_process_create_wrapper( $full_path, $command_line ); $self->_restore_stdin_stdout( $local_stdin, $local_stdout ); if ( !$result ) { my $error = Win32::FormatMessage( Win32::GetLastError() ); $error =~ s/[\r\n]//smxg; $error =~ s/[.]$//smxg; chomp $error; Firefox::Marionette::Exception->throw( "Failed to create process from '$binary':$error"); } return $process; } sub _execute_win32_process { my ( $self, $binary, @arguments ) = @_; my $process = $self->_start_win32_process( $binary, @arguments ); $process->GetExitCode( my $exit_code ); while ( $exit_code == Win32::Process::STILL_ACTIVE() ) { $process->GetExitCode($exit_code); } if ( $exit_code == 0 ) { return 1; } else { return; } } sub _launch_via_ssh { my ( $self, @arguments ) = @_; if ( $OSNAME eq 'MSWin32' ) { my $ssh_binary = $self->_get_full_short_path_for_win32_binary('ssh') or Firefox::Marionette::Exception->throw( "Failed to find 'ssh' anywhere in the Path environment variable:$ENV{Path}" ); my @ssh_arguments = ( $self->_ssh_arguments( env => 1 ), $self->_ssh_address() ); my $process = $self->_start_win32_process( 'ssh', @ssh_arguments, q["] . $self->_binary() . q["], @arguments ); $self->{_win32_ssh_process} = $process; my $pid = $process->GetProcessID(); $self->{_ssh}->{pid} = $pid; return $pid; } else { my $dev_null = File::Spec->devnull(); if ( my $pid = fork ) { $self->{_ssh}->{pid} = $pid; return $pid; } elsif ( defined $pid ) { eval { open STDIN, q[<], $dev_null or Firefox::Marionette::Exception->throw( "Failed to redirect STDIN to $dev_null:$EXTENDED_OS_ERROR"); $self->_ssh_exec( $self->_ssh_arguments( env => 1 ), $self->_ssh_address(), q["] . $self->_binary() . q["], @arguments ) or Firefox::Marionette::Exception->throw( "Failed to exec 'ssh':$EXTENDED_OS_ERROR"); } or do { if ( $self->debug() ) { chomp $EVAL_ERROR; warn "$EVAL_ERROR\n"; } }; exit 1; } else { Firefox::Marionette::Exception->throw( "Failed to fork:$EXTENDED_OS_ERROR"); } } return; } sub _remote_firefox_tmp_directory { my ($self) = @_; return $self->{_remote_tmp_directory}; } sub _local_firefox_tmp_directory { my ($self) = @_; my $root_directory = $self->_root_directory(); return File::Spec->catdir( $root_directory, 'tmp' ); } sub _launch { my ( $self, @arguments ) = @_; $self->{_initial_arguments} = []; foreach my $argument (@arguments) { push @{ $self->{_initial_arguments} }, $argument; } local $ENV{XPCSHELL_TEST_PROFILE_DIR} = 1; if ( $self->_ssh() ) { $self->{_local_ssh_pid} = $self->_launch_via_ssh(@arguments); $self->_wait_for_updating_to_finish(); return; } if ( $OSNAME eq 'MSWin32' ) { local $ENV{TMPDIR} = $self->_local_firefox_tmp_directory(); $self->{_firefox_pid} = $self->_launch_win32(@arguments); } elsif (( $OSNAME ne 'darwin' ) && ( $OSNAME ne 'cygwin' ) && ( $self->_visible() ) && ( !$ENV{DISPLAY} ) && ( !$self->{_launched_xvfb_anyway} ) && ( $self->_xvfb_exists() ) && ( $self->_launch_xvfb_if_not_present() ) ) { # if not MacOS or Win32 and no DISPLAY variable, launch Xvfb if at all possible local $ENV{DISPLAY} = $self->xvfb_display(); local $ENV{XAUTHORITY} = $self->xvfb_xauthority(); local $ENV{TMPDIR} = $self->_local_firefox_tmp_directory(); $self->{_firefox_pid} = $self->_launch_unix(@arguments); } elsif ( $self->{_launched_xvfb_anyway} ) { local $ENV{DISPLAY} = $self->xvfb_display(); local $ENV{XAUTHORITY} = $self->xvfb_xauthority(); local $ENV{TMPDIR} = $self->_local_firefox_tmp_directory(); $self->{_firefox_pid} = $self->_launch_unix(@arguments); } else { local $ENV{TMPDIR} = $self->_local_firefox_tmp_directory(); $self->{_firefox_pid} = $self->_launch_unix(@arguments); } $self->_wait_for_updating_to_finish(); return; } sub _launch_win32 { my ( $self, @arguments ) = @_; my $binary = $self->_binary(); my $process = $self->_start_win32_process( $binary, @arguments ); $self->{_win32_firefox_process} = $process; return $process->GetProcessID(); } sub _xvfb_binary { return 'Xvfb'; } sub _dev_fd_works { my ($self) = @_; my $test_handle = File::Temp::tempfile( File::Spec->tmpdir(), 'firefox_marionette_dev_fd_test_XXXXXXXXXXX' ) or Firefox::Marionette::Exception->throw( "Failed to open temporary file for writing:$EXTENDED_OS_ERROR"); my @stats = stat '/dev/fd/' . fileno $test_handle; if ( scalar @stats ) { return 1; } elsif ( $OSNAME eq 'freebsd' ) { Carp::carp( q[/dev/fd is not working. Perhaps you need to mount fdescfs like so 'sudo mount -t fdescfs fdesc /dev/fd'] ); } else { Carp::carp("/dev/fd is not working for $OSNAME"); } return 0; } sub _xvfb_exists { my ($self) = @_; my $binary = $self->_xvfb_binary(); my $dev_null = File::Spec->devnull(); if ( !$self->_dev_fd_works() ) { return 0; } eval { require Crypt::URandom; } or do { Carp::carp('Unable to load Crypt::URandom'); return 0; }; if ( my $pid = fork ) { waitpid $pid, 0; if ( $CHILD_ERROR == 0 ) { return 1; } } elsif ( defined $pid ) { eval { open STDERR, q[>], $dev_null or Firefox::Marionette::Exception->throw( "Failed to redirect STDERR to $dev_null:$EXTENDED_OS_ERROR"); open STDOUT, q[>], $dev_null or Firefox::Marionette::Exception->throw( "Failed to redirect STDOUT to $dev_null:$EXTENDED_OS_ERROR"); exec {$binary} $binary, '-help' or Firefox::Marionette::Exception->throw( "Failed to exec '$binary':$EXTENDED_OS_ERROR"); } or do { if ( $self->debug() ) { chomp $EVAL_ERROR; warn "$EVAL_ERROR\n"; } }; exit 1; } else { Firefox::Marionette::Exception->throw( "Failed to fork:$EXTENDED_OS_ERROR"); } return; } sub xvfb { my ($self) = @_; Carp::carp( '**** DEPRECATED METHOD - using xvfb() HAS BEEN REPLACED BY xvfb_pid ****' ); return $self->xvfb_pid(); } sub _launch_xauth { my ( $self, $display_number ) = @_; my $auth_handle = FileHandle->new( $ENV{XAUTHORITY}, Fcntl::O_CREAT() | Fcntl::O_WRONLY() | Fcntl::O_EXCL(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR() ) or Firefox::Marionette::Exception->throw( "Failed to open '$ENV{XAUTHORITY}' for writing:$EXTENDED_OS_ERROR"); $auth_handle->close() or Firefox::Marionette::Exception->throw( "Failed to close '$ENV{XAUTHORITY}':$EXTENDED_OS_ERROR"); my $mcookie = unpack 'H*', Crypt::URandom::urandom( _NUMBER_OF_MCOOKIE_BYTES() ); my $source_handle = File::Temp::tempfile( File::Spec->tmpdir(), 'firefox_marionette_xauth_source_XXXXXXXXXXX' ) or Firefox::Marionette::Exception->throw( "Failed to open temporary file for writing:$EXTENDED_OS_ERROR"); fcntl $source_handle, Fcntl::F_SETFD(), 0 or Firefox::Marionette::Exception->throw( "Failed to clear the close-on-exec flag on a temporary file:$EXTENDED_OS_ERROR" ); my $xauth_proto = q[.]; $source_handle->print("add :$display_number $xauth_proto $mcookie\n"); seek $source_handle, 0, Fcntl::SEEK_SET() or Firefox::Marionette::Exception->throw( "Failed to seek to start of temporary file:$EXTENDED_OS_ERROR"); my $dev_null = File::Spec->devnull(); my $binary = 'xauth'; my @arguments = ( 'source', '/dev/fd/' . fileno $source_handle ); if ( $self->debug() ) { warn q[** ] . ( join q[ ], $binary, @arguments ) . "\n"; } if ( my $pid = fork ) { waitpid $pid, 0; if ( $CHILD_ERROR == 0 ) { close $source_handle or Firefox::Marionette::Exception->throw( "Failed to close temporary file:$EXTENDED_OS_ERROR"); return 1; } } elsif ( defined $pid ) { eval { if ( !$self->debug() ) { open STDERR, q[>], $dev_null or Firefox::Marionette::Exception->throw( "Failed to redirect STDERR to $dev_null:$EXTENDED_OS_ERROR" ); open STDOUT, q[>], $dev_null or Firefox::Marionette::Exception->throw( "Failed to redirect STDOUT to $dev_null:$EXTENDED_OS_ERROR" ); } exec {$binary} $binary, @arguments or Firefox::Marionette::Exception->throw( "Failed to exec '$binary':$EXTENDED_OS_ERROR"); } or do { if ( $self->debug() ) { chomp $EVAL_ERROR; warn "$EVAL_ERROR\n"; } }; exit 1; } else { Firefox::Marionette::Exception->throw( "Failed to fork:$EXTENDED_OS_ERROR"); } return; } sub xvfb_pid { my ($self) = @_; return $self->{_xvfb_pid}; } sub xvfb_display { my ($self) = @_; return ":$self->{_xvfb_display_number}"; } sub xvfb_xauthority { my ($self) = @_; return File::Spec->catfile( $self->{_xvfb_authority_directory}, 'Xauthority' ); } sub _launch_xvfb_if_not_present { my ($self) = @_; if ( ( $self->{_xvfb_pid} ) && ( kill 0, $self->{_xvfb_pid} ) ) { return 1; } else { return $self->_launch_xvfb(); } } sub _xvfb_directory { my ($self) = @_; my $root_directory = $self->_root_directory(); my $xvfb_directory = File::Spec->catdir( $root_directory, 'xvfb' ); return $xvfb_directory; } sub _debug_xvfb_execution { my ( $self, $binary, @arguments ) = @_; if ( $self->debug() ) { warn q[** ] . ( join q[ ], $binary, @arguments ) . "\n"; } return; } sub _launch_xvfb { my ($self) = @_; my $xvfb_directory = $self->_xvfb_directory(); mkdir $xvfb_directory, Fcntl::S_IRWXU() or Firefox::Marionette::Exception->throw( "Failed to create directory $xvfb_directory:$EXTENDED_OS_ERROR"); my $fbdir_directory = File::Spec->catdir( $xvfb_directory, 'fbdir' ); mkdir $fbdir_directory, Fcntl::S_IRWXU() or Firefox::Marionette::Exception->throw( "Failed to create directory $fbdir_directory:$EXTENDED_OS_ERROR"); my $display_no_path = File::Spec->catfile( $xvfb_directory, 'display_no' ); my $display_no_handle = FileHandle->new( $display_no_path, Fcntl::O_CREAT() | Fcntl::O_RDWR() | Fcntl::O_EXCL(), Fcntl::S_IWUSR() | Fcntl::S_IRUSR() ) or Firefox::Marionette::Exception->throw( "Failed to open '$display_no_path' for writing:$EXTENDED_OS_ERROR"); fcntl $display_no_handle, Fcntl::F_SETFD(), 0 or Firefox::Marionette::Exception->throw( "Failed to clear the close-on-exec flag on a temporary file:$EXTENDED_OS_ERROR" ); my $width = defined $self->{window_width} ? $self->{window_width} : _DEFAULT_WINDOW_WIDTH(); my $height = defined $self->{window_height} ? $self->{window_height} : _DEFAULT_WINDOW_HEIGHT(); my $width_height_depth = join q[x], $width, $height, _DEFAULT_DEPTH(); my @arguments = ( '-displayfd' => fileno $display_no_handle, '-screen' => '0', $width_height_depth, '-nolisten' => 'tcp', '-fbdir' => $fbdir_directory, ); my $binary = $self->_xvfb_binary(); $self->_debug_xvfb_execution( $binary, @arguments ); my $dev_null = File::Spec->devnull(); if ( my $pid = fork ) { $self->{_xvfb_pid} = $pid; my $display_number = $self->_wait_for_display_number( $pid, $display_no_handle ); if ( !defined $display_number ) { return; } $self->{_xvfb_display_number} = $display_number; close $display_no_handle or Firefox::Marionette::Exception->throw( "Failed to close temporary file:$EXTENDED_OS_ERROR"); $self->{_xvfb_authority_directory} = File::Spec->catdir( $xvfb_directory, 'xauth' ); mkdir $self->{_xvfb_authority_directory}, Fcntl::S_IRWXU() or Firefox::Marionette::Exception->throw( "Failed to create directory $self->{_xvfb_authority_directory}:$EXTENDED_OS_ERROR" ); local $ENV{DISPLAY} = $self->xvfb_display(); local $ENV{XAUTHORITY} = $self->xvfb_xauthority(); if ( $self->_launch_xauth($display_number) ) { return 1; } } elsif ( defined $pid ) { eval { if ( !$self->debug() ) { open STDERR, q[>], $dev_null or Firefox::Marionette::Exception->throw( "Failed to redirect STDERR to $dev_null:$EXTENDED_OS_ERROR" ); open STDOUT, q[>], $dev_null or Firefox::Marionette::Exception->throw( "Failed to redirect STDOUT to $dev_null:$EXTENDED_OS_ERROR" ); } exec {$binary} $binary, @arguments or Firefox::Marionette::Exception->throw( "Failed to exec '$binary':$EXTENDED_OS_ERROR"); } or do { if ( $self->debug() ) { chomp $EVAL_ERROR; warn "$EVAL_ERROR\n"; } }; exit 1; } else { Firefox::Marionette::Exception->throw( "Failed to fork:$EXTENDED_OS_ERROR"); } return; } sub _wait_for_display_number { my ( $self, $pid, $display_no_handle ) = @_; my $display_number = []; while ( $display_number !~ /^\d+$/smx ) { seek $display_no_handle, 0, Fcntl::SEEK_SET() or Firefox::Marionette::Exception->throw( "Failed to seek to start of temporary file:$EXTENDED_OS_ERROR"); defined sysread $display_no_handle, $display_number, _MAX_DISPLAY_LENGTH() or Firefox::Marionette::Exception->throw( "Failed to read from temporary file:$EXTENDED_OS_ERROR"); chomp $display_number; if ( $display_number !~ /^\d+$/smx ) { sleep 1; } waitpid $pid, POSIX::WNOHANG(); if ( !kill 0, $pid ) { Carp::carp('Xvfb has crashed before sending a display number'); return; } else { sleep 1; } } return $display_number; } sub _launch_unix { my ( $self, @arguments ) = @_; my $binary = $self->_binary(); my $pid; if ( $self->debug() ) { warn q[** ] . ( join q[ ], $binary, @arguments ) . "\n"; } if ( $OSNAME eq 'cygwin' ) { eval { $pid = IPC::Open3::open3( my $writer, my $reader, my $error, $binary, @arguments ); } or do { Firefox::Marionette::Exception->throw( "Failed to exec '$binary':$EXTENDED_OS_ERROR"); }; } else { my $dev_null = File::Spec->devnull(); if ( $pid = fork ) { } elsif ( defined $pid ) { eval { if ( !$self->debug() ) { open STDERR, q[>], $dev_null or Firefox::Marionette::Exception->throw( "Failed to redirect STDERR to $dev_null:$EXTENDED_OS_ERROR" ); open STDOUT, q[>], $dev_null or Firefox::Marionette::Exception->throw( "Failed to redirect STDOUT to $dev_null:$EXTENDED_OS_ERROR" ); } exec {$binary} $binary, @arguments or Firefox::Marionette::Exception->throw( "Failed to exec '$binary':$EXTENDED_OS_ERROR"); } or do { if ( $self->debug() ) { chomp $EVAL_ERROR; warn "$EVAL_ERROR\n"; } }; exit 1; } else { Firefox::Marionette::Exception->throw( "Failed to fork:$EXTENDED_OS_ERROR"); } } return $pid; } sub macos_binary_paths { my ($self) = @_; if ( $self->{requested_version} ) { if ( $self->{requested_version}->{nightly} ) { return ( '/Applications/Firefox Nightly.app/Contents/MacOS/firefox', ); } if ( $self->{requested_version}->{developer} ) { return ( '/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox', ); } if ( $self->{requested_version}->{waterfox} ) { return ( '/Applications/Waterfox Current.app/Contents/MacOS/waterfox', ); } } return ( '/Applications/Firefox.app/Contents/MacOS/firefox', '/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox', '/Applications/Firefox Nightly.app/Contents/MacOS/firefox', '/Applications/Waterfox Current.app/Contents/MacOS/waterfox', '/Applications/Waterfox Classic.app/Contents/MacOS/waterfox', ); } my %_known_win32_organisations = ( 'Mozilla Firefox' => 'Mozilla', 'Mozilla Firefox ESR' => 'Mozilla', 'Firefox Developer Edition' => 'Mozilla', Nightly => 'Mozilla', 'Waterfox' => 'Waterfox', 'Waterfox Current' => 'Waterfox', 'Waterfox Classic' => 'Waterfox', Basilisk => 'Mozilla', 'Pale Moon' => 'Mozilla', ); sub win32_organisation { my ( $self, $name ) = @_; return $_known_win32_organisations{$name}; } sub win32_product_names { my ($self) = @_; my %known_win32_preferred_names = ( 'Mozilla Firefox' => 1, 'Mozilla Firefox ESR' => 2, 'Firefox Developer Edition' => 3, Nightly => 4, 'Waterfox' => 5, 'Waterfox Current' => 6, 'Waterfox Classic' => 7, Basilisk => 8, 'Pale Moon' => 9, ); if ( $self->{requested_version} ) { if ( $self->{requested_version}->{nightly} ) { foreach my $key ( sort { $a cmp $b } keys %known_win32_preferred_names ) { if ( $key ne 'Nightly' ) { delete $known_win32_preferred_names{$key}; } } } if ( $self->{requested_version}->{developer} ) { foreach my $key ( sort { $a cmp $b } keys %known_win32_preferred_names ) { if ( $key ne 'Firefox Developer Edition' ) { delete $known_win32_preferred_names{$key}; } } } if ( $self->{requested_version}->{waterfox} ) { foreach my $key ( sort { $a cmp $b } keys %known_win32_preferred_names ) { if ( $key !~ /^Waterfox/smx ) { delete $known_win32_preferred_names{$key}; } } } } return %known_win32_preferred_names; } sub _reg_query_via_ssh { my ( $self, %parameters ) = @_; my $binary = 'reg'; my @parameters = ( 'query', q["] . ( join q[\\], @{ $parameters{subkey} } ) . q["] ); if ( $parameters{name} ) { push @parameters, ( '/v', q["] . $parameters{name} . q["] ); } my @values; my $reg_query = $self->_execute_via_ssh( { ignore_exit_status => 1 }, $binary, @parameters ); if ( defined $reg_query ) { foreach my $line ( split /\r?\n/smx, $reg_query ) { if ( defined $parameters{name} ) { my $name = $parameters{name} eq q[] ? '(Default)' : $parameters{name}; my $quoted_name = quotemeta $name; if ( $line =~ /^[ ]+${quoted_name}[ ]+(?:REG_SZ)[ ]+(\S.*\S)\s*$/smx ) { push @values, $1; } } else { push @values, $line; } } } return @values; } sub _cygwin_reg_query_value { my ( $self, $path ) = @_; my $handle = FileHandle->new( $path, Fcntl::O_RDONLY() ); my $value; if ( defined $handle ) { $value = $self->_read_and_close_handle( $handle, $path ); $value =~ s/\0$//smx; } elsif ( $EXTENDED_OS_ERROR == POSIX::ENOENT() ) { } else { Firefox::Marionette::Exception->throw( "Failed to open '$path' for reading:$EXTENDED_OS_ERROR"); } return $value; } sub _get_binary_from_cygwin_registry_via_ssh { my ($self) = @_; my $binary; my %known_win32_preferred_names = $self->win32_product_names(); NAME: foreach my $name ( sort { $known_win32_preferred_names{$a} <=> $known_win32_preferred_names{$b} } keys %known_win32_preferred_names ) { ROOT_SUBKEY: foreach my $root_subkey (qw(SOFTWARE SOFTWARE/WOW6432Node)) { my $organisation = $self->win32_organisation($name); my $version = $self->_execute_via_ssh( { ignore_exit_status => 1 }, 'cat', '"/proc/registry/HKEY_LOCAL_MACHINE/' . $root_subkey . q[/] . $organisation . q[/] . $name . '/CurrentVersion"' ); if ( !defined $version ) { next ROOT_SUBKEY; } $version =~ s/\0$//smx; my $initial_version = $self->_execute_via_ssh( {}, 'cat', '"/proc/registry/HKEY_LOCAL_MACHINE/' . $root_subkey . q[/] . $organisation . q[/] . $name . q[/@] . q["] ); # (Default) value my $name_for_path_to_exe = $name; $name_for_path_to_exe =~ s/[ ]ESR//smx; my $path = $self->_execute_via_ssh( {}, 'cat', '"/proc/registry/HKEY_LOCAL_MACHINE/' . $root_subkey . q[/] . $organisation . q[/] . $name_for_path_to_exe . q[/] . $version . '/Main/PathToExe"' ); my $version_regex = qr/(\d+)[.](\d+(?:\w\d+)?)(?:[.](\d+))?/smx; if ( ( defined $path ) && ( $initial_version =~ /^$version_regex$/smx ) ) { $self->{_initial_version}->{major} = $1; $self->{_initial_version}->{minor} = $2; $self->{_initial_version}->{patch} = $3; $path =~ s/\0$//smx; $binary = $self->_execute_via_ssh( {}, 'cygpath', '-s', '-m', q["] . $path . q["] ); chomp $binary; last NAME; } } } return $binary; } sub _get_binary_from_cygwin_registry { my ($self) = @_; my $binary; my %known_win32_preferred_names = $self->win32_product_names(); NAME: foreach my $name ( sort { $known_win32_preferred_names{$a} <=> $known_win32_preferred_names{$b} } keys %known_win32_preferred_names ) { ROOT_SUBKEY: foreach my $root_subkey (qw(SOFTWARE SOFTWARE/WOW6432Node)) { my $organisation = $self->win32_organisation($name); my $version = $self->_cygwin_reg_query_value( '/proc/registry/HKEY_LOCAL_MACHINE/' . $root_subkey . q[/] . $organisation . q[/] . $name . '/CurrentVersion' ); if ( !defined $version ) { next ROOT_SUBKEY; } my $initial_version = $self->_cygwin_reg_query_value( '/proc/registry/HKEY_LOCAL_MACHINE/' . $root_subkey . q[/] . $organisation . q[/] . $name . q[/@] ); # (Default) value my $name_for_path_to_exe = $name; $name_for_path_to_exe =~ s/[ ]ESR//smx; my $path = $self->_cygwin_reg_query_value( '/proc/registry/HKEY_LOCAL_MACHINE/' . $root_subkey . q[/] . $organisation . q[/] . $name_for_path_to_exe . q[/] . $version . '/Main/PathToExe' ); my $version_regex = qr/(\d+)[.](\d+(?:\w\d+)?)(?:[.](\d+))?/smx; if ( ( defined $path ) && ( -e $path ) && ( $initial_version =~ /^$version_regex$/smx ) ) { $self->{_initial_version}->{major} = $1; $self->{_initial_version}->{minor} = $2; $self->{_initial_version}->{patch} = $3; $binary = $path; last NAME; } } } return $binary; } sub _get_binary_from_win32_registry_via_ssh { my ($self) = @_; my $binary; my %known_win32_preferred_names = $self->win32_product_names(); NAME: foreach my $name ( sort { $known_win32_preferred_names{$a} <=> $known_win32_preferred_names{$b} } keys %known_win32_preferred_names ) { ROOT_SUBKEY: foreach my $root_subkey ( ['SOFTWARE'], [ 'SOFTWARE', 'WOW6432Node' ] ) { my $organisation = $self->win32_organisation($name); my ($version) = $self->_reg_query_via_ssh( subkey => [ 'HKLM', @{$root_subkey}, $organisation, $name ], name => 'CurrentVersion' ); if ( !defined $version ) { next ROOT_SUBKEY; } my ($initial_version) = $self->_reg_query_via_ssh( subkey => [ 'HKLM', @{$root_subkey}, $organisation, $name ], name => q[] # (Default) value ); my $name_for_path_to_exe = $name; $name_for_path_to_exe =~ s/[ ]ESR//smx; my ($path) = $self->_reg_query_via_ssh( subkey => [ 'HKLM', @{$root_subkey}, $organisation, $name_for_path_to_exe, $version, 'Main' ], name => 'PathToExe' ); my $version_regex = qr/(\d+)[.](\d+(?:\w\d+)?)(?:[.](\d+))?/smx; if ( ( defined $path ) && ( $initial_version =~ /^$version_regex$/smx ) ) { $self->{_initial_version}->{major} = $1; $self->{_initial_version}->{minor} = $2; $self->{_initial_version}->{patch} = $3; $binary = $path; last NAME; } } } return $binary; } sub _win32_registry_query_key { my ( $self, $hkey, $subkey, $name ) = @_; Win32API::Registry::RegOpenKeyEx( $hkey, $subkey, 0, Win32API::Registry::KEY_QUERY_VALUE(), my $key ) or return; Win32API::Registry::RegQueryValueEx( $key, $name, [], my $type, my $value, [] ) or return; Win32API::Registry::RegCloseKey($key) or Firefox::Marionette::Exception->throw( "Failed to close registry key $subkey:" . Win32API::Registry::regLastError() ); return $value; } sub _get_binary_from_local_win32_registry { my ($self) = @_; my $binary; my %known_win32_preferred_names = $self->win32_product_names(); NAME: foreach my $name ( sort { $known_win32_preferred_names{$a} <=> $known_win32_preferred_names{$b} } keys %known_win32_preferred_names ) { ROOT_SUBKEY: foreach my $root_subkey (qw(SOFTWARE SOFTWARE\\WOW6432Node)) { my $organisation = $self->win32_organisation($name); my $version = $self->_win32_registry_query_key( Win32API::Registry::HKEY_LOCAL_MACHINE(), "$root_subkey\\$organisation\\$name", 'CurrentVersion' ); if ( !defined $version ) { next ROOT_SUBKEY; } my $initial_version = $self->_win32_registry_query_key( Win32API::Registry::HKEY_LOCAL_MACHINE(), "$root_subkey\\$organisation\\$name", q[] ); # (Default) value my $name_for_path_to_exe = $name; $name_for_path_to_exe =~ s/[ ]ESR//smx; my $path = $self->_win32_registry_query_key( Win32API::Registry::HKEY_LOCAL_MACHINE(), "$root_subkey\\$organisation\\$name_for_path_to_exe\\$version\\Main", 'PathToExe' ); my $version_regex = qr/(\d+)[.](\d+(?:\w\d+)?)(?:[.](\d+))?/smx; if ( ( defined $path ) && ( $initial_version =~ /^$version_regex$/smx ) ) { $self->{_initial_version}->{major} = $1; $self->{_initial_version}->{minor} = $2; $self->{_initial_version}->{patch} = $3; $binary = $path; last NAME; } } } return $binary; } sub _get_binary_from_local_osx_filesystem { my ($self) = @_; foreach my $path ( $self->macos_binary_paths() ) { if ( stat $path ) { return $path; } } return; } sub _get_binary_from_remote_osx_filesystem { my ($self) = @_; foreach my $path ( $self->macos_binary_paths() ) { foreach my $result ( split /\n/smx, $self->execute( 'ls', '-1', q["] . $path . q["] ) ) { if ( $result eq $path ) { my $plist_path = $path; if ( $plist_path =~ s/Contents\/MacOS.*$/Contents\/Info.plist/smx ) { my $plist_json = $self->execute( 'plutil', '-convert', 'json', '-o', q[-], q["] . $plist_path . q["] ); my $plist_ref = JSON::decode_json($plist_json); my $version_regex = qr/(\d+)[.](\d+(?:\w\d+)?)(?:[.](\d+))?/smx; if ( $plist_ref->{CFBundleShortVersionString} =~ /^$version_regex$/smx ) { $self->{_initial_version}->{major} = $1; $self->{_initial_version}->{minor} = $2; $self->{_initial_version}->{patch} = $3; return $path; } } } } } return; } sub _get_remote_binary { my ($self) = @_; my $binary; if ( $self->_remote_uname() eq 'MSWin32' ) { if ( !$self->{binary_from_registry} ) { $self->{binary_from_registry} = $self->_get_binary_from_win32_registry_via_ssh(); } if ( $self->{binary_from_registry} ) { $binary = $self->{binary_from_registry}; } } elsif ( $self->_remote_uname() eq 'darwin' ) { if ( !$self->{binary_from_osx_filesystem} ) { $self->{binary_from_osx_filesystem} = $self->_get_binary_from_remote_osx_filesystem(); } if ( $self->{binary_from_osx_filesystem} ) { $binary = $self->{binary_from_osx_filesystem}; } } elsif ( $self->_remote_uname() eq 'cygwin' ) { if ( !$self->{binary_from_cygwin_registry} ) { $self->{binary_from_cygwin_registry} = $self->_get_binary_from_cygwin_registry_via_ssh(); } if ( $self->{binary_from_cygwin_registry} ) { $binary = $self->{binary_from_cygwin_registry}; } } return $binary; } sub _get_local_binary { my ($self) = @_; my $binary; if ( $OSNAME eq 'MSWin32' ) { if ( !$self->{binary_from_registry} ) { $self->{binary_from_registry} = $self->_get_binary_from_local_win32_registry(); } if ( $self->{binary_from_registry} ) { $binary = Win32::GetShortPathName( $self->{binary_from_registry} ); } } elsif ( $OSNAME eq 'darwin' ) { if ( !$self->{binary_from_osx_filesystem} ) { $self->{binary_from_osx_filesystem} = $self->_get_binary_from_local_osx_filesystem(); } if ( $self->{binary_from_osx_filesystem} ) { $binary = $self->{binary_from_osx_filesystem}; } } elsif ( $OSNAME eq 'cygwin' ) { my $cygwin_binary = $self->_get_binary_from_cygwin_registry(); if ( defined $cygwin_binary ) { $binary = $self->execute( 'cygpath', '-u', $cygwin_binary ); } } return $binary; } sub default_binary_name { return 'firefox'; } sub _binary { my ($self) = @_; my $binary = $self->default_binary_name(); if ( $self->{marionette_binary} ) { $binary = $self->{marionette_binary}; } elsif ( $self->_ssh() ) { if ( my $remote_binary = $self->_get_remote_binary() ) { $binary = $remote_binary; } } else { if ( my $local_binary = $self->_get_local_binary() ) { $binary = $local_binary; } } return $binary; } sub child_error { my ($self) = @_; return $self->{_child_error}; } sub _signal_name { my ( $proto, $number ) = @_; return $sig_names[$number]; } sub error_message { my ($self) = @_; return $self->_error_message( 'Firefox', $self->child_error() ); } sub _error_message { my ( $self, $binary, $child_error ) = @_; my $message; if ( !defined $child_error ) { } elsif ( $OSNAME eq 'MSWin32' ) { $message = Win32::FormatMessage( Win32::GetLastError() ); } else { if ( ( POSIX::WIFEXITED($child_error) ) || ( POSIX::WIFSIGNALED($child_error) ) ) { if ( POSIX::WIFEXITED($child_error) ) { $message = $binary . ' exited with a ' . POSIX::WEXITSTATUS($child_error); } elsif ( POSIX::WIFSIGNALED($child_error) ) { my $name = $self->_signal_name( POSIX::WTERMSIG($child_error) ); if ( defined $name ) { $message = "$binary killed by a $name signal (" . POSIX::WTERMSIG($child_error) . q[)]; } else { $message = $binary . ' killed by a signal (' . POSIX::WTERMSIG($child_error) . q[)]; } } } } return $message; } sub _reap { my ($self) = @_; if ( $OSNAME eq 'MSWin32' ) { if ( $self->{_win32_firefox_process} ) { $self->{_win32_firefox_process}->GetExitCode( my $exit_code ); if ( $exit_code != Win32::Process::STILL_ACTIVE() ) { $self->{_child_error} = $exit_code; delete $self->{_win32_firefox_process}; } } if ( $self->{_win32_ssh_process} ) { $self->{_win32_ssh_process}->GetExitCode( my $exit_code ); if ( $exit_code != Win32::Process::STILL_ACTIVE() ) { $self->{_child_error} = $exit_code; delete $self->{_win32_ssh_process}; } } $self->_reap_other_win32_ssh_processes(); } elsif ( my $ssh = $self->_ssh() ) { while ( ( my $pid = waitpid _ANYPROCESS(), POSIX::WNOHANG() ) > 0 ) { if ( ( $ssh->{pid} ) && ( $pid == $ssh->{pid} ) ) { $self->{_child_error} = $CHILD_ERROR; } elsif ( ( $self->xvfb_pid() ) && ( $pid == $self->xvfb_pid() ) ) { $self->{_xvfb_child_error} = $CHILD_ERROR; delete $self->{xvfb_pid}; delete $self->{_xvfb_display_number}; } } } else { while ( ( my $pid = waitpid _ANYPROCESS(), POSIX::WNOHANG() ) > 0 ) { if ( ( $self->_firefox_pid() ) && ( $pid == $self->_firefox_pid() ) ) { $self->{_child_error} = $CHILD_ERROR; } elsif (( $self->_local_ssh_pid() ) && ( $pid == $self->_local_ssh_pid() ) ) { $self->{_child_error} = $CHILD_ERROR; } elsif ( ( $self->xvfb_pid() ) && ( $pid == $self->xvfb_pid() ) ) { $self->{_xvfb_child_error} = $CHILD_ERROR; delete $self->{xvfb_pid}; delete $self->{_xvfb_display_number}; } } } return; } sub _reap_other_win32_ssh_processes { my ($self) = @_; my @other_processes; foreach my $process ( @{ $self->{_other_win32_ssh_processes} } ) { $process->GetExitCode( my $exit_code ); if ( $exit_code == Win32::Process::STILL_ACTIVE() ) { push @other_processes, $process; } } $self->{_other_win32_ssh_processes} = \@other_processes; return; } sub _remote_process_running { my ( $self, $remote_pid ) = @_; my $now = time; if ( ( defined $self->{last_remote_alive_status} ) && ( $self->{last_remote_kill_time} >= $now ) ) { return $self->{last_remote_alive_status}; } $self->{last_remote_kill_time} = $now; if ( $self->_remote_uname() eq 'MSWin32' ) { return $self->_win32_remote_process_running($remote_pid); } else { return $self->_generic_remote_process_running($remote_pid); } } sub _win32_remote_process_running { my ( $self, $remote_pid ) = @_; my $binary = 'tasklist'; my @arguments = ( '/FI', q["PID eq ] . $remote_pid . q["] ); $self->{last_remote_alive_status} = 0; foreach my $line ( split /\r?\n/smx, $self->execute( $binary, @arguments ) ) { if ( $line =~ /^firefox[.]exe[ ]+(\d+)[ ]/smx ) { if ( $1 == $remote_pid ) { $self->{last_remote_alive_status} = 1; } } } return $self->{last_remote_alive_status}; } sub _generic_remote_process_running { my ( $self, $remote_pid ) = @_; my $binary = 'kill'; my @arguments = ( '-0', $remote_pid ); my $dev_null = File::Spec->devnull(); if ( my $pid = fork ) { waitpid $pid, 0; if ( $CHILD_ERROR == 0 ) { $self->{last_remote_alive_status} = 1; } else { $self->{last_remote_alive_status} = 0; } } elsif ( defined $pid ) { eval { $self->_ssh_exec( $self->_ssh_arguments(), $self->_ssh_address(), $binary, @arguments ) or Firefox::Marionette::Exception->throw( "Failed to exec 'ssh':$EXTENDED_OS_ERROR"); } or do { chomp $EVAL_ERROR; warn "$EVAL_ERROR\n"; }; exit 1; } else { Firefox::Marionette::Exception->throw( "Failed to fork:$EXTENDED_OS_ERROR"); } return $self->{last_remote_alive_status}; } sub alive { my ($self) = @_; if ( my $ssh = $self->_ssh() ) { $self->_reap(); if ( defined $ssh->{pid} ) { if ( $OSNAME eq 'MSWin32' ) { $self->_reap_other_win32_ssh_processes(); if ( $self->{_win32_ssh_process} ) { $self->{_win32_ssh_process}->GetExitCode( my $exit_code ); $self->_reap(); if ( $exit_code == Win32::Process::STILL_ACTIVE() ) { return 1; } } return 0; } else { return kill 0, $ssh->{pid}; } } elsif ( $self->_firefox_pid() ) { return $self->_remote_process_running( $self->_firefox_pid() ); } } elsif ( $OSNAME eq 'MSWin32' ) { $self->_reap_other_win32_ssh_processes(); if ( $self->{_win32_firefox_process} ) { $self->{_win32_firefox_process}->GetExitCode( my $exit_code ); $self->_reap(); if ( $exit_code == Win32::Process::STILL_ACTIVE() ) { return 1; } } return 0; } elsif ( $self->_firefox_pid() ) { $self->_reap(); return kill 0, $self->_firefox_pid(); } return; } sub _ssh_local_path_or_port { my ($self) = @_; if ( $self->{_ssh}->{use_unix_sockets} ) { if ( defined $self->ssh_local_directory() ) { my $path = File::Spec->catfile( $self->ssh_local_directory(), 'forward.sock' ); return $path; } } else { my $key = 'ssh_local_tcp_socket'; if ( !defined $self->{_ssh}->{$key} ) { socket my $socket, Socket::PF_INET(), Socket::SOCK_STREAM(), 0 or Firefox::Marionette::Exception->throw( "Failed to create a socket:$EXTENDED_OS_ERROR"); bind $socket, Socket::sockaddr_in( 0, Socket::INADDR_LOOPBACK() ) or Firefox::Marionette::Exception->throw( "Failed to bind socket:$EXTENDED_OS_ERROR"); my $port = ( Socket::sockaddr_in( getsockname $socket ) )[0]; close $socket or Firefox::Marionette::Exception->throw( "Failed to close random socket:$EXTENDED_OS_ERROR"); $self->{_ssh}->{$key} = $port; } return $self->{_ssh}->{$key}; } return; } sub _setup_local_socket_via_ssh_with_control_path { my ( $self, $ssh_local_path, $localhost, $port ) = @_; if ( $self->{_ssh_port_forwarding} ) { $self->_cancel_port_forwarding_via_ssh_with_control_path(); } $self->_start_port_forwarding_via_ssh_with_control_path( $ssh_local_path, $localhost, $port ); return; } sub _cancel_port_forwarding_via_ssh_with_control_path { my ($self) = @_; if ( my $pid = fork ) { waitpid $pid, 0; if ( $CHILD_ERROR != 0 ) { Firefox::Marionette::Exception->throw( 'Failed to forward marionette port from ' . $self->_ssh_address() . q[:] . $self->_error_message( 'ssh', $CHILD_ERROR ) ); } } elsif ( defined $pid ) { eval { $self->_ssh_exec( $self->_ssh_arguments(), '-O', 'cancel', $self->_ssh_address() ) or Firefox::Marionette::Exception->throw( "Failed to exec 'ssh':$EXTENDED_OS_ERROR"); } or do { if ( $self->debug() ) { chomp $EVAL_ERROR; warn "$EVAL_ERROR\n"; } }; exit 1; } else { Firefox::Marionette::Exception->throw( "Failed to fork:$EXTENDED_OS_ERROR"); } return; } sub _start_port_forwarding_via_ssh_with_control_path { my ( $self, $ssh_local_path, $localhost, $port ) = @_; if ( my $pid = fork ) { waitpid $pid, 0; if ( $CHILD_ERROR == 0 ) { $self->{_ssh_port_forwarding}->{$localhost}->{$port} = 1; } else { Firefox::Marionette::Exception->throw( 'Failed to forward marionette port from ' . $self->_ssh_address() . q[:] . $self->_error_message( 'ssh', $CHILD_ERROR ) ); } } elsif ( defined $pid ) { eval { $self->_ssh_exec( $self->_ssh_arguments(), '-L', "$ssh_local_path:$localhost:$port", '-O', 'forward', $self->_ssh_address() ) or Firefox::Marionette::Exception->throw( "Failed to exec 'ssh':$EXTENDED_OS_ERROR"); } or do { if ( $self->debug() ) { chomp $EVAL_ERROR; warn "$EVAL_ERROR\n"; } }; exit 1; } else { Firefox::Marionette::Exception->throw( "Failed to fork:$EXTENDED_OS_ERROR"); } return; } sub _setup_local_socket_via_ssh_without_control_path { my ( $self, $ssh_local_port, $localhost, $port ) = @_; my @ssh_arguments = ( $self->_ssh_arguments(), '-N', '-L', "$ssh_local_port:$localhost:$port", $self->_ssh_address(), ); if ( $OSNAME eq 'MSWin32' ) { my $process = $self->_start_win32_process( 'ssh', @ssh_arguments ); push @{ $self->{_other_win32_ssh_processes} }, $process; } else { if ( my $pid = fork ) { } elsif ( defined $pid ) { eval { $self->_ssh_exec( @ssh_arguments, ) or Firefox::Marionette::Exception->throw( "Failed to exec 'ssh':$EXTENDED_OS_ERROR"); } or do { if ( $self->debug() ) { chomp $EVAL_ERROR; warn "$EVAL_ERROR\n"; } }; exit 1; } else { Firefox::Marionette::Exception->throw( "Failed to fork:$EXTENDED_OS_ERROR"); } } if ( $self->_ssh()->{use_unix_sockets} ) { while ( !-e $ssh_local_port ) { sleep 1; } } else { my $found_port = 0; while ( $found_port == 0 ) { socket my $socket, Socket::PF_INET(), Socket::SOCK_STREAM(), 0 or Firefox::Marionette::Exception->throw( "Failed to create a socket:$EXTENDED_OS_ERROR"); my $sock_addr = Socket::pack_sockaddr_in( $ssh_local_port, Socket::inet_aton($localhost) ); if ( connect $socket, $sock_addr ) { $found_port = $ssh_local_port; } close $socket or Firefox::Marionette::Exception->throw( "Failed to close test socket:$EXTENDED_OS_ERROR"); } } return; } sub _setup_local_socket_via_ssh { my ( $self, $port ) = @_; my $localhost = '127.0.0.1'; if ( my $ssh = $self->_ssh() ) { my $ssh_local_path_or_port = $self->_ssh_local_path_or_port(); if ( $ssh->{use_control_path} ) { my $ssh_local_path = $ssh_local_path_or_port; $self->_setup_local_socket_via_ssh_with_control_path( $ssh_local_path, $localhost, $port ); return $ssh_local_path; } else { my $ssh_local_port = $ssh_local_path_or_port; $self->_setup_local_socket_via_ssh_without_control_path( $ssh_local_port, $localhost, $port ); return $ssh_local_port; } } return; } sub _get_marionette_port_or_undef { my ($self) = @_; my $port; if ( $self->{profile_path} ) { $port = defined $port && $port > 0 ? $port : $self->_get_marionette_port(); if ( ( !defined $port ) || ( $port == 0 ) ) { sleep 1; return; } } return $port; } sub _get_sock_addr { my ( $self, $host, $port ) = @_; my $sock_addr; if ( my $ssh = $self->_ssh() ) { if ( !-e $self->_ssh_local_path_or_port() ) { my $port_or_path = $self->_setup_local_socket_via_ssh($port); if ( $ssh->{use_unix_sockets} ) { $sock_addr = Socket::pack_sockaddr_un($port_or_path); } else { $sock_addr = Socket::pack_sockaddr_in( $port_or_path, Socket::inet_aton($host) ); } } else { sleep 1; return; } } else { $sock_addr = Socket::pack_sockaddr_in( $port, Socket::inet_aton($host) ); } return $sock_addr; } sub _using_unix_sockets_for_ssh_connection { my ($self) = @_; if ( my $ssh = $self->_ssh() ) { if ( $ssh->{use_unix_sockets} ) { return 1; } } return 0; } sub _setup_local_connection_to_firefox { my ( $self, @arguments ) = @_; my $host = _DEFAULT_HOST(); my $port; my $socket; my $sock_addr; my $connected; while ( ( !$connected ) && ( $self->alive() ) ) { $socket = undef; socket $socket, $self->_using_unix_sockets_for_ssh_connection() ? Socket::PF_UNIX() : Socket::PF_INET(), Socket::SOCK_STREAM(), 0 or Firefox::Marionette::Exception->throw( "Failed to create a socket:$EXTENDED_OS_ERROR"); binmode $socket; $port ||= $self->_get_marionette_port_or_undef(); next if ( !defined $port ); $sock_addr ||= $self->_get_sock_addr( $host, $port ); next if ( !defined $sock_addr ); if ( connect $socket, $sock_addr ) { $connected = 1; } elsif ( $EXTENDED_OS_ERROR == POSIX::ECONNREFUSED() ) { sleep 1; } elsif (( $OSNAME eq 'MSWin32' ) && ( $EXTENDED_OS_ERROR == _WIN32_CONNECTION_REFUSED() ) ) { sleep 1; } else { Firefox::Marionette::Exception->throw( "Failed to connect to $host on port $port:$EXTENDED_OS_ERROR"); } } $self->_reap(); if ( ( $self->alive() ) && ($socket) ) { } else { my $error_message = $self->error_message() ? $self->error_message() : q[Firefox was not launched]; Firefox::Marionette::Exception->throw($error_message); } return $socket; } sub _remote_catfile { my ( $self, @parts ) = @_; if ( ( $self->_remote_uname() ) && ( $self->_remote_uname() eq 'MSWin32' ) ) { return join q[\\], @parts; } else { return join q[/], @parts; } } sub _ssh_address { my ($self) = @_; my $address; if ( defined $self->{_ssh}->{user} ) { $address = join q[], $self->{_ssh}->{user}, q[@], $self->{_ssh}->{host}; } else { $address = $self->{_ssh}->{host}; } return $address; } sub _ssh_arguments { my ( $self, %parameters ) = @_; my @arguments = ( '-2', ); if ( my $ssh = $self->_ssh() ) { if ( my $port = $ssh->{port} ) { push @arguments, ( '-p' => $port, ); } } return ( @arguments, $self->_ssh_common_arguments(%parameters) ); } sub _ssh_exec { my ( $self, @parameters ) = @_; if ( $self->debug() ) { warn q[** ] . ( join q[ ], 'ssh', @parameters ) . "\n"; } my $dev_null = File::Spec->devnull(); open STDERR, q[>], $dev_null or Firefox::Marionette::Exception->throw( "Failed to redirect STDERR to $dev_null:$EXTENDED_OS_ERROR"); if ( $self->_remote_firefox_tmp_directory() ) { local $ENV{TMPDIR} = $self->_remote_firefox_tmp_directory(); return exec {'ssh'} 'ssh', @parameters; } else { return exec {'ssh'} 'ssh', @parameters; } } sub _make_remote_directory { my ( $self, $path ) = @_; if ( $OSNAME eq 'MSWin32' ) { if ( $self->_execute_win32_process( 'ssh', $self->_ssh_arguments(), $self->_ssh_address(), 'mkdir', $path ) ) { return $path; } else { Firefox::Marionette::Exception->throw( 'Failed to create directory ' . $self->_ssh_address() . ":$path:" . $self->_error_message( 'ssh', Win32::FormatMessage( Win32::GetLastError() ) ) ); } } else { my @mkdir_parameters; if ( $self->_remote_uname() ne 'MSWin32' ) { push @mkdir_parameters, qw(-m 700); } if ( my $pid = fork ) { waitpid $pid, 0; if ( $CHILD_ERROR != 0 ) { Firefox::Marionette::Exception->throw( 'Failed to create directory ' . $self->_ssh_address() . ":$path:" . $self->_error_message( 'ssh', $CHILD_ERROR ) ); } return $path; } elsif ( defined $pid ) { eval { $self->_ssh_exec( $self->_ssh_arguments(), $self->_ssh_address(), 'mkdir', @mkdir_parameters, $path ) or Firefox::Marionette::Exception->throw( "Failed to exec 'ssh':$EXTENDED_OS_ERROR"); } or do { if ( $self->debug() ) { chomp $EVAL_ERROR; warn "$EVAL_ERROR\n"; } }; exit 1; } else { Firefox::Marionette::Exception->throw( "Failed to fork:$EXTENDED_OS_ERROR"); } } return; } sub root_directory { my ($self) = @_; return $self->{_root_directory}; } sub _root_directory { my ($self) = @_; if ( !defined $self->{_root_directory} ) { my $root_directory = File::Temp->newdir( CLEANUP => 0, TEMPLATE => File::Spec->catdir( File::Spec->tmpdir(), 'firefox_marionette_local_XXXXXXXXXXX' ) ) or Firefox::Marionette::Exception->throw( "Failed to create temporary directory:$EXTENDED_OS_ERROR"); $self->{_root_directory} = $root_directory->dirname(); } return $self->root_directory(); } sub _write_local_proxy { my ( $self, $ssh ) = @_; my $local_proxy_path; if ( defined $ssh ) { $local_proxy_path = File::Spec->catfile( $self->ssh_local_directory(), 'reconnect' ); } else { $local_proxy_path = File::Spec->catfile( $self->{_root_directory}, 'reconnect' ); } unlink $local_proxy_path or ( $OS_ERROR == POSIX::ENOENT() ) or Firefox::Marionette::Exception->throw( "Failed to unlink $local_proxy_path:$EXTENDED_OS_ERROR"); my $local_proxy_handle = FileHandle->new( $local_proxy_path, Fcntl::O_CREAT() | Fcntl::O_EXCL() | Fcntl::O_WRONLY() ) or Firefox::Marionette::Exception->throw( "Failed to open $local_proxy_path for writing:$EXTENDED_OS_ERROR"); my $local_proxy = {}; if ( defined $local_proxy->{version} ) { foreach my $key (qw(major minor patch)) { if ( defined $self->{_initial_version}->{$key} ) { $local_proxy->{version}->{$key} = $self->{_initial_version}->{$key}; } } } if ( defined $ssh ) { $local_proxy->{ssh}->{root} = $self->{_root_directory}; $local_proxy->{ssh}->{name} = $self->_remote_uname(); $local_proxy->{ssh}->{binary} = $self->_binary(); $local_proxy->{ssh}->{uname} = $self->_remote_uname(); foreach my $key (qw(user host port pid)) { if ( defined $ssh->{$key} ) { $local_proxy->{ssh}->{$key} = $ssh->{$key}; } } } if ( defined $self->{_xvfb_pid} ) { $local_proxy->{xvfb}->{pid} = $self->{_xvfb_pid}; } if ( defined $self->{_firefox_pid} ) { $local_proxy->{firefox}->{pid} = $self->{_firefox_pid}; $local_proxy->{firefox}->{binary} = $self->_binary(); $local_proxy->{firefox}->{version} = $self->{_initial_version}; } $local_proxy_handle->print( JSON::encode_json($local_proxy) ) or Firefox::Marionette::Exception->throw( "Failed to write to $local_proxy_path:$EXTENDED_OS_ERROR"); $local_proxy_handle->close() or Firefox::Marionette::Exception->throw( "Failed to close '$local_proxy_path':$EXTENDED_OS_ERROR"); return; } sub _setup_profile_directories { my ( $self, $profile ) = @_; if ( ($profile) && ( $profile->download_directory() ) ) { } elsif ( my $ssh = $self->_ssh() ) { $self->{_root_directory} = $self->_get_remote_root_directory(); $self->_write_local_proxy($ssh); $self->{_profile_directory} = $self->_make_remote_directory( $self->_remote_catfile( $self->{_root_directory}, 'profile' ) ); $self->{_download_directory} = $self->_make_remote_directory( $self->_remote_catfile( $self->{_root_directory}, 'downloads' ) ); $self->{_remote_tmp_directory} = $self->_make_remote_directory( $self->_remote_catfile( $self->{_root_directory}, 'tmp' ) ); } else { my $root_directory = $self->_root_directory(); my $profile_directory = File::Spec->catdir( $root_directory, 'profile' ); mkdir $profile_directory, Fcntl::S_IRWXU() or Firefox::Marionette::Exception->throw( "Failed to create directory $profile_directory:$EXTENDED_OS_ERROR"); $self->{_profile_directory} = $profile_directory; my $download_directory = File::Spec->catdir( $root_directory, 'downloads' ); mkdir $download_directory, Fcntl::S_IRWXU() or Firefox::Marionette::Exception->throw( "Failed to create directory $download_directory:$EXTENDED_OS_ERROR" ); $self->{_download_directory} = $download_directory; my $tmp_directory = $self->_local_firefox_tmp_directory(); mkdir $tmp_directory, Fcntl::S_IRWXU() or Firefox::Marionette::Exception->throw( "Failed to create directory $tmp_directory:$EXTENDED_OS_ERROR"); } return; } sub _setup_new_profile { my ( $self, $profile, %parameters ) = @_; $self->_setup_profile_directories($profile); my $profile_path; if ( $self->_ssh() ) { $profile_path = $self->_remote_catfile( $self->{_profile_directory}, 'prefs.js' ); } else { $profile_path = File::Spec->catfile( $self->{_profile_directory}, 'prefs.js' ); } $self->{profile_path} = $profile_path; if ($profile) { if ( !$profile->download_directory() ) { my $download_directory = $self->{_download_directory}; if ( $OSNAME eq 'cygwin' ) { $download_directory = $self->execute( 'cygpath', '-s', '-w', $download_directory ); } $profile->download_directory($download_directory); } } else { my %profile_parameters = (); foreach my $profile_key (qw(chatty seer nightly)) { if ( $parameters{$profile_key} ) { $profile_parameters{$profile_key} = 1; } } if ( $self->{waterfox} ) { $profile = Waterfox::Marionette::Profile->new(%profile_parameters); } else { $profile = Firefox::Marionette::Profile->new(%profile_parameters); } my $download_directory = $self->{_download_directory}; my $bookmarks_path = $self->_setup_empty_bookmarks(); $self->_setup_search_json_mozlz4(); if ( ( $self->_remote_uname() ) && ( $self->_remote_uname() eq 'cygwin' ) ) { $download_directory = $self->_execute_via_ssh( {}, 'cygpath', '-s', '-w', $download_directory ); chomp $download_directory; $bookmarks_path = $self->_execute_via_ssh( {}, 'cygpath', '-s', '-w', $bookmarks_path ); chomp $bookmarks_path; } $profile->download_directory($download_directory); $profile->set_value( 'browser.bookmarks.file', $bookmarks_path, 1 ); if ( !$self->_is_firefox_major_version_at_least( _MIN_VERSION_FOR_LINUX_SANDBOX() ) ) { $profile->set_value( 'security.sandbox.content.level', 0, 0 ) ; # https://wiki.mozilla.org/Security/Sandbox#Customization_Settings } if ( !$parameters{chatty} ) { my $port = $self->_get_local_port_for_profile_urls(); $profile->set_value( 'media.gmp-manager.url', q[http://localhost:] . $port, 1 ); $profile->set_value( 'app.update.url', q[http://localhost:] . $port, 1 ); $profile->set_value( 'app.update.url.manual', q[http://localhost:] . $port, 1 ); $profile->set_value( 'browser.newtabpage.directory.ping', q[http://localhost:] . $port, 1 ); $profile->set_value( 'browser.newtabpage.directory.source', q[http://localhost:] . $port, 1 ); $profile->set_value( 'browser.selfsupport.url', q[http://localhost:] . $port, 1 ); $profile->set_value( 'extensions.systemAddon.update.url', q[http://localhost:] . $port, 1 ); $profile->set_value( 'dom.push.serverURL', q[http://localhost:] . $port, 1 ); $profile->set_value( 'services.settings.server', q[http://localhost:] . $port . q[/v1/], 1 ); $profile->set_value( 'browser.safebrowsing.gethashURL', q[http://localhost:] . $port, 1 ); $profile->set_value( 'browser.safebrowsing.keyURL', q[http://localhost:] . $port, 1 ); $profile->set_value( 'browser.safebrowsing.provider.mozilla.updateURL', q[http://localhost:] . $port, 1 ); $profile->set_value( 'browser.safebrowsing.provider.mozilla.gethashURL', q[http://localhost:] . $port, 1 ); $profile->set_value( 'browser.safebrowsing.provider.google.updateURL', q[http://localhost:] . $port, 1 ); $profile->set_value( 'browser.safebrowsing.provider.google4.updateURL', q[http://localhost:] . $port, 1 ); $profile->set_value( 'browser.safebrowsing.updateURL', q[http://localhost:] . $port, 1 ); $profile->set_value( 'extensions.shield-recipe-client.api_url', q[http://localhost:] . $port, 1 ); $profile->set_value( 'geo.provider-country.network.url', q[http://localhost:] . $port, 1 ); $profile->set_value( 'geo.wifi.uri', q[http://localhost:] . $port, 1 ); } } my $mime_types = join q[,], $self->mime_types(); $profile->set_value( 'browser.helperApps.neverAsk.saveToDisk', $mime_types ); if ( !$self->_is_auto_listen_okay() ) { my $port = $self->_get_empty_port(); $profile->set_value( 'marionette.defaultPrefs.port', $port ); $profile->set_value( 'marionette.port', $port ); } if ( $self->_ssh() ) { $self->_save_profile_via_ssh($profile); } else { $profile->save($profile_path); } return $self->{_profile_directory}; } sub _get_empty_port { my ($self) = @_; socket my $socket, Socket::PF_INET(), Socket::SOCK_STREAM(), 0 or Firefox::Marionette::Exception->throw( "Failed to create a socket:$EXTENDED_OS_ERROR"); bind $socket, Socket::sockaddr_in( 0, Socket::INADDR_LOOPBACK() ) or Firefox::Marionette::Exception->throw( "Failed to bind socket:$EXTENDED_OS_ERROR"); my $port = ( Socket::sockaddr_in( getsockname $socket ) )[0]; close $socket or Firefox::Marionette::Exception->throw( "Failed to close random socket:$EXTENDED_OS_ERROR"); return $port; } sub _get_local_port_for_profile_urls { my ($self) = @_; socket my $socket, Socket::PF_INET(), Socket::SOCK_STREAM(), 0 or Firefox::Marionette::Exception->throw( "Failed to create a socket:$EXTENDED_OS_ERROR"); bind $socket, Socket::sockaddr_in( 0, Socket::INADDR_LOOPBACK() ) or Firefox::Marionette::Exception->throw( "Failed to bind socket:$EXTENDED_OS_ERROR"); my $port = ( Socket::sockaddr_in( getsockname $socket ) )[0]; close $socket or Firefox::Marionette::Exception->throw( "Failed to close random socket:$EXTENDED_OS_ERROR"); return $port; } sub _setup_search_json_mozlz4 { my ($self) = @_; my $profile_directory = $self->{_profile_directory}; my $uncompressed = <<"_JSON_"; {"version":6,"engines":[{"_name":"DuckDuckGo","_isAppProvided":true,"_metaData":{}}],"metaData":{"useSavedOrder":false}} _JSON_ chomp $uncompressed; # my $content = _MAGIC_NUMBER_MOZL4Z() . Compress::LZ4::compress($uncompressed); my $content = MIME::Base64::decode_base64( 'bW96THo0MAB4AAAA8Bd7InZlcnNpb24iOjYsImVuZ2luZXMiOlt7Il9uYW1lIjoiRHVjawQA9x1HbyIsIl9pc0FwcFByb3ZpZGVkIjp0cnVlLCJfbWV0YURhdGEiOnt9fV0sIhAA8AgidXNlU2F2ZWRPcmRlciI6ZmFsc2V9fQ==' ); return $self->_copy_content_to_profile_directory( $content, 'search.json.mozlz4' ); } sub _setup_empty_bookmarks { my ($self) = @_; my $now = time; my $content = <<"_HTML_"; Bookmarks

Bookmarks Menu

Bookmarks Toolbar

Other Bookmarks

_HTML_ return $self->_copy_content_to_profile_directory( $content, 'bookmarks.html' ); } sub _copy_content_to_profile_directory { my ( $self, $content, $name ) = @_; my $profile_directory = $self->{_profile_directory}; my $path; if ( $self->_ssh() ) { my $handle = File::Temp::tempfile( File::Spec->catfile( File::Spec->tmpdir(), 'firefox_marionette_local_bookmarks_XXXXXXXXXXX' ) ) or Firefox::Marionette::Exception->throw( "Failed to open temporary file for writing:$EXTENDED_OS_ERROR"); print {$handle} $content or Firefox::Marionette::Exception->throw( "Failed to write to temporary file:$EXTENDED_OS_ERROR"); seek $handle, 0, Fcntl::SEEK_SET() or Firefox::Marionette::Exception->throw( "Failed to seek to start of temporary file:$EXTENDED_OS_ERROR"); $path = $self->_remote_catfile( $profile_directory, $name ); $self->_put_file_via_scp( $handle, $path, $name ); if ( $self->_remote_uname() eq 'cygwin' ) { $path = $self->_execute_via_ssh( {}, 'cygpath', '-l', '-w', $path ); chomp $path; } } else { $path = File::Spec->catfile( $profile_directory, $name ); my $handle = FileHandle->new( $path, Fcntl::O_CREAT() | Fcntl::O_EXCL() | Fcntl::O_WRONLY() ) or Firefox::Marionette::Exception->throw( "Failed to open $path for writing:$EXTENDED_OS_ERROR"); $handle->print($content) or Firefox::Marionette::Exception->throw( "Failed to write to $path:$EXTENDED_OS_ERROR"); $handle->close() or Firefox::Marionette::Exception->throw( "Failed to close '$path':$EXTENDED_OS_ERROR"); if ( $OSNAME eq 'cygwin' ) { $path = $self->execute( 'cygpath', '-s', '-w', $path ); chomp $path; } } return $path; } sub _save_profile_via_ssh { my ( $self, $profile ) = @_; my $handle = File::Temp::tempfile( File::Spec->catfile( File::Spec->tmpdir(), 'firefox_marionette_saved_profile_XXXXXXXXXXX' ) ) or Firefox::Marionette::Exception->throw( "Failed to open temporary file for writing:$EXTENDED_OS_ERROR"); print {$handle} $profile->as_string() or Firefox::Marionette::Exception->throw( "Failed to write to temporary file:$EXTENDED_OS_ERROR"); seek $handle, 0, Fcntl::SEEK_SET() or Firefox::Marionette::Exception->throw( "Failed to seek to start of temporary file:$EXTENDED_OS_ERROR"); $self->_put_file_via_scp( $handle, $self->{profile_path}, 'profile data' ); return; } sub _get_local_handle_for_generic_command_output { my ( $self, $parameters, $binary, @arguments ) = @_; my $dev_null = File::Spec->devnull(); my $handle = FileHandle->new(); if ( my $pid = $handle->open(q[-|]) ) { } elsif ( defined $pid ) { eval { if ( $parameters->{capture_stderr} ) { open STDERR, '>&', ( fileno STDOUT ) or Firefox::Marionette::Exception->throw( "Failed to redirect STDERR to STDOUT:$EXTENDED_OS_ERROR"); } elsif (( defined $parameters->{stderr} ) && ( $parameters->{stderr} == 0 ) ) { open STDERR, q[>], $dev_null or Firefox::Marionette::Exception->throw( "Failed to redirect STDERR to $dev_null:$EXTENDED_OS_ERROR" ); } else { open STDERR, q[>], $dev_null or Firefox::Marionette::Exception->throw( "Failed to redirect STDERR to $dev_null:$EXTENDED_OS_ERROR" ); } if ( $self->_remote_firefox_tmp_directory() ) { local $ENV{TMPDIR} = $self->_remote_firefox_tmp_directory(); exec {$binary} $binary, @arguments or Firefox::Marionette::Exception->throw( "Failed to exec $binary:$EXTENDED_OS_ERROR"); } else { exec {$binary} $binary, @arguments or Firefox::Marionette::Exception->throw( "Failed to exec $binary:$EXTENDED_OS_ERROR"); } } or do { chomp $EVAL_ERROR; warn "$EVAL_ERROR\n"; }; } else { Firefox::Marionette::Exception->throw( "Failed to fork:$EXTENDED_OS_ERROR"); } return $handle; } sub _get_local_command_output { my ( $self, $parameters, $binary, @arguments ) = @_; my $output; my $handle; if ( $OSNAME eq 'MSWin32' ) { my $shell_command = _quoting_for_cmd_exe( $binary, @arguments ); if ( $parameters->{capture_stderr} ) { $shell_command = "\"$shell_command 2>&1\""; } else { $shell_command .= ' 2>nul'; } if ( $self->debug() ) { warn q[** ] . $shell_command . "\n"; } $handle = FileHandle->new("$shell_command |"); } else { if ( $self->debug() ) { warn q[** ] . ( join q[ ], $binary, @arguments ) . "\n"; } $handle = $self->_get_local_handle_for_generic_command_output( $parameters, $binary, @arguments ); } my $result; while ( $result = $handle->read( my $buffer, _LOCAL_READ_BUFFER_SIZE() ) ) { $output .= $buffer; } defined $result or $parameters->{ignore_exit_status} or Firefox::Marionette::Exception->throw( "Failed to read from $binary " . ( join q[ ], @arguments ) . ":$EXTENDED_OS_ERROR" ); $handle->close() or $parameters->{ignore_exit_status} or Firefox::Marionette::Exception->throw( q[Command '] . ( join q[ ], $binary, @arguments ) . q[ did not complete successfully:] . $self->_error_message( $binary, $CHILD_ERROR ) ); return $output; } sub _ssh_client_version { my ($self) = @_; my $key = '_ssh_client_version'; if ( !defined $self->{$key} ) { foreach my $line ( split /\r?\n/smx, $self->_get_local_command_output( { capture_stderr => 1 }, 'ssh', '-V' ) ) { if ( $line =~ /^OpenSSH(?:_for_Windows)?_(\d+[.]\d+(?:p\d+)),/smx ) { ( $self->{$key} ) = ($1); } } } return $self->{$key}; } sub _scp_t_ok { my ($self) = @_; if ( $self->_ssh_client_version() =~ /^[1234567][.]/smx ) { return 0; } else { return 1; } } sub _scp_arguments { my ( $self, %parameters ) = @_; my @arguments = qw(-p); if ( $self->_scp_t_ok() ) { push @arguments, qw(-T); } if ( my $ssh = $self->_ssh() ) { if ( my $port = $ssh->{port} ) { push @arguments, ( '-P' => $port, ); } } return ( @arguments, $self->_ssh_common_arguments(%parameters) ); } sub _ssh_common_arguments { my ( $self, %parameters ) = @_; my @arguments = ( '-q', '-o' => 'ServerAliveInterval=15', '-o' => 'BatchMode=yes', '-o' => 'ExitOnForwardFailure=yes', ); if ( ( $parameters{master} ) || ( $parameters{env} ) ) { push @arguments, ( '-o' => 'SendEnv=TMPDIR' ); } if ( $parameters{accept_new} ) { push @arguments, ( '-o' => 'StrictHostKeyChecking=accept-new' ); } else { push @arguments, ( '-o' => 'StrictHostKeyChecking=yes' ); } if ( ( $parameters{master} ) && ( $self->{_ssh} ) && ( $self->{_ssh}->{use_control_path} ) ) { push @arguments, ( '-o' => 'ControlPath=' . $self->_control_path(), '-o' => 'ControlMaster=yes', '-o' => 'ControlPersist=30', ); } elsif ( ( $self->{_ssh} ) && ( $self->{_ssh}->{use_control_path} ) ) { push @arguments, ( '-o' => 'ControlPath=' . $self->_control_path(), '-o' => 'ControlMaster=no', ); } return @arguments; } sub _system { my ( $self, $parameters, $binary, @arguments ) = @_; my $command_line; my $result; if ( $OSNAME eq 'MSWin32' ) { $command_line = _quoting_for_cmd_exe( $binary, @arguments ); if ( $self->_execute_win32_process( $binary, @arguments ) ) { $result = 0; } else { $result = 1; } } else { my $dev_null = File::Spec->devnull(); $command_line = join q[ ], $binary, @arguments; if ( $self->debug() ) { warn q[** ] . $command_line . "\n"; } if ( my $pid = fork ) { waitpid $pid, 0; if ( $CHILD_ERROR == 0 ) { } elsif ( $parameters->{ignore_exit_status} ) { } else { Firefox::Marionette::Exception->throw( "Failed to successfully execute $command_line:" . $self->_error_message( $binary, $CHILD_ERROR ) ); } } elsif ( defined $pid ) { eval { if ( !$self->debug() ) { open STDERR, q[>], $dev_null or Firefox::Marionette::Exception->throw( "Failed to redirect STDERR to $dev_null:$EXTENDED_OS_ERROR" ); open STDOUT, q[>], $dev_null or Firefox::Marionette::Exception->throw( "Failed to redirect STDOUT to $dev_null:$EXTENDED_OS_ERROR" ); } exec {$binary} $binary, @arguments or Firefox::Marionette::Exception->throw( "Failed to exec '$binary':$EXTENDED_OS_ERROR"); } or do { if ( $self->debug() ) { chomp $EVAL_ERROR; warn "$EVAL_ERROR\n"; } }; exit 1; } else { Firefox::Marionette::Exception->throw( "Failed to fork:$EXTENDED_OS_ERROR"); } } return; } sub _get_file_via_scp { my ( $self, $parameters, $remote_path, $description ) = @_; $self->{_scp_get_file_index} += 1; my $local_name = 'file_' . $self->{_scp_get_file_index} . '.dat'; my $local_path = File::Spec->catfile( $self->{_local_scp_get_directory}, $local_name ); if ( $OSNAME eq 'MSWin32' ) { $remote_path = _quoting_for_cmd_exe($remote_path); } my @arguments = ( $self->_scp_arguments(), $self->_ssh_address() . ":\"$remote_path\"", $local_path, ); $self->_system( {}, 'scp', @arguments ); my $handle = FileHandle->new( $local_path, Fcntl::O_RDONLY() ); if ($handle) { binmode $handle; if ( ( $OSNAME eq 'MSWin32' ) || ( $OSNAME eq 'cygwin' ) ) { } else { unlink $local_path or Firefox::Marionette::Exception->throw( "Failed to unlink '$local_path':$EXTENDED_OS_ERROR"); } return $handle; } else { Firefox::Marionette::Exception->throw( "Failed to open '$local_path' for reading:$EXTENDED_OS_ERROR"); } return; } sub _put_file_via_scp { my ( $self, $original_handle, $remote_path, $description ) = @_; $self->{_scp_put_file_index} += 1; my $local_name = 'file_' . $self->{_scp_put_file_index} . '.dat'; my $local_path = File::Spec->catfile( $self->{_local_scp_put_directory}, $local_name ); my $temp_handle = FileHandle->new( $local_path, Fcntl::O_WRONLY() | Fcntl::O_CREAT() | Fcntl::O_EXCL(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR() ) or Firefox::Marionette::Exception->throw( "Failed to open '$local_path' for writing:$EXTENDED_OS_ERROR"); binmode $temp_handle; my $result; while ( $result = $original_handle->read( my $buffer, _LOCAL_READ_BUFFER_SIZE() ) ) { $temp_handle->print($buffer) or Firefox::Marionette::Exception->throw( "Failed to write to '$local_path':$EXTENDED_OS_ERROR"); } defined $result or Firefox::Marionette::Exception->throw( "Failed to read from $description:$EXTENDED_OS_ERROR"); close $temp_handle or Firefox::Marionette::Exception->throw( "Failed to close $local_path:$EXTENDED_OS_ERROR"); if ( $OSNAME eq 'MSWin32' ) { $remote_path = _quoting_for_cmd_exe($remote_path); } my @arguments = ( $self->_scp_arguments(), $local_path, $self->_ssh_address() . ":\"$remote_path\"", ); $self->_system( {}, 'scp', @arguments ); unlink $local_path or Firefox::Marionette::Exception->throw( "Failed to unlink $local_path:$EXTENDED_OS_ERROR"); return; } sub _initialise_remote_uname { my ($self) = @_; if ( defined $self->{_remote_uname} ) { } else { my $uname; my $command = 'uname || ver'; foreach my $line ( split /\r?\n/smx, $self->execute($command) ) { $line =~ s/[\r\n]//smxg; if ( ($line) && ( $line =~ /^Microsoft[ ]Windows[ ]/smx ) ) { $uname = 'MSWin32'; } elsif ( ($line) && ( $line =~ /^CYGWIN_NT/smx ) ) { $uname = 'cygwin'; } elsif ($line) { $uname = lc $line; } } $self->{_remote_uname} = $uname; chomp $self->{_remote_uname}; } return; } sub _remote_uname { my ($self) = @_; return $self->{_remote_uname}; } sub _get_marionette_port_via_ssh { my ($self) = @_; my $handle; my $sandbox_regex = $self->_sandbox_regex(); $self->_initialise_remote_uname(); if ( $self->_remote_uname() eq 'MSWin32' ) { $handle = $self->_get_file_via_scp( {}, $self->{profile_path}, 'profile path' ); } else { $handle = $self->_search_file_via_ssh( $self->{profile_path}, 'profile path', [ 'marionette\\.port', 'security\\.sandbox\\.content\\.tempDirSuffix', 'security\\.sandbox\\.plugin\\.tempDirSuffix' ] ); } my $port; while ( my $line = <$handle> ) { if ( $line =~ /^user_pref[(]"marionette[.]port",[ ]*(\d+)[)];\s*$/smx ) { $port = $1; } elsif ( $line =~ /^user_pref[(]"$sandbox_regex",[ ]*"[{]?([^"}]+)[}]?"[)];\s*$/smx ) { my ( $sandbox, $uuid ) = ( $1, $2 ); $self->{_ssh}->{sandbox}->{$sandbox} = $uuid; } } return $port; } sub _search_file_via_ssh { my ( $self, $path, $description, $patterns ) = @_; my $output = $self->_execute_via_ssh( {}, 'grep', ( map { ( q[-e], $_ ) } @{$patterns} ), $path ); my $handle = File::Temp::tempfile( File::Spec->catfile( File::Spec->tmpdir(), 'firefox_marionette_search_file_via_ssh_XXXXXXXXXXX' ) ) or Firefox::Marionette::Exception->throw( "Failed to open temporary file for writing:$EXTENDED_OS_ERROR"); $handle->print($output) or Firefox::Marionette::Exception->throw( "Failed to write to temporary file:$EXTENDED_OS_ERROR"); $handle->seek( 0, Fcntl::SEEK_SET() ) or Firefox::Marionette::Exception->throw( "Failed to seek to start of temporary file:$EXTENDED_OS_ERROR"); return $handle; } sub _get_marionette_port { my ($self) = @_; my $port; if ( my $ssh = $self->_ssh() ) { $port = $self->_get_marionette_port_via_ssh(); } else { my $profile_handle = FileHandle->new( $self->{profile_path}, Fcntl::O_RDONLY() ) or ( $OS_ERROR == POSIX::ENOENT() ) or ( ( $OSNAME eq 'MSWin32' ) && ( $EXTENDED_OS_ERROR == _WIN32_ERROR_SHARING_VIOLATION() ) ) or Firefox::Marionette::Exception->throw( "Failed to open '$self->{profile_path}' for reading:$EXTENDED_OS_ERROR" ); if ($profile_handle) { while ( my $line = <$profile_handle> ) { if ( $line =~ /^user_pref[(]"marionette.port",[ ]*(\d+)[)];\s*$/smx ) { $port = $1; } } $profile_handle->close() or Firefox::Marionette::Exception->throw( "Failed to close '$self->{profile_path}':$EXTENDED_OS_ERROR"); } } if ( defined $port ) { } else { $port = _DEFAULT_PORT(); } return $port; } sub _initial_socket_setup { my ( $self, $socket, $capabilities ) = @_; $self->{_socket} = $socket; my $initial_response = $self->_read_from_socket(); $self->{marionette_protocol} = $initial_response->{marionetteProtocol}; $self->{application_type} = $initial_response->{applicationType}; $self->_compatibility_checks_for_older_marionette(); return $self->new_session($capabilities); } sub _request_proxy { my ( $self, $proxy ) = @_; my $build = {}; if ( $proxy->type() ) { $build->{proxyType} = $proxy->type(); } elsif ( $proxy->pac() ) { $build->{proxyType} = 'pac'; } if ( $proxy->pac() ) { $build->{proxyAutoconfigUrl} = $proxy->pac()->as_string(); } if ( $proxy->ftp() ) { my ( $major, $minor, $patch ) = split /[.]/smx, $self->browser_version(); if ( $major <= _MAX_VERSION_FOR_FTP_PROXY() ) { $build->{proxyType} ||= 'manual'; $build->{ftpProxy} = $proxy->ftp(); } else { Carp::carp( '**** FTP proxying is no longer supported, ignoring this request ****' ); } } if ( $proxy->http() ) { $build->{proxyType} ||= 'manual'; $build->{httpProxy} = $proxy->http(); } if ( $proxy->none() ) { $build->{proxyType} ||= 'manual'; $build->{noProxy} = [ $proxy->none() ]; } if ( $proxy->https() ) { $build->{proxyType} ||= 'manual'; $build->{sslProxy} = $proxy->https(); } if ( $proxy->socks() ) { $build->{proxyType} ||= 'manual'; $build->{socksProxy} = $proxy->socks(); } if ( $proxy->socks_version() ) { $build->{proxyType} ||= 'manual'; $build->{socksProxyVersion} = $build->{socksVersion} = $proxy->socks_version() + 0; } elsif ( $proxy->socks() ) { $build->{proxyType} ||= 'manual'; $build->{socksProxyVersion} = $build->{socksVersion} = _DEFAULT_SOCKS_VERSION(); } return $self->_convert_proxy_before_request($build); } sub _convert_proxy_before_request { my ( $self, $build ) = @_; if ( defined $build ) { foreach my $key (qw(ftpProxy socksProxy sslProxy httpProxy)) { if ( defined $build->{$key} ) { if ( !$self->_is_new_hostport_okay() ) { if ( $build->{$key} =~ s/:(\d+)$//smx ) { $build->{ $key . 'Port' } = $1 + 0; } } } } } return $build; } sub _proxy_from_env { my ($self) = @_; my $build; my @keys = (qw(all https http)); my ( $major, $minor, $patch ) = split /[.]/smx, $self->browser_version(); if ( $major <= _MAX_VERSION_FOR_FTP_PROXY() ) { unshift @keys, qw(ftp); } foreach my $key (@keys) { my $full_name = $key . '_proxy'; if ( $ENV{$full_name} ) { } elsif ( $ENV{ uc $full_name } ) { $full_name = uc $full_name; } if ( $ENV{$full_name} ) { $build->{proxyType} = 'manual'; my $value = $ENV{$full_name}; if ( $value !~ /^\w+:\/\//smx ) { # add an http scheme if none exist $value = 'http://' . $value; } my $uri = URI->new($value); my $build_key = $key; if ( $key eq 'https' ) { $build_key = 'ssl'; } $build->{ $build_key . 'Proxy' } = $uri->host_port(); } } return $self->_convert_proxy_before_request($build); } sub _new_session_parameters { my ( $self, $capabilities ) = @_; my $parameters = {}; $parameters->{capabilities}->{requiredCapabilities} = {}; # for Mozilla 50 (and below???) if ( $self->_is_marionette_object( $capabilities, 'Firefox::Marionette::Capabilities' ) ) { my $actual = {}; my %booleans = ( set_window_rect => 'setWindowRect', accept_insecure_certs => 'acceptInsecureCerts', moz_webdriver_click => 'moz:webdriverClick', strict_file_interactability => 'strictFileInteractability', moz_use_non_spec_compliant_pointer_origin => 'moz:useNonSpecCompliantPointerOrigin', moz_accessibility_checks => 'moz:accessibilityChecks', ); foreach my $method ( sort { $a cmp $b } keys %booleans ) { if ( defined $capabilities->$method() ) { $actual->{ $booleans{$method} } = $capabilities->$method() ? \1 : \0; } } if ( defined $capabilities->page_load_strategy() ) { $actual->{pageLoadStrategy} = $capabilities->page_load_strategy(); } if ( defined $capabilities->unhandled_prompt_behavior() ) { $actual->{unhandledPromptBehavior} = $capabilities->unhandled_prompt_behavior(); } if ( $capabilities->proxy() ) { $actual->{proxy} = $self->_request_proxy( $capabilities->proxy() ); } elsif ( my $env_proxy = $self->_proxy_from_env() ) { $actual->{proxy} = $env_proxy; } $parameters = $actual; # for Mozilla 57 and after foreach my $key ( sort { $a cmp $b } keys %{$actual} ) { $parameters->{capabilities}->{requiredCapabilities}->{$key} = $actual->{$key}; # for Mozilla 56 (and below???) } $parameters->{capabilities}->{requiredCapabilities} ||= {}; # for Mozilla 50 (and below???) } elsif ( my $env_proxy = $self->_proxy_from_env() ) { $parameters->{proxy} = $env_proxy; # for Mozilla 57 and after $parameters->{capabilities}->{requiredCapabilities}->{proxy} = $env_proxy; # for Mozilla 56 (and below???) } return $parameters; } sub new_session { my ( $self, $capabilities ) = @_; my $parameters = $self->_new_session_parameters($capabilities); my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:NewSession'), $parameters ] ); my $response = $self->_get_response($message_id); $self->{session_id} = $response->result()->{sessionId}; my $new; if ( $self->marionette_protocol() == _MARIONETTE_PROTOCOL_VERSION_3() ) { $new = $self->_create_capabilities( $response->result()->{capabilities} ); } elsif ( ref $response->result()->{value} ) { $new = $self->_create_capabilities( $response->result()->{value} ); } else { $new = $self->capabilities(); } $self->{_cached_per_instance}->{_browser_version} = $new->browser_version(); if ( ( defined $capabilities ) && ( defined $capabilities->timeouts() ) ) { $self->timeouts( $capabilities->timeouts() ); $new->timeouts( $capabilities->timeouts() ); } return ( $self->{session_id}, $new ); } sub browser_version { my ($self) = @_; if ( defined $self->{_cached_per_instance}->{_browser_version} ) { return $self->{_cached_per_instance}->{_browser_version}; } elsif ( defined $self->{_initial_version} ) { return join q[.], map { defined $_ ? $_ : () } $self->{_initial_version}->{major}, $self->{_initial_version}->{minor}, $self->{_initial_version}->{patch}; } else { return; } } sub _create_capabilities { my ( $self, $parameters ) = @_; my $pid = $parameters->{'moz:processID'} || $parameters->{processId}; if ( ($pid) && ( $OSNAME eq 'cygwin' ) ) { $pid = $self->_firefox_pid(); } my $headless = $self->_visible() ? 0 : 1; if ( defined $parameters->{'moz:headless'} ) { my $firefox_headless = $parameters->{'moz:headless'} ? 1 : 0; if ( $firefox_headless != $headless ) { Firefox::Marionette::Exception->throw( 'moz:headless has not been determined correctly'); } } else { $parameters->{'moz:headless'} = $headless; } if ( !defined $self->{_cached_per_instance}->{_page_load_timeouts_key} ) { if ( $parameters->{timeouts} ) { if ( defined $parameters->{timeouts}->{'page load'} ) { $self->{_cached_per_instance}->{_page_load_timeouts_key} = 'page load'; } else { $self->{_cached_per_instance}->{_page_load_timeouts_key} = 'pageLoad'; } } else { $self->{_no_timeouts_command} = {}; $self->{_cached_per_instance}->{_page_load_timeouts_key} = 'pageLoad'; $self->timeouts( Firefox::Marionette::Timeouts->new( page_load => _DEFAULT_PAGE_LOAD_TIMEOUT(), script => _DEFAULT_SCRIPT_TIMEOUT(), implicit => _DEFAULT_IMPLICIT_TIMEOUT(), ) ); } } elsif ( $self->{_no_timeouts_command} ) { $parameters->{timeouts} = { $self->{_cached_per_instance}->{_page_load_timeouts_key} => $self->{_no_timeouts_command}->page_load(), script => $self->{_no_timeouts_command}->script(), implicit => $self->{_no_timeouts_command}->implicit(), }; } my %optional = $self->_get_optional_capabilities($parameters); return Firefox::Marionette::Capabilities->new( timeouts => Firefox::Marionette::Timeouts->new( page_load => $parameters->{timeouts} ->{ $self->{_cached_per_instance}->{_page_load_timeouts_key} }, script => $parameters->{timeouts}->{script}, implicit => $parameters->{timeouts}->{implicit}, ), browser_version => defined $parameters->{browserVersion} ? $parameters->{browserVersion} : $parameters->{version}, platform_name => defined $parameters->{platformName} ? $parameters->{platformName} : $parameters->{platform}, rotatable => $parameters->{rotatable} ? 1 : 0, platform_version => $parameters->{platformVersion}, moz_profile => $parameters->{'moz:profile'} || $self->{_profile_directory}, moz_process_id => $pid, moz_build_id => $parameters->{'moz:buildID'} || $parameters->{appBuildId}, browser_name => $parameters->{browserName}, moz_headless => $headless, %optional, ); } sub _get_optional_capabilities { my ( $self, $parameters ) = @_; my %optional; if ( ( defined $parameters->{proxy} ) && ( keys %{ $parameters->{proxy} } ) ) { $optional{proxy} = Firefox::Marionette::Proxy->new( $self->_response_proxy( $parameters->{proxy} ) ); } if ( defined $parameters->{'moz:accessibilityChecks'} ) { $optional{moz_accessibility_checks} = $parameters->{'moz:accessibilityChecks'} ? 1 : 0; } if ( defined $parameters->{strictFileInteractability} ) { $optional{strict_file_interactability} = $parameters->{strictFileInteractability} ? 1 : 0; } if ( defined $parameters->{'moz:shutdownTimeout'} ) { $optional{moz_shutdown_timeout} = $parameters->{'moz:shutdownTimeout'}; } if ( defined $parameters->{unhandledPromptBehavior} ) { $optional{unhandled_prompt_behavior} = $parameters->{unhandledPromptBehavior}; } if ( defined $parameters->{setWindowRect} ) { $optional{set_window_rect} = $parameters->{setWindowRect} ? 1 : 0; } if ( defined $parameters->{'moz:webdriverClick'} ) { $optional{moz_webdriver_click} = $parameters->{'moz:webdriverClick'} ? 1 : 0; } if ( defined $parameters->{acceptInsecureCerts} ) { $optional{accept_insecure_certs} = $parameters->{acceptInsecureCerts} ? 1 : 0; } if ( defined $parameters->{pageLoadStrategy} ) { $optional{page_load_strategy} = $parameters->{pageLoadStrategy}; } if ( defined $parameters->{'moz:useNonSpecCompliantPointerOrigin'} ) { $optional{moz_use_non_spec_compliant_pointer_origin} = $parameters->{'moz:useNonSpecCompliantPointerOrigin'} ? 1 : 0; } return %optional; } sub _response_proxy { my ( $self, $parameters ) = @_; my %proxy; if ( defined $parameters->{proxyType} ) { $proxy{type} = $parameters->{proxyType}; } if ( defined $parameters->{proxyAutoconfigUrl} ) { $proxy{pac} = $parameters->{proxyAutoconfigUrl}; } if ( defined $parameters->{ftpProxy} ) { $proxy{ftp} = $parameters->{ftpProxy}; if ( $parameters->{ftpProxyPort} ) { $proxy{ftp} .= q[:] . $parameters->{ftpProxyPort}; } } if ( defined $parameters->{httpProxy} ) { $proxy{http} = $parameters->{httpProxy}; if ( $parameters->{httpProxyPort} ) { $proxy{http} .= q[:] . $parameters->{httpProxyPort}; } } if ( defined $parameters->{sslProxy} ) { $proxy{https} = $parameters->{sslProxy}; if ( $parameters->{sslProxyPort} ) { $proxy{https} .= q[:] . $parameters->{sslProxyPort}; } } if ( defined $parameters->{noProxy} ) { $proxy{none} = $parameters->{noProxy}; } if ( defined $parameters->{socksProxy} ) { $proxy{socks} = $parameters->{socksProxy}; if ( $parameters->{socksProxyPort} ) { $proxy{socks} .= q[:] . $parameters->{socksProxyPort}; } } if ( defined $parameters->{socksProxyVersion} ) { $proxy{socks_version} = $parameters->{socksProxyVersion}; } elsif ( defined $parameters->{socksVersion} ) { $proxy{socks_version} = $parameters->{socksVersion}; } return %proxy; } sub find_elements { my ( $self, $value, $using ) = @_; Carp::carp( '**** DEPRECATED METHOD - find_elements HAS BEEN REPLACED BY find ****' ); return $self->_find( $value, $using ); } sub list { my ( $self, $value, $using, $from ) = @_; Carp::carp('**** DEPRECATED METHOD - list HAS BEEN REPLACED BY find ****'); return $self->_find( $value, $using, $from ); } sub list_by_id { my ( $self, $value, $from ) = @_; Carp::carp( '**** DEPRECATED METHOD - list_by_id HAS BEEN REPLACED BY find_id ****' ); return $self->_find( $value, 'id', $from ); } sub list_by_name { my ( $self, $value, $from ) = @_; Carp::carp( '**** DEPRECATED METHOD - list_by_name HAS BEEN REPLACED BY find_name ****' ); return $self->_find( $value, 'name', $from ); } sub list_by_tag { my ( $self, $value, $from ) = @_; Carp::carp( '**** DEPRECATED METHOD - list_by_tag HAS BEEN REPLACED BY find_tag ****' ); return $self->_find( $value, 'tag name', $from ); } sub list_by_class { my ( $self, $value, $from ) = @_; Carp::carp( '**** DEPRECATED METHOD - list_by_class HAS BEEN REPLACED BY find_class ****' ); return $self->_find( $value, 'class name', $from ); } sub list_by_selector { my ( $self, $value, $from ) = @_; Carp::carp( '**** DEPRECATED METHOD - list_by_selector HAS BEEN REPLACED BY find_selector ****' ); return $self->_find( $value, 'css selector', $from ); } sub list_by_link { my ( $self, $value, $from ) = @_; Carp::carp( '**** DEPRECATED METHOD - list_by_link HAS BEEN REPLACED BY find_link ****' ); return $self->_find( $value, 'link text', $from ); } sub list_by_partial { my ( $self, $value, $from ) = @_; Carp::carp( '**** DEPRECATED METHOD - list_by_partial HAS BEEN REPLACED BY find_partial ****' ); return $self->_find( $value, 'partial link text', $from ); } sub add_cookie { my ( $self, $cookie ) = @_; my $domain = $cookie->domain(); if ( !defined $domain ) { my $uri = $self->uri(); if ($uri) { my $obj = URI->new($uri); $domain = $obj->host(); } } my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:AddCookie'), { cookie => { httpOnly => $cookie->http_only() ? \1 : \0, secure => $cookie->secure() ? \1 : \0, domain => $domain, path => $cookie->path(), value => $cookie->value(), ( defined $cookie->expiry() ? ( expiry => $cookie->expiry() ) : () ), ( defined $cookie->same_site() ? ( sameSite => $cookie->same_site() ) : () ), name => $cookie->name() } } ] ); my $response = $self->_get_response($message_id); return $self; } sub add_header { my ( $self, %headers ) = @_; while ( ( my $name, my $value ) = each %headers ) { $self->{_headers}->{$name} ||= []; push @{ $self->{_headers}->{$name} }, { value => $value, merge => 1 }; } $self->_set_headers(); return $self; } sub add_site_header { my ( $self, $host, %headers ) = @_; while ( ( my $name, my $value ) = each %headers ) { $self->{_site_headers}->{$host}->{$name} ||= []; push @{ $self->{_site_headers}->{$host}->{$name} }, { value => $value, merge => 1 }; } $self->_set_headers(); return $self; } sub delete_header { my ( $self, @names ) = @_; foreach my $name (@names) { $self->{_headers}->{$name} = [ { value => q[], merge => 0 } ]; $self->{_deleted_headers}->{$name} = 1; } $self->_set_headers(); return $self; } sub delete_site_header { my ( $self, $host, @names ) = @_; foreach my $name (@names) { $self->{_site_headers}->{$host}->{$name} = [ { value => q[], merge => 0 } ]; $self->{_deleted_site_headers}->{$host}->{$name} = 1; } $self->_set_headers(); return $self; } sub _validate_request_header_merge { my ( $self, $merge ) = @_; if ($merge) { return 'true'; } else { return 'false'; } } sub _set_headers { my ($self) = @_; my $old = $self->_context('chrome'); my $script = <<'_JS_'; (function() { let observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService); let iterator = observerService.enumerateObservers("http-on-modify-request"); while (iterator.hasMoreElements()) { observerService.removeObserver(iterator.getNext(), "http-on-modify-request"); } })(); ({ observe: function(subject, topic, data) { this.onHeaderChanged(subject.QueryInterface(Components.interfaces.nsIHttpChannel), topic, data); }, register: function() { let observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService); observerService.addObserver(this, "http-on-modify-request", false); }, unregister: function() { let observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService); observerService.removeObserver(this, "http-on-modify-request"); }, onHeaderChanged: function(channel, topic, data) { let host = channel.URI.host; _JS_ foreach my $name ( sort { $a cmp $b } keys %{ $self->{_headers} } ) { my @headers = @{ $self->{_headers}->{$name} }; my $first = shift @headers; my $encoded_name = URI::Escape::uri_escape($name); if ( defined $first ) { my $value = $first->{value}; my $encoded_value = URI::Escape::uri_escape($value); my $validated_merge = $self->_validate_request_header_merge( $first->{merge} ); $script .= <<"_JS_"; channel.setRequestHeader(decodeURIComponent("$encoded_name"), decodeURIComponent("$encoded_value"), $validated_merge); _JS_ } foreach my $value (@headers) { my $encoded_value = URI::Escape::uri_escape( $value->{value} ); my $validated_merge = $self->_validate_request_header_merge( $value->{merge} ); $script .= <<"_JS_"; channel.setRequestHeader(decodeURIComponent("$encoded_name"), decodeURIComponent("$encoded_value"), $validated_merge); _JS_ } } foreach my $host ( sort { $a cmp $b } keys %{ $self->{_site_headers} } ) { my $encoded_host = URI::Escape::uri_escape($host); foreach my $name ( sort { $a cmp $b } keys %{ $self->{_site_headers}->{$host} } ) { my @headers = @{ $self->{_site_headers}->{$host}->{$name} }; my $first = shift @headers; my $encoded_name = URI::Escape::uri_escape($name); if ( defined $first ) { my $encoded_value = URI::Escape::uri_escape( $first->{value} ); my $validated_merge = $self->_validate_request_header_merge( $first->{merge} ); $script .= <<"_JS_"; if (host === decodeURIComponent("$encoded_host")) { channel.setRequestHeader(decodeURIComponent("$encoded_name"), decodeURIComponent("$encoded_value"), $validated_merge); } _JS_ } foreach my $value (@headers) { my $encoded_value = URI::Escape::uri_escape( $value->{value} ); my $validated_merge = $self->_validate_request_header_merge( $value->{merge} ); $script .= <<"_JS_"; if (host === decodeURIComponent("$encoded_host")) { channel.setRequestHeader(decodeURIComponent("$encoded_name"), decodeURIComponent("$encoded_value"), $validated_merge); } _JS_ } } } $script .= <<'_JS_'; } }).register(); _JS_ $self->script( $self->_compress_script($script) ); $self->_context($old); return; } sub _compress_script { my ( $self, $script ) = @_; $script =~ s/\/[*].*?[*]\///smxg; $script =~ s/\/\/.*$//smxg; $script =~ s/[\r\n\t]+/ /smxg; $script =~ s/[ ]+/ /smxg; return $script; } sub _is_marionette_object { my ( $self, $element, $class ) = @_; if ( ( Scalar::Util::blessed($element) && ( $element->isa($class) ) ) ) { return 1; } else { return 0; } } sub is_selected { my ( $self, $element ) = @_; if ( !$self->_is_marionette_object( $element, 'Firefox::Marionette::Element' ) ) { Firefox::Marionette::Exception->throw( 'is_selected method requires a Firefox::Marionette::Element parameter' ); } my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:IsElementSelected'), { id => $element->uuid() } ] ); my $response = $self->_get_response($message_id); return $self->_response_result_value($response) ? 1 : 0; } sub _response_result_value { my ( $self, $response ) = @_; return $response->result()->{value}; } sub is_enabled { my ( $self, $element ) = @_; if ( !$self->_is_marionette_object( $element, 'Firefox::Marionette::Element' ) ) { Firefox::Marionette::Exception->throw( 'is_enabled method requires a Firefox::Marionette::Element parameter' ); } my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:IsElementEnabled'), { id => $element->uuid() } ] ); my $response = $self->_get_response($message_id); return $self->_response_result_value($response) ? 1 : 0; } sub is_displayed { my ( $self, $element ) = @_; if ( !$self->_is_marionette_object( $element, 'Firefox::Marionette::Element' ) ) { Firefox::Marionette::Exception->throw( 'is_displayed method requires a Firefox::Marionette::Element parameter' ); } my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:IsElementDisplayed'), { id => $element->uuid() } ] ); my $response = $self->_get_response($message_id); return $self->_response_result_value($response) ? 1 : 0; } sub send_keys { my ( $self, $element, $text ) = @_; Carp::carp( '**** DEPRECATED METHOD - send_keys HAS BEEN REPLACED BY type ****'); return $self->type( $element, $text ); } sub type { my ( $self, $element, $text ) = @_; if ( !$self->_is_marionette_object( $element, 'Firefox::Marionette::Element' ) ) { Firefox::Marionette::Exception->throw( 'type method requires a Firefox::Marionette::Element parameter'); } my $message_id = $self->_new_message_id(); my $parameters = { id => $element->uuid(), text => $text }; if ( !$self->_is_new_sendkeys_okay() ) { $parameters->{value} = [ split //smx, $text ]; } $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:ElementSendKeys'), $parameters ] ); my $response = $self->_get_response($message_id); return $self; } sub delete_session { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:DeleteSession') ] ); my $response = $self->_get_response($message_id); delete $self->{session_id}; return $self; } sub minimise { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:MinimizeWindow') ] ); my $response = $self->_get_response($message_id); return $self; } sub maximise { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:MaximizeWindow') ] ); my $response = $self->_get_response($message_id); return $self; } sub refresh { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:Refresh') ] ); my $response = $self->_get_response($message_id); return $self; } my %_deprecated_commands = ( 'Marionette:Quit' => 'quitApplication', 'Marionette:SetContext' => 'setContext', 'Marionette:GetContext' => 'getContext', 'Marionette:AcceptConnections' => 'acceptConnections', 'Marionette:GetScreenOrientation' => 'getScreenOrientation', 'Marionette:SetScreenOrientation' => 'setScreenOrientation', 'Addon:Install' => 'addon:install', 'Addon:Uninstall' => 'addon:uninstall', 'WebDriver:AcceptAlert' => 'acceptDialog', 'WebDriver:AcceptDialog' => 'acceptDialog', 'WebDriver:AddCookie' => 'addCookie', 'WebDriver:Back' => 'goBack', 'WebDriver:CloseChromeWindow' => 'closeChromeWindow', 'WebDriver:CloseWindow' => [ { command => 'closeWindow', before_major => _MAX_VERSION_FOR_ANCIENT_CMDS() }, { command => 'close', before_major => _MAX_VERSION_FOR_NEW_CMDS() } ], 'WebDriver:DeleteAllCookies' => 'deleteAllCookies', 'WebDriver:DeleteCookie' => 'deleteCookie', 'WebDriver:DeleteSession' => 'deleteSession', 'WebDriver:DismissAlert' => 'dismissDialog', 'Marionette:GetWindowType' => [ { command => 'getWindowType', before_major => _MAX_VERSION_FOR_NEW_CMDS(), }, ], 'WebDriver:DismissAlert' => 'dismissDialog', 'WebDriver:ElementClear' => 'clearElement', 'WebDriver:ElementClick' => 'clickElement', 'WebDriver:ElementSendKeys' => 'sendKeysToElement', 'WebDriver:ExecuteAsyncScript' => 'executeAsyncScript', 'WebDriver:ExecuteScript' => 'executeScript', 'WebDriver:FindElement' => 'findElement', 'WebDriver:FindElements' => 'findElements', 'WebDriver:Forward' => 'goForward', 'WebDriver:FullscreenWindow' => 'fullscreen', 'WebDriver:GetActiveElement' => 'getActiveElement', 'WebDriver:GetActiveFrame' => 'getActiveFrame', 'WebDriver:GetAlertText' => 'getTextFromDialog', 'WebDriver:GetCapabilities' => 'getSessionCapabilities', 'WebDriver:GetChromeWindowHandle' => 'getChromeWindowHandle', 'WebDriver:GetChromeWindowHandles' => 'getChromeWindowHandles', 'WebDriver:GetCookies' => [ { command => 'getAllCookies', before_major => _MAX_VERSION_FOR_ANCIENT_CMDS() }, { command => 'getCookies', before_major => _MAX_VERSION_FOR_NEW_CMDS() } ], 'WebDriver:GetCurrentChromeWindowHandle' => [ { command => 'getChromeWindowHandle', before_major => 60 } ], 'WebDriver:GetCurrentURL' => [ { command => 'getUrl', before_major => _MAX_VERSION_FOR_ANCIENT_CMDS() }, { command => 'getCurrentUrl', before_major => _MAX_VERSION_FOR_NEW_CMDS() } ], 'WebDriver:GetElementAttribute' => 'getElementAttribute', 'WebDriver:GetElementCSSValue' => 'getElementValueOfCssProperty', 'WebDriver:GetElementProperty' => 'getElementProperty', 'WebDriver:GetElementRect' => 'getElementRect', 'WebDriver:GetElementTagName' => 'getElementTagName', 'WebDriver:GetElementText' => 'getElementText', 'WebDriver:GetPageSource' => 'getPageSource', 'WebDriver:GetTimeouts' => 'getTimeouts', 'WebDriver:GetTitle' => 'getTitle', 'WebDriver:GetWindowHandle' => [ { command => 'getWindow', before_major => _MAX_VERSION_FOR_ANCIENT_CMDS() }, { command => 'getWindowHandle', before_major => _MAX_VERSION_FOR_NEW_CMDS() } ], 'WebDriver:GetWindowHandles' => [ { command => 'getWindows', before_major => _MAX_VERSION_FOR_ANCIENT_CMDS() }, { command => 'getWindowHandles', before_major => _MAX_VERSION_FOR_NEW_CMDS() } ], 'WebDriver:GetWindowRect' => [ { command => 'getWindowSize', before_major => 60 } ], 'WebDriver:IsElementDisplayed' => 'isElementDisplayed', 'WebDriver:IsElementEnabled' => 'isElementEnabled', 'WebDriver:IsElementSelected' => 'isElementSelected', 'WebDriver:MaximizeWindow' => 'maximizeWindow', 'WebDriver:MinimizeWindow' => 'minimizeWindow', 'WebDriver:Navigate' => [ { command => 'goUrl', before_major => _MAX_VERSION_FOR_ANCIENT_CMDS() }, { command => 'get', before_major => _MAX_VERSION_FOR_NEW_CMDS() } ], 'WebDriver:NewSession' => 'newSession', 'WebDriver:PerformActions' => 'performActions', 'WebDriver:Refresh' => 'refresh', 'WebDriver:ReleaseActions' => 'releaseActions', 'WebDriver:SendAlertText' => 'sendKeysToDialog', 'WebDriver:SetTimeouts' => 'setTimeouts', 'WebDriver:SetWindowRect' => [ { command => 'setWindowSize', before_major => 60 } ], 'WebDriver:SwitchToFrame' => 'switchToFrame', 'WebDriver:SwitchToParentFrame' => 'switchToParentFrame', 'WebDriver:SwitchToShadowRoot' => 'switchToShadowRoot', 'WebDriver:SwitchToWindow' => 'switchToWindow', 'WebDriver:TakeScreenshot' => [ { command => 'screenShot', before_major => _MAX_VERSION_FOR_ANCIENT_CMDS() }, { command => 'takeScreenshot', before_major => _MAX_VERSION_FOR_NEW_CMDS() } ], ); sub _command { my ( $self, $command ) = @_; if ( defined $self->browser_version() ) { my ( $major, $minor, $patch ) = split /[.]/smx, $self->browser_version(); if ( $_deprecated_commands{$command} ) { if ( ref $_deprecated_commands{$command} ) { foreach my $command ( @{ $_deprecated_commands{$command} } ) { if ( $major < $command->{before_major} ) { return $command->{command}; } } } elsif ( $major < _MAX_VERSION_FOR_NEW_CMDS() ) { return $_deprecated_commands{$command}; } } } return $command; } sub capabilities { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetCapabilities') ] ); my $response = $self->_get_response($message_id); if ( $self->marionette_protocol() == _MARIONETTE_PROTOCOL_VERSION_3() ) { return $self->_create_capabilities( $response->result()->{capabilities} ); } else { return $self->_create_capabilities( $response->result()->{value} ); } } sub delete_cookies { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:DeleteAllCookies') ] ); my $response = $self->_get_response($message_id); return $self; } sub delete_cookie { my ( $self, $name ) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:DeleteCookie'), { name => $name } ] ); my $response = $self->_get_response($message_id); return $self; } sub cookies { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetCookies') ] ); my $response = $self->_get_response($message_id); my @cookies; if ( $self->marionette_protocol() == _MARIONETTE_PROTOCOL_VERSION_3() ) { @cookies = @{ $response->result() }; } else { @cookies = @{ $response->result()->{value} }; } return map { Firefox::Marionette::Cookie->new( http_only => $_->{httpOnly} ? 1 : 0, secure => $_->{secure} ? 1 : 0, domain => $_->{domain}, path => $_->{path}, value => $_->{value}, expiry => $_->{expiry}, name => $_->{name}, same_site => $_->{sameSite}, ) } @cookies; } sub tag_name { my ( $self, $element ) = @_; if ( !$self->_is_marionette_object( $element, 'Firefox::Marionette::Element' ) ) { Firefox::Marionette::Exception->throw( 'tag_name method requires a Firefox::Marionette::Element parameter' ); } my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetElementTagName'), { id => $element->uuid() } ] ); my $response = $self->_get_response($message_id); return $self->_response_result_value($response); } sub window_rect { my ( $self, $new ) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetWindowRect') ] ); my $response = $self->_get_response($message_id); my $result = $response->result(); if ( $result->{value} ) { $result = $result->{value}; } my $old = Firefox::Marionette::Window::Rect->new( pos_x => $result->{x}, pos_y => $result->{y}, width => $result->{width}, height => $result->{height}, wstate => $result->{state}, ); if ( defined $new ) { $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:SetWindowRect'), { x => $new->pos_x(), y => $new->pos_y(), width => $new->width(), height => $new->height() } ] ); $self->_get_response($message_id); } return $old; } sub rect { my ( $self, $element ) = @_; if ( !$self->_is_marionette_object( $element, 'Firefox::Marionette::Element' ) ) { Firefox::Marionette::Exception->throw( 'rect method requires a Firefox::Marionette::Element parameter'); } my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetElementRect'), { id => $element->uuid() } ] ); my $response = $self->_get_response($message_id); my $result = $response->result(); if ( $result->{value} ) { $result = $result->{value}; } return Firefox::Marionette::Element::Rect->new( pos_x => $result->{x}, pos_y => $result->{y}, width => $result->{width}, height => $result->{height}, ); } sub text { my ( $self, $element ) = @_; if ( !$self->_is_marionette_object( $element, 'Firefox::Marionette::Element' ) ) { Firefox::Marionette::Exception->throw( 'text method requires a Firefox::Marionette::Element parameter'); } my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetElementText'), { id => $element->uuid() } ] ); my $response = $self->_get_response($message_id); return $self->_response_result_value($response); } sub clear { my ( $self, $element ) = @_; if ( !$self->_is_marionette_object( $element, 'Firefox::Marionette::Element' ) ) { Firefox::Marionette::Exception->throw( 'clear method requires a Firefox::Marionette::Element parameter'); } my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:ElementClear'), { id => $element->uuid() } ] ); my $response = $self->_get_response($message_id); return $self; } sub click { my ( $self, $element ) = @_; if ( !$self->_is_marionette_object( $element, 'Firefox::Marionette::Element' ) ) { Firefox::Marionette::Exception->throw( 'click method requires a Firefox::Marionette::Element parameter'); } my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:ElementClick'), { id => $element->uuid() } ] ); my $response = $self->_get_response($message_id); return $self; } sub timeouts { my ( $self, $new ) = @_; my $old; if ( $self->{_no_timeouts_command} ) { if ( !defined $self->{_no_timeouts_command}->{page_load} ) { $self->{_no_timeouts_command} = $new; } $old = $self->{_no_timeouts_command}; } else { my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetTimeouts') ] ); my $response = $self->_get_response($message_id); $old = Firefox::Marionette::Timeouts->new( page_load => $response->result() ->{ $self->{_cached_per_instance}->{_page_load_timeouts_key} }, script => $response->result()->{script}, implicit => $response->result()->{implicit} ); } if ( defined $new ) { if ( $self->{_no_timeouts_command} ) { my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, 'timeouts', { type => 'implicit', ms => $new->implicit(), } ] ); $self->_get_response($message_id); $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, 'timeouts', { type => 'script', ms => $new->script(), } ] ); $self->_get_response($message_id); $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, 'timeouts', { type => 'default', ms => $new->page_load(), } ] ); $self->_get_response($message_id); $self->{_no_timeouts_command} = $new; } else { my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:SetTimeouts'), { $self->{_cached_per_instance} ->{_page_load_timeouts_key} => $new->page_load(), script => $new->script(), implicit => $new->implicit() } ] ); $self->_get_response($message_id); } } return $old; } sub active_element { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetActiveElement') ] ); my $response = $self->_get_response($message_id); if ( ref $self->_response_result_value($response) ) { return Firefox::Marionette::Element->new( $self, %{ $self->_response_result_value($response) } ); } else { return Firefox::Marionette::Element->new( $self, ELEMENT => $self->_response_result_value($response) ); } } sub uri { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetCurrentURL') ] ); my $response = $self->_get_response($message_id); return URI->new( $self->_response_result_value($response) ); } sub full_screen { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:FullscreenWindow') ] ); my $response = $self->_get_response($message_id); return $self; } sub dismiss_alert { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:DismissAlert') ] ); my $response = $self->_get_response($message_id); return $self; } sub send_alert_text { my ( $self, $text ) = @_; my $message_id = $self->_new_message_id(); my $parameters = { text => $text }; if ( !$self->_is_new_sendkeys_okay() ) { $parameters->{value} = [ split //smx, $text ]; } $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:SendAlertText'), $parameters ] ); my $response = $self->_get_response($message_id); return $self; } sub accept_alert { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:AcceptAlert') ] ); my $response = $self->_get_response($message_id); return $self; } sub accept_dialog { my ($self) = @_; Carp::carp( '**** DEPRECATED METHOD - using accept_dialog() HAS BEEN REPLACED BY accept_alert ****' ); my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:AcceptDialog') ] ); my $response = $self->_get_response($message_id); return $self; } sub alert_text { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetAlertText') ] ); my $response = $self->_get_response($message_id); return $self->_response_result_value($response); } my %_pdf_sizes = ( # '4A0' => { width => 168.2, height => 237.8 }, # '2A0' => { width => 118.9, height => 168.2 }, # A9 => { width => 3.7, height => 5.2 }, # A10 => { width => 2.6, height => 3.7 }, # B0 => { width => 100, height => 141.4 }, A1 => { width => 59.4, height => 84.1 }, A2 => { width => 42, height => 59.4 }, A3 => { width => 29.7, height => 42 }, A4 => { width => 21, height => 29.7 }, A5 => { width => 14.8, height => 21 }, A6 => { width => 10.5, height => 14.8 }, A7 => { width => 7.4, height => 10.5 }, A8 => { width => 5.2, height => 7.4 }, B1 => { width => 70.7, height => 100 }, B2 => { width => 50, height => 70.7 }, B3 => { width => 35.3, height => 50 }, B4 => { width => 25, height => 35.3 }, B5 => { width => 17.6, height => 25 }, B6 => { width => 12.5, height => 17.6 }, B7 => { width => 8.8, height => 12.5 }, B8 => { width => 6.2, height => 8.8 }, HALF_LETTER => { width => 14, height => 21.6 }, LETTER => { width => 21.6, height => 27.9 }, LEGAL => { width => 21.6, height => 35.6 }, JUNIOR_LEGAL => { width => 12.7, height => 20.3 }, LEDGER => { width => 12.7, height => 20.3 }, ); sub paper_sizes { my @keys = sort { $a cmp $b } keys %_pdf_sizes; return @keys; } sub _map_deprecated_pdf_parameters { my ( $self, %parameters ) = @_; my %mapping = ( shrink_to_fit => 'shrinkToFit', print_background => 'printBackground', page_ranges => 'pageRanges', ); foreach my $from ( sort { $a cmp $b } keys %mapping ) { my $to = $mapping{$from}; if ( defined $parameters{$to} ) { Carp::carp( "**** DEPRECATED PARAMETER - using $to as a parameter for the pdf(...) method HAS BEEN REPLACED BY the $from parameter ****" ); } elsif ( defined $parameters{$from} ) { $parameters{$to} = $parameters{$from}; delete $parameters{$from}; } } foreach my $key ( sort { $a cmp $b } keys %parameters ) { next if ( $key eq 'landscape' ); next if ( $key eq 'shrinkToFit' ); next if ( $key eq 'printBackground' ); next if ( $key eq 'margin' ); next if ( $key eq 'page' ); next if ( $key eq 'pageRanges' ); next if ( $key eq 'size' ); next if ( $key eq 'raw' ); Firefox::Marionette::Exception->throw( "Unknown key $key for the pdf method"); } return %parameters; } sub _initialise_pdf_parameters { my ( $self, %parameters ) = @_; %parameters = $self->_map_deprecated_pdf_parameters(%parameters); foreach my $key (qw(landscape shrinkToFit printBackground)) { if ( defined $parameters{$key} ) { $parameters{$key} = $parameters{$key} ? \1 : \0; } } if ( defined $parameters{page} ) { foreach my $key ( sort { $a cmp $b } keys %{ $parameters{page} } ) { next if ( $key eq 'width' ); next if ( $key eq 'height' ); Firefox::Marionette::Exception->throw( "Unknown key $key for the page parameter"); } } if ( defined $parameters{margin} ) { foreach my $key ( sort { $a cmp $b } keys %{ $parameters{margin} } ) { next if ( $key eq 'top' ); next if ( $key eq 'left' ); next if ( $key eq 'bottom' ); next if ( $key eq 'right' ); Firefox::Marionette::Exception->throw( "Unknown key $key for the margin parameter"); } } if ( my $size = delete $parameters{size} ) { $size =~ s/[ ]/_/smxg; if ( defined( my $instance = $_pdf_sizes{ uc $size } ) ) { $parameters{page}{width} = $instance->{width}; $parameters{page}{height} = $instance->{height}; } else { Firefox::Marionette::Exception->throw( "Page size of $size is unknown"); } } return %parameters; } sub pdf { my ( $self, %parameters ) = @_; %parameters = $self->_initialise_pdf_parameters(%parameters); my $raw = delete $parameters{raw}; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:Print'), \%parameters ] ); my $response = $self->_get_response($message_id); if ($raw) { my $content = $self->_response_result_value($response); return MIME::Base64::decode_base64($content); } else { my $handle = File::Temp->new( TEMPLATE => File::Spec->catfile( File::Spec->tmpdir(), 'firefox_marionette_print_XXXXXXXXXXX' ) ) or Firefox::Marionette::Exception->throw( "Failed to open temporary file for writing:$EXTENDED_OS_ERROR"); binmode $handle; my $content = $self->_response_result_value($response); $handle->print( MIME::Base64::decode_base64($content) ) or Firefox::Marionette::Exception->throw( "Failed to write to temporary file:$EXTENDED_OS_ERROR"); $handle->seek( 0, Fcntl::SEEK_SET() ) or Firefox::Marionette::Exception->throw( "Failed to seek to start of temporary file:$EXTENDED_OS_ERROR"); return $handle; } } sub selfie { my ( $self, $element, @remaining ) = @_; my $message_id = $self->_new_message_id(); my $parameters = {}; my %extra; if ( $self->_is_marionette_object( $element, 'Firefox::Marionette::Element' ) ) { $parameters = { id => $element->uuid() }; %extra = @remaining; } elsif (( defined $element ) && ( not( ref $element ) ) && ( ( scalar @remaining ) % 2 ) ) { %extra = ( $element, @remaining ); $element = undef; } if ( $extra{highlights} ) { foreach my $highlight ( @{ $extra{highlights} } ) { push @{ $parameters->{highlights} }, $highlight->uuid(); } } foreach my $key (qw(hash full scroll)) { if ( $extra{$key} ) { $parameters->{$key} = \1; } } $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:TakeScreenshot'), $parameters ] ); my $response = $self->_get_response($message_id); if ( $extra{hash} ) { return $self->_response_result_value($response); } elsif ( $extra{raw} ) { my $content = $self->_response_result_value($response); $content =~ s/^data:image\/png;base64,//smx; return MIME::Base64::decode_base64($content); } else { my $handle = File::Temp->new( TEMPLATE => File::Spec->catfile( File::Spec->tmpdir(), 'firefox_marionette_selfie_XXXXXXXXXXX' ) ) or Firefox::Marionette::Exception->throw( "Failed to open temporary file for writing:$EXTENDED_OS_ERROR"); binmode $handle; my $content = $self->_response_result_value($response); $content =~ s/^data:image\/png;base64,//smx; $handle->print( MIME::Base64::decode_base64($content) ) or Firefox::Marionette::Exception->throw( "Failed to write to temporary file:$EXTENDED_OS_ERROR"); $handle->seek( 0, Fcntl::SEEK_SET() ) or Firefox::Marionette::Exception->throw( "Failed to seek to start of temporary file:$EXTENDED_OS_ERROR"); return $handle; } } sub current_chrome_window_handle { my ($self) = @_; if ( $self->_is_firefox_major_version_at_least( _MIN_VERSION_NO_CHROME_CALLS() ) ) { Carp::carp( '**** DEPRECATED METHOD - using current_chrome_window_handle() HAS BEEN REPLACED BY window_handle() wrapped with appropriate context() calls ****' ); my $old = $self->context('chrome'); my $response = $self->window_handle(); $self->context($old); return $response; } else { my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetCurrentChromeWindowHandle') ] ); my $response = $self->_get_response($message_id); if ( ( defined $response->{result}->{ok} ) && ( $response->{result}->{ok} ) ) { $response = $self->_get_response($message_id); } return $self->_response_result_value($response); } } sub chrome_window_handle { my ($self) = @_; if ( $self->_is_firefox_major_version_at_least( _MIN_VERSION_NO_CHROME_CALLS() ) ) { Carp::carp( '**** DEPRECATED METHOD - using chrome_window_handle() HAS BEEN REPLACED BY window_handle() wrapped with appropriate context() calls ****' ); my $old = $self->context('chrome'); my $response = $self->window_handle(); $self->context($old); return $response; } else { my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetChromeWindowHandle') ] ); my $response = $self->_get_response($message_id); return $self->_response_result_value($response); } } sub key_down { my ( $self, $key ) = @_; return { type => 'keyDown', value => $key }; } sub key_up { my ( $self, $key ) = @_; return { type => 'keyUp', value => $key }; } sub pause { my ( $self, $duration ) = @_; return { type => 'pause', duration => $duration }; } sub mouse_move { my ( $self, @parameters ) = @_; my %arguments; if ( $self->_is_marionette_object( $parameters[0], 'Firefox::Marionette::Element' ) ) { my $origin = shift @parameters; my $rect = $origin->rect(); $arguments{x} = $rect->pos_x() + ( $rect->width() / 2 ); if ( $arguments{x} != int $arguments{x} ) { $arguments{x} = int $arguments{x} + 1; } $arguments{y} = $rect->pos_y() + ( $rect->height() / 2 ); if ( $arguments{y} != int $arguments{y} ) { $arguments{y} = int $arguments{y} + 1; } } while (@parameters) { my $key = shift @parameters; $arguments{$key} = shift @parameters; } return { type => 'pointerMove', pointerType => 'mouse', %arguments }; } sub mouse_down { my ( $self, $button ) = @_; return { type => 'pointerDown', pointerType => 'mouse', button => ( $button || 0 ) }; } sub mouse_up { my ( $self, $button ) = @_; return { type => 'pointerUp', pointerType => 'mouse', button => ( $button || 0 ) }; } sub perform { my ( $self, @actions ) = @_; my $message_id = $self->_new_message_id(); my $previous_type; my @action_sequence; foreach my $parameter_action (@actions) { my $marionette_action = {}; foreach my $key ( sort { $a cmp $b } keys %{$parameter_action} ) { $marionette_action->{$key} = $parameter_action->{$key}; } my $type; my %arguments; if ( ( $marionette_action->{type} eq 'keyUp' ) || ( $marionette_action->{type} eq 'keyDown' ) ) { $type = 'key'; } elsif (( $marionette_action->{type} eq 'pointerMove' ) || ( $marionette_action->{type} eq 'pointerDown' ) || ( $marionette_action->{type} eq 'pointerUp' ) ) { $type = 'pointer'; %arguments = ( parameters => { pointerType => delete $marionette_action->{pointerType} } ); } elsif ( $marionette_action->{type} eq 'pause' ) { if ( defined $previous_type ) { $type = $previous_type; } else { $type = 'none'; } } else { Firefox::Marionette::Exception->throw( 'Unknown action type in sequence. keyUp, keyDown, pointerMove, pointerDown, pointerUp or pause are the only known types' ); } $self->{next_action_sequence_id}++; my $id = $self->{next_action_sequence_id}; if ( ( defined $previous_type ) && ( $type eq $previous_type ) ) { push @{ $action_sequence[-1]{actions} }, $marionette_action; } else { push @action_sequence, { type => $type, id => 'seq' . $id, %arguments, actions => [$marionette_action] }; } $previous_type = $type; } $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:PerformActions'), { actions => \@action_sequence }, ] ); my $response = $self->_get_response($message_id); return $self; } sub release { my ( $self, @actions ) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:ReleaseActions') ] ); my $response = $self->_get_response($message_id); $self->{next_action_sequence_id} = 0; return $self; } sub chrome_window_handles { my ( $self, $element ) = @_; if ( $self->_is_firefox_major_version_at_least( _MIN_VERSION_NO_CHROME_CALLS() ) ) { Carp::carp( '**** DEPRECATED METHOD - using chrome_window_handles() HAS BEEN REPLACED BY window_handles() wrapped with appropriate context() calls ****' ); my $old = $self->context('chrome'); my @response = $self->window_handles(); $self->context($old); return @response; } else { my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetChromeWindowHandles') ] ); my $response = $self->_get_response($message_id); if ( $self->marionette_protocol() == _MARIONETTE_PROTOCOL_VERSION_3() ) { return @{ $response->result() }; } else { return @{ $response->result()->{value} }; } } } sub window_handle { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetWindowHandle') ] ); my $response = $self->_get_response($message_id); return $self->_response_result_value($response); } sub window_handles { my ( $self, $element ) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetWindowHandles') ] ); my $response = $self->_get_response($message_id); if ( $self->marionette_protocol() == _MARIONETTE_PROTOCOL_VERSION_3() ) { return @{ $response->result() }; } else { return @{ $response->result()->{value} }; } } sub new_window { my ( $self, %parameters ) = @_; foreach my $key (qw(focus private)) { if ( defined $parameters{$key} ) { $parameters{$key} = $parameters{$key} ? \1 : \0; } } my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:NewWindow'), {%parameters} ] ); my $response = $self->_get_response($message_id); return $response->result()->{handle}; } sub close_current_chrome_window_handle { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:CloseChromeWindow') ] ); my $response = $self->_get_response($message_id); if ( ref $response->result() eq 'HASH' ) { return ( $self->_response_result_value($response) ); } else { return @{ $response->result() }; } } sub close_current_window_handle { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:CloseWindow') ] ); my $response = $self->_get_response($message_id); if ( ref $response->result() eq 'HASH' ) { return ( $response->result() ); } else { return @{ $response->result() }; } } sub css { my ( $self, $element, $property_name ) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetElementCSSValue'), { id => $element->uuid(), propertyName => $property_name } ] ); my $response = $self->_get_response($message_id); return $self->_response_result_value($response); } sub property { my ( $self, $element, $name ) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetElementProperty'), { id => $element->uuid(), name => $name } ] ); my $response = $self->_get_response($message_id); return $self->_response_result_value($response); } sub attribute { my ( $self, $element, $name ) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetElementAttribute'), { id => $element->uuid(), name => $name } ] ); my $response = $self->_get_response($message_id); return $self->_response_result_value($response); } sub has { my ( $self, $value, $using, $from ) = @_; return $self->_find( $value, $using, $from, { return_undef_if_no_such_element => 1 } ); } sub has_id { my ( $self, $value, $from ) = @_; return $self->_find( $value, 'id', $from, { return_undef_if_no_such_element => 1 } ); } sub has_name { my ( $self, $value, $from ) = @_; return $self->_find( $value, 'name', $from, { return_undef_if_no_such_element => 1 } ); } sub has_tag { my ( $self, $value, $from ) = @_; return $self->_find( $value, 'tag name', $from, { return_undef_if_no_such_element => 1 } ); } sub has_class { my ( $self, $value, $from ) = @_; return $self->_find( $value, 'class name', $from, { return_undef_if_no_such_element => 1 } ); } sub has_selector { my ( $self, $value, $from ) = @_; return $self->_find( $value, 'css selector', $from, { return_undef_if_no_such_element => 1 } ); } sub has_link { my ( $self, $value, $from ) = @_; return $self->_find( $value, 'link text', $from, { return_undef_if_no_such_element => 1 } ); } sub has_partial { my ( $self, $value, $from ) = @_; return $self->_find( $value, 'partial link text', $from, { return_undef_if_no_such_element => 1 } ); } sub find_element { my ( $self, $value, $using ) = @_; Carp::carp( '**** DEPRECATED METHOD - find_element HAS BEEN REPLACED BY find ****'); return $self->find( $value, $using ); } sub find { my ( $self, $value, $using, $from ) = @_; return $self->_find( $value, $using, $from ); } sub find_id { my ( $self, $value, $from ) = @_; return $self->_find( $value, 'id', $from ); } sub find_name { my ( $self, $value, $from ) = @_; return $self->_find( $value, 'name', $from ); } sub find_tag { my ( $self, $value, $from ) = @_; return $self->_find( $value, 'tag name', $from ); } sub find_class { my ( $self, $value, $from ) = @_; return $self->_find( $value, 'class name', $from ); } sub find_selector { my ( $self, $value, $from ) = @_; return $self->_find( $value, 'css selector', $from ); } sub find_link { my ( $self, $value, $from ) = @_; return $self->_find( $value, 'link text', $from ); } sub find_partial { my ( $self, $value, $from ) = @_; return $self->_find( $value, 'partial link text', $from ); } sub find_by_id { my ( $self, $value, $from ) = @_; Carp::carp( '**** DEPRECATED METHOD - find_by_id HAS BEEN REPLACED BY find_id ****' ); return $self->find_id( $value, $from ); } sub find_by_name { my ( $self, $value, $from ) = @_; Carp::carp( '**** DEPRECATED METHOD - find_by_name HAS BEEN REPLACED BY find_name ****' ); return $self->find_name( $value, $from ); } sub find_by_tag { my ( $self, $value, $from ) = @_; Carp::carp( '**** DEPRECATED METHOD - find_by_tag HAS BEEN REPLACED BY find_tag ****' ); return $self->find_tag( $value, $from ); } sub find_by_class { my ( $self, $value, $from ) = @_; Carp::carp( '**** DEPRECATED METHOD - find_by_class HAS BEEN REPLACED BY find_class ****' ); return $self->find_class( $value, $from ); } sub find_by_selector { my ( $self, $value, $from ) = @_; Carp::carp( '**** DEPRECATED METHOD - find_by_selector HAS BEEN REPLACED BY find_selector ****' ); return $self->find_selector( $value, $from ); } sub find_by_link { my ( $self, $value, $from ) = @_; Carp::carp( '**** DEPRECATED METHOD - find_by_link HAS BEEN REPLACED BY find_link ****' ); return $self->find_link( $value, $from ); } sub find_by_partial { my ( $self, $value, $from ) = @_; Carp::carp( '**** DEPRECATED METHOD - find_by_partial HAS BEEN REPLACED BY find_partial ****' ); return $self->find_partial( $value, $from ); } sub _find { my ( $self, $value, $using, $from, $options ) = @_; $using ||= 'xpath'; my $message_id = $self->_new_message_id(); my $parameters = { using => $using, value => $value }; if ( defined $from ) { if ( $self->marionette_protocol() == _MARIONETTE_PROTOCOL_VERSION_3() ) { $parameters->{element} = $from->uuid(); } else { $parameters->{ELEMENT} = $from->uuid(); } } my $command = wantarray ? 'WebDriver:FindElements' : 'WebDriver:FindElement'; $self->_send_request( [ _COMMAND(), $message_id, $self->_command($command), $parameters, ] ); my $response = $self->_get_response( $message_id, { using => $using, value => $value }, $options ); if (wantarray) { if ( $response->ignored_exception() ) { return (); } if ( $self->marionette_protocol() == _MARIONETTE_PROTOCOL_VERSION_3() ) { return map { Firefox::Marionette::Element->new( $self, %{$_} ) } @{ $response->result() }; } elsif ( ( ref $self->_response_result_value($response) ) && ( ( ref $self->_response_result_value($response) ) eq 'ARRAY' ) && ( ref $self->_response_result_value($response)->[0] ) && ( ( ref $self->_response_result_value($response)->[0] ) eq 'HASH' ) ) { return map { Firefox::Marionette::Element->new( $self, %{$_} ) } @{ $self->_response_result_value($response) }; } else { return map { Firefox::Marionette::Element->new( $self, ELEMENT => $_ ) } @{ $self->_response_result_value($response) }; } } else { if ( $response->ignored_exception() ) { return; } if ( ( $self->marionette_protocol() == _MARIONETTE_PROTOCOL_VERSION_3() ) || ( $self->{_initial_packet_size} != _OLD_INITIAL_PACKET_SIZE() ) ) { return Firefox::Marionette::Element->new( $self, %{ $self->_response_result_value($response) } ); } else { return Firefox::Marionette::Element->new( $self, ELEMENT => $self->_response_result_value($response) ); } } } sub active_frame { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetActiveFrame') ] ); my $response = $self->_get_response($message_id); if ( defined $self->_response_result_value($response) ) { if ( ref $self->_response_result_value($response) ) { return Firefox::Marionette::Element->new( $self, %{ $self->_response_result_value($response) } ); } else { return Firefox::Marionette::Element->new( $self, ELEMENT => $self->_response_result_value($response) ); } } else { return; } } sub title { my ($self) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetTitle') ] ); my $response = $self->_get_response($message_id); return $self->_response_result_value($response); } sub quit { my ( $self, $flags ) = @_; if ( !$self->alive() ) { my $socket = delete $self->{_socket}; if ($socket) { close $socket or Firefox::Marionette::Exception->throw( "Failed to close socket to firefox:$EXTENDED_OS_ERROR"); } $self->_terminate_xvfb(); } elsif ( $self->_socket() ) { if ( $self->_session_id() ) { $self->_quit_over_marionette($flags); delete $self->{session_id}; } $self->_terminate_process(); } if ( !$self->_reconnected() ) { if ( $self->ssh_local_directory() ) { File::Path::rmtree( $self->ssh_local_directory(), 0, 0 ); } elsif ( defined $self->root_directory() ) { File::Path::rmtree( $self->root_directory(), 0, 0 ); } } return $self->child_error(); } sub _quit_over_marionette { my ( $self, $flags ) = @_; $flags ||= ['eAttemptQuit']; # ["eConsiderQuit", "eAttemptQuit", "eForceQuit"] my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('Marionette:Quit'), { flags => $flags } ] ); my $response = $self->_get_response($message_id); my $socket = delete $self->{_socket}; if ( $OSNAME eq 'MSWin32' ) { if ( defined $self->{_win32_ssh_process} ) { $self->{_win32_ssh_process}->Wait( Win32::Process::INFINITE() ); $self->_reap(); } if ( defined $self->{_win32_firefox_process} ) { $self->{_win32_firefox_process}->Wait( Win32::Process::INFINITE() ); $self->_reap(); } } elsif ( ( $OSNAME eq 'MSWin32' ) && ( !$self->_ssh() ) ) { $self->{_win32_firefox_process}->Wait( Win32::Process::INFINITE() ); $self->_reap(); } else { if ( !$self->_is_firefox_major_version_at_least( _MIN_VERSION_FOR_MODERN_EXIT() ) ) { close $socket or Firefox::Marionette::Exception->throw( "Failed to close socket to firefox:$EXTENDED_OS_ERROR"); $socket = undef; } elsif ( $self->_ssh() ) { close $socket or Firefox::Marionette::Exception->throw( "Failed to close socket to firefox:$EXTENDED_OS_ERROR"); $socket = undef; } $self->_wait_for_firefox_to_exit(); } if ( defined $socket ) { close $socket or Firefox::Marionette::Exception->throw( "Failed to close socket to firefox:$EXTENDED_OS_ERROR"); } return; } sub _sandbox_regex { my ($self) = @_; return qr/security[.]sandbox[.](\w+)[.]tempDirSuffix/smx; } sub _sandbox_prefix { my ($self) = @_; return 'Temp-'; } sub _wait_for_firefox_to_exit { my ($self) = @_; if ( $self->_ssh() ) { if ( $self->_reconnected() ) { while ( $self->_remote_process_running( $self->_firefox_pid() ) ) { sleep 1; } } else { while ( kill 0, $self->_local_ssh_pid() ) { sleep 1; $self->_reap(); } } } elsif ( $OSNAME eq 'MSWin32' ) { $self->{_win32_firefox_process}->GetExitCode( my $exit_code ); while ( $exit_code == Win32::Process::STILL_ACTIVE() ) { sleep 1; $exit_code = $self->{_win32_firefox_process}->Kill(1); } } else { while ( kill 0, $self->_firefox_pid() ) { sleep 1; $self->_reap(); } } return; } sub _get_remote_root_directory { my ($self) = @_; $self->_initialise_remote_uname(); my $original_tmp_directory; { local %ENV = %ENV; delete $ENV{TMPDIR}; delete $ENV{TMP}; $original_tmp_directory = $self->_get_remote_environment_variable_via_ssh('TMPDIR') || $self->_get_remote_environment_variable_via_ssh('TMP') || '/tmp'; $original_tmp_directory =~ s/\/$//smx; # remove trailing / for darwin $self->{_original_remote_tmp_directory} = $original_tmp_directory; } my $name = File::Temp::mktemp('firefox_marionette_remote_XXXXXXXXXXX'); my $proposed_tmp_directory = $self->_remote_catfile( $original_tmp_directory, $name ); local $ENV{TMPDIR} = $proposed_tmp_directory; my $new_tmp_dir = $self->_get_remote_environment_variable_via_ssh('TMPDIR'); my $remote_root_directory; if ( ( defined $new_tmp_dir ) && ( $new_tmp_dir eq $proposed_tmp_directory ) ) { $remote_root_directory = $self->_make_remote_directory($new_tmp_dir); } else { $remote_root_directory = $self->_make_remote_directory( $self->_remote_catfile( $original_tmp_directory, $name ) ); } return $remote_root_directory; } sub _get_remote_environment_command { my ( $self, $name ) = @_; my $command; if ( ( $self->_remote_uname() ) && ( $self->_remote_uname() eq 'MSWin32' ) ) { $command = q[echo ] . $name . q[="%] . $name . q[%"]; } else { $command = 'echo "' . $name . q[=] . q[\\] . q["] . q[$] . $name . q[\\] . q[""]; } return $command; } sub _get_remote_environment_variable_via_ssh { my ( $self, $name ) = @_; my $value; my $output = $self->_execute_via_ssh( {}, $self->_get_remote_environment_command($name) ); if ( defined $output ) { foreach my $line ( split /\r?\n/smx, $output ) { if ( $line eq "$name=\"%$name%\"" ) { } elsif ( $line =~ /^$name="([^"]*)"$/smx ) { $value = $1; } } } return $value; } sub _cleanup_remote_filesystem { my ($self) = @_; if ( ( my $ssh = $self->_ssh() ) && ( defined $self->{_root_directory} ) ) { my $binary = 'rm'; my @parameters = ('-Rf'); if ( $self->_remote_uname() eq 'MSWin32' ) { $binary = 'rmdir'; @parameters = ( '/S', '/Q' ); } my @remote_directories = ( $self->{_root_directory} ); if ( $self->{_original_remote_tmp_directory} ) { foreach my $sandbox ( sort { $a cmp $b } keys %{ $ssh->{sandbox} } ) { push @remote_directories, $self->_remote_catfile( $self->{_original_remote_tmp_directory}, $self->_sandbox_prefix() . $ssh->{sandbox}->{$sandbox} ); } } if ( $self->_remote_uname() eq 'MSWin32' ) { foreach my $remote_directory (@remote_directories) { $self->_system( {}, 'ssh', $self->_ssh_arguments(), $self->_ssh_address(), ( join q[ ], 'if', 'exist', $remote_directory, 'rmdir', '/S', '/Q', $remote_directory ) ); } } else { $self->_system( {}, 'ssh', $self->_ssh_arguments(), $self->_ssh_address(), ( join q[ ], $binary, @parameters, @remote_directories ) ); } } return; } sub _terminate_master_control_via_ssh { my ($self) = @_; my $path = $self->_control_path(); if ( ( defined $path ) && ( -e $path ) ) { } elsif ( ( !defined $path ) || ( $OS_ERROR == POSIX::ENOENT() ) ) { return; } else { Firefox::Marionette::Exception->throw( "Failed to stat '$path':$EXTENDED_OS_ERROR"); } $self->_system( {}, 'ssh', $self->_ssh_arguments(), '-O', 'exit', $self->_ssh_address() ); return; } sub _terminate_process_via_ssh { my ($self) = @_; if ( $self->_reconnected() ) { } else { my $term_signal = $self->_signal_number('TERM') ; # https://support.mozilla.org/en-US/questions/752748 if ( $term_signal > 0 ) { my $count = 0; while (( $count < _NUMBER_OF_TERM_ATTEMPTS() ) && ( defined $self->_local_ssh_pid() ) && ( kill $term_signal, $self->_local_ssh_pid() ) ) { $count += 1; sleep 1; $self->_reap(); } } my $kill_signal = $self->_signal_number('KILL'); # no more mr nice guy if ( ( $kill_signal > 0 ) && ( defined $self->_local_ssh_pid() ) ) { while ( kill $kill_signal, $self->_local_ssh_pid() ) { sleep 1; $self->_reap(); } } } return; } sub _terminate_local_non_win32_process { my ($self) = @_; my $term_signal = $self->_signal_number('TERM') ; # https://support.mozilla.org/en-US/questions/752748 if ( $term_signal > 0 ) { my $count = 0; while (( $count < _NUMBER_OF_TERM_ATTEMPTS() ) && ( kill $term_signal, $self->_firefox_pid() ) ) { $count += 1; sleep 1; $self->_reap(); } } my $kill_signal = $self->_signal_number('KILL'); # no more mr nice guy if ( $kill_signal > 0 ) { while ( kill $kill_signal, $self->_firefox_pid() ) { sleep 1; $self->_reap(); } } return; } sub _terminate_local_win32_process { my ($self) = @_; if ( $self->{_win32_firefox_process} ) { $self->{_win32_firefox_process}->Kill(1); sleep 1; $self->{_win32_firefox_process}->GetExitCode( my $exit_code ); while ( $exit_code == Win32::Process::STILL_ACTIVE() ) { $self->{_win32_firefox_process}->Kill(1); sleep 1; $exit_code = $self->{_win32_firefox_process}->Kill(1); } $self->_reap(); } if ( $self->{_win32_ssh_process} ) { $self->{_win32_ssh_process}->Kill(1); sleep 1; $self->{_win32_ssh_process}->GetExitCode( my $exit_code ); while ( $exit_code == Win32::Process::STILL_ACTIVE() ) { $self->{_win32_ssh_process}->Kill(1); sleep 1; $exit_code = $self->{_win32_ssh_process}->Kill(1); } $self->_reap(); } foreach my $process ( @{ $self->{_other_win32_ssh_processes} } ) { $process->Kill(1); sleep 1; $process->GetExitCode( my $exit_code ); while ( $exit_code == Win32::Process::STILL_ACTIVE() ) { $process->Kill(1); sleep 1; $exit_code = $process->Kill(1); } $self->_reap(); } return; } sub _terminate_marionette_process { my ($self) = @_; if ( $OSNAME eq 'MSWin32' ) { $self->_terminate_local_win32_process(); } elsif ( my $ssh = $self->_ssh() ) { $self->_terminate_process_via_ssh(); } elsif ( ( $self->_firefox_pid() ) && ( kill 0, $self->_firefox_pid() ) ) { $self->_terminate_local_non_win32_process(); } return; } sub _terminate_process { my ($self) = @_; $self->_terminate_marionette_process(); $self->_terminate_xvfb(); return; } sub _terminate_xvfb { my ($self) = @_; if ( my $pid = $self->xvfb_pid() ) { my $int_signal = $self->_signal_number('INT'); while ( kill 0, $pid ) { kill $int_signal, $pid; sleep 1; $self->_reap(); } } return; } sub content { my ($self) = @_; $self->_context('content'); return $self; } sub chrome { my ($self) = @_; $self->_context('chrome'); return $self; } sub context { my ( $self, $new ) = @_; return $self->_context($new); } sub _context { my ( $self, $new ) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('Marionette:GetContext') ] ); my $response; eval { $response = $self->_get_response($message_id); } or do { Carp::carp( 'Retrieving context is not supported for Firefox ' . $self->browser_version() . q[:] . $EVAL_ERROR ); }; my $context; if ( defined $response ) { $context = $self->_response_result_value($response); # 'content' or 'chrome' } else { $context = $self->{'_context'} || 'content'; } $self->{'_context'} = $context; if ( defined $new ) { $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('Marionette:SetContext'), { value => $new } ] ); $response = $self->_get_response($message_id); $self->{'_context'} = $new; } return $context; } sub accept_connections { my ( $self, $new ) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('Marionette:AcceptConnections'), { value => $new ? \1 : \0 } ] ); my $response = $self->_get_response($message_id); return $self; } sub async_script { my ( $self, $script, %parameters ) = @_; %parameters = $self->_script_parameters( %parameters, script => $script ); my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:ExecuteAsyncScript'), {%parameters} ] ); return $self; } sub interactive { my ($self) = @_; if ( $self->loaded() ) { return 1; } else { return $self->script( 'if (document.readyState === "interactive") { return 1; } else { return 0 }' ); } } sub loaded { my ($self) = @_; return $self->script( 'if (document.readyState === "complete") { return 1; } else { return 0 }' ); } sub _script_parameters { my ( $self, %parameters ) = @_; my $script = delete $parameters{script}; if ( !$self->_is_script_missing_args_okay() ) { $parameters{args} ||= []; } if ( ( $parameters{args} ) && ( ref $parameters{args} ne 'ARRAY' ) ) { $parameters{args} = [ $parameters{args} ]; } my %mapping = ( timeout => 'scriptTimeout', new => 'newSandbox', ); foreach my $from ( sort { $a cmp $b } keys %mapping ) { my $to = $mapping{$from}; if ( defined $parameters{$to} ) { Carp::carp( "**** DEPRECATED PARAMETER - using $to as a parameter for the script(...) method HAS BEEN REPLACED BY the $from parameter ****" ); } elsif ( defined $parameters{$from} ) { $parameters{$to} = $parameters{$from}; delete $parameters{$from}; } } foreach my $key (qw(newSandbox)) { if ( defined $parameters{$key} ) { $parameters{$key} = $parameters{$key} ? \1 : \0; } } $parameters{script} = $script; if ( $self->_is_script_script_parameter_okay() ) { } else { $parameters{value} = $parameters{script}; } return %parameters; } sub script { my ( $self, $script, %parameters ) = @_; %parameters = $self->_script_parameters( %parameters, script => $script ); my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:ExecuteScript'), {%parameters} ] ); my $response = $self->_get_response($message_id); return $self->_check_for_and_translate_into_objects( $self->_response_result_value($response) ); } sub _get_any_class_from_variable { my ( $self, $object ) = @_; my $class; my $old_class; my $count = 0; foreach my $key ( sort { $a cmp $b } keys %{$object} ) { foreach my $known_class ( qw( Firefox::Marionette::Element Firefox::Marionette::ShadowRoot ) ) { if ( $key eq $known_class->IDENTIFIER() ) { $class = $known_class; } } if ( $key eq 'ELEMENT' ) { $old_class = 'Firefox::Marionette::Element'; } $count += 1; } if ( ( $count == 1 ) && ( defined $class ) ) { return $class; } elsif ( !$self->_is_using_webdriver_ids_exclusively() ) { if ( ( $count == 1 ) && ( defined $old_class ) ) { return $old_class; } elsif (( $count == 2 ) && ( defined $class ) ) { return $class; } else { foreach my $key ( sort { $a cmp $b } keys %{$object} ) { $object->{$key} = $self->_check_for_and_translate_into_objects( $object->{$key} ); } } } else { foreach my $key ( sort { $a cmp $b } keys %{$object} ) { $object->{$key} = $self->_check_for_and_translate_into_objects( $object->{$key} ); } } return; } sub _check_for_and_translate_into_objects { my ( $self, $value ) = @_; if ( my $ref = ref $value ) { if ( $ref eq 'HASH' ) { if ( my $class = $self->_get_any_class_from_variable($value) ) { my $instance = $class->new( $self, %{$value} ); return $instance; } } elsif ( $ref eq 'ARRAY' ) { my @objects; foreach my $object ( @{$value} ) { push @objects, $self->_check_for_and_translate_into_objects($object); } return \@objects; } } return $value; } sub json { my ($self) = @_; my $content = $self->strip(); my $json = JSON->new()->decode($content); return $json; } sub strip { my ($self) = @_; my $content = $self->html(); my $head_regex = qr/]+><\/head>/smx; my $script_regex = qr/(?:]+><\/script>)?/smx; my $header = qr/]*>$script_regex$head_regex
/smx;
    my $footer       = qr/<\/pre><\/body><\/html>/smx;
    $content =~ s/^$header(.*)$footer$/$1/smx;
    return $content;
}

sub html {
    my ($self) = @_;
    my $message_id = $self->_new_message_id();
    $self->_send_request(
        [
            _COMMAND(),
            $message_id,
            $self->_command('WebDriver:GetPageSource'),
            { sessionId => $self->_session_id() }
        ]
    );
    my $response = $self->_get_response($message_id);
    return $self->_response_result_value($response);
}

sub page_source {
    my ($self) = @_;
    Carp::carp(
        '**** DEPRECATED METHOD - page_source HAS BEEN REPLACED BY html ****');
    return $self->html();
}

sub back {
    my ($self) = @_;
    my $message_id = $self->_new_message_id();
    $self->_send_request(
        [ _COMMAND(), $message_id, $self->_command('WebDriver:Back') ] );
    my $response = $self->_get_response($message_id);
    return $self;
}

sub forward {
    my ($self) = @_;
    my $message_id = $self->_new_message_id();
    $self->_send_request(
        [ _COMMAND(), $message_id, $self->_command('WebDriver:Forward') ] );
    my $response = $self->_get_response($message_id);
    return $self;
}

sub screen_orientation {
    my ($self) = @_;
    my $message_id = $self->_new_message_id();
    $self->_send_request(
        [
            _COMMAND(), $message_id,
            $self->_command('Marionette:GetScreenOrientation')
        ]
    );
    my $response = $self->_get_response($message_id);
    return $self->_response_result_value($response);
}

sub switch_to_parent_frame {
    my ($self) = @_;
    my $message_id = $self->_new_message_id();
    $self->_send_request(
        [
            _COMMAND(), $message_id,
            $self->_command('WebDriver:SwitchToParentFrame')
        ]
    );
    my $response = $self->_get_response($message_id);
    return $self;
}

sub window_type {
    my ($self) = @_;
    my $message_id = $self->_new_message_id();
    $self->_send_request(
        [
            _COMMAND(), $message_id, $self->_command('Marionette:GetWindowType')
        ]
    );
    my $response = $self->_get_response($message_id);
    return $self->_response_result_value($response);
}

sub shadowy {
    my ( $self, $element ) = @_;
    if (
        $self->script(
q[if (arguments[0].shadowRoot) { return true } else { return false }],
            args => [$element]
        )
      )
    {
        return 1;
    }
    else {
        return 0;
    }
}

sub shadow_root {
    my ( $self, $element ) = @_;
    my $message_id = $self->_new_message_id();
    $self->_send_request(
        [
            _COMMAND(), $message_id,
            $self->_command('WebDriver:GetShadowRoot'),
            { id => $element->uuid() }
        ]
    );
    my $response = $self->_get_response($message_id);
    return Firefox::Marionette::ShadowRoot->new( $self,
        %{ $self->_response_result_value($response) } );
}

sub switch_to_shadow_root {
    my ( $self, $element ) = @_;
    my $message_id = $self->_new_message_id();
    $self->_send_request(
        [
            _COMMAND(), $message_id,
            $self->_command('WebDriver:SwitchToShadowRoot'),
            { id => $element->uuid() }
        ]
    );
    my $response = $self->_get_response($message_id);
    return $self;
}

sub switch_to_window {
    my ( $self, $window_handle ) = @_;
    my $message_id = $self->_new_message_id();
    $self->_send_request(
        [
            _COMMAND(),
            $message_id,
            $self->_command('WebDriver:SwitchToWindow'),
            {
                value  => "$window_handle",
                name   => "$window_handle",
                handle => "$window_handle",
            }
        ]
    );
    my $response = $self->_get_response($message_id);
    return $self;
}

sub switch_to_frame {
    my ( $self, $element ) = @_;
    my $message_id = $self->_new_message_id();
    my $parameters;
    if ( $self->marionette_protocol() == _MARIONETTE_PROTOCOL_VERSION_3() ) {
        $parameters = { element => $element->uuid() };
    }
    else {
        $parameters = { ELEMENT => $element->uuid() };
    }
    $self->_send_request(
        [
            _COMMAND(),                                 $message_id,
            $self->_command('WebDriver:SwitchToFrame'), $parameters,
        ]
    );
    my $response = $self->_get_response($message_id);
    return $self;
}

sub go {
    my ( $self, $uri ) = @_;
    my $message_id = $self->_new_message_id();
    $self->_send_request(
        [
            _COMMAND(),
            $message_id,
            $self->_command('WebDriver:Navigate'),
            {
                url       => "$uri",
                value     => "$uri",
                sessionId => $self->_session_id()
            }
        ]
    );
    my $response = $self->_get_response($message_id);
    return $self;
}

sub sleep_time_in_ms {
    my ( $self, $new ) = @_;
    my $old = $self->{sleep_time_in_ms} || 1;
    if ( defined $new ) {
        $self->{sleep_time_in_ms} = $new;
    }
    return $old;
}

sub bye {
    my ( $self, $code ) = @_;
    my $found = 1;
    while ($found) {
        eval { &{$code} } and do {
            Time::HiRes::sleep(
                $self->sleep_time_in_ms() / _MILLISECONDS_IN_ONE_SECOND() );
          }
          or do {
            if (
                ( ref $EVAL_ERROR )
                && (
                    (
                        ref $EVAL_ERROR eq
                        'Firefox::Marionette::Exception::NotFound'
                    )
                    || (
                        ref $EVAL_ERROR eq
                        'Firefox::Marionette::Exception::StaleElement' )
                )
              )
            {
                $found = 0;
            }
            else {
                Firefox::Marionette::Exception->throw($EVAL_ERROR);
            }
          };
    }
    return $self;
}

sub await {
    my ( $self, $code ) = @_;
    my $result;
    while ( !$result ) {
        $result = eval { &{$code} } or do {
            if (
                ( ref $EVAL_ERROR )
                && (
                    (
                        ref $EVAL_ERROR eq
                        'Firefox::Marionette::Exception::NotFound'
                    )
                    || (
                        ref $EVAL_ERROR eq
                        'Firefox::Marionette::Exception::StaleElement' )
                    || (
                        ref $EVAL_ERROR eq
                        'Firefox::Marionette::Exception::NoSuchAlert' )
                )
              )
            {
            }
            elsif ( ref $EVAL_ERROR ) {
                Firefox::Marionette::Exception->throw($EVAL_ERROR);
            }
        };
        if ( !$result ) {
            Time::HiRes::sleep(
                $self->sleep_time_in_ms() / _MILLISECONDS_IN_ONE_SECOND() );
        }
    }
    return $result;
}

sub developer {
    my ($self) = @_;
    $self->_initialise_version();
    if ( $self->{developer_edition} ) {
        return 1;
    }
    elsif (( defined $self->{_initial_version} )
        && ( $self->{_initial_version}->{minor} )
        && ( $self->{_initial_version}->{minor} =~ /b\d+$/smx ) )
    {
        return 1;
    }
    else {
        return 0;
    }
}

sub nightly {
    my ($self) = @_;
    $self->_initialise_version();
    if (   ( defined $self->{_initial_version} )
        && ( $self->{_initial_version}->{minor} )
        && ( $self->{_initial_version}->{minor} =~ /a\d+$/smx ) )
    {
        return 1;
    }
    else {
        return 0;
    }
}

sub _get_xpi_path {
    my ( $self, $path ) = @_;
    if ( File::Spec->file_name_is_absolute($path) ) {
    }
    else {
        $path = File::Spec->rel2abs($path);
    }
    my $xpi_path;
    if ( $path =~ /[.]xpi$/smx ) {
        $xpi_path = $path;
    }
    else {
        my $base_directory;
        my ( $volume, $directories, $name );
        if ( -d $path ) {
            ( $volume, $directories, $name ) =
              File::Spec->splitpath( $path, 1 );
            $base_directory = $path;
        }
        else {
            ( $volume, $directories, $name ) = File::Spec->splitpath($path);
            $base_directory = File::Spec->catdir( $volume, $directories );
            if ( $OSNAME eq 'cygwin' ) {
                $base_directory =~
                  s/^\/\//\//smx;   # seems to be a bug in File::Spec for cygwin
            }
        }
        my @directories = File::Spec->splitdir($directories);
        if ( $directories[-1] eq q[] ) {
            pop @directories;
        }
        my $xpi_name = $directories[-1];
        my $zip      = Archive::Zip->new();
        File::Find::find(
            {
                no_chdir => 1,
                wanted   => sub {
                    my $full_path = $File::Find::name;
                    my ( undef, undef, $file_name ) =
                      File::Spec->splitpath($path);
                    if ( $file_name !~ /^[.]/smx ) {
                        my $relative_path =
                          File::Spec->abs2rel( $full_path, $base_directory );
                        my $member;
                        if ( -d $full_path ) {
                            $member = $zip->addDirectory($relative_path);
                        }
                        else {
                            $member =
                              $zip->addFile( $full_path, $relative_path );
                            $member->desiredCompressionMethod(
                                Archive::Zip::COMPRESSION_DEFLATED() );
                        }
                    }

                }
            },
            $base_directory
        );
        $self->_build_local_extension_directory();
        $self->{extension_index} += 1;
        $xpi_path = File::Spec->catfile( $self->{_local_extension_directory},
            $self->{extension_index} . q[_] . $xpi_name . '.xpi' );
        $zip->writeToFileNamed($xpi_path) == Archive::Zip::AZ_OK()
          or Firefox::Marionette::Exception->throw(
            "Failed to write to $xpi_path:$EXTENDED_OS_ERROR");
    }
    return $xpi_path;
}

sub install {
    my ( $self, $path, $temporary ) = @_;
    my $xpi_path = $self->_get_xpi_path($path);
    my $actual_path;
    if ( $self->_ssh() ) {
        if ( !$self->{_addons_directory} ) {
            $self->{_addons_directory} =
              $self->_make_remote_directory(
                $self->_remote_catfile( $self->_root_directory(), 'addons' ) );
        }
        my ( $volume, $directories, $name ) =
          File::Spec->splitpath("$xpi_path");
        my $handle = FileHandle->new( $xpi_path, Fcntl::O_RDONLY() )
          or Firefox::Marionette::Exception->throw(
            "Failed to open $xpi_path for reading:$EXTENDED_OS_ERROR");
        binmode $handle;
        $actual_path =
          $self->_remote_catfile( $self->{_addons_directory}, $name );
        $self->_put_file_via_scp( $handle, $actual_path, 'addon ' . $name );
    }
    elsif ( $OSNAME eq 'cygwin' ) {
        $actual_path = $self->execute( 'cygpath', '-s', '-w', $xpi_path );
    }
    else {
        $actual_path = "$xpi_path";
    }
    my $message_id = $self->_new_message_id();
    $self->_send_request(
        [
            _COMMAND(),
            $message_id,
            $self->_command('Addon:Install'),
            {
                path      => $actual_path,
                temporary => $temporary ? \1 : \0
            }
        ]
    );
    my $response = $self->_get_response($message_id);
    $self->_clean_local_extension_directory();
    return $self->_response_result_value($response);
}

sub uninstall {
    my ( $self, $id ) = @_;
    my $message_id = $self->_new_message_id();
    $self->_send_request(
        [
            _COMMAND(), $message_id,
            $self->_command('Addon:Uninstall'), { id => $id }
        ]
    );
    my $response = $self->_get_response($message_id);
    return $self;
}

sub marionette_protocol {
    my ($self) = @_;
    return $self->{marionette_protocol} || 0;
}

sub application_type {
    my ($self) = @_;
    return $self->{application_type};
}

sub _session_id {
    my ($self) = @_;
    return $self->{session_id};
}

sub _new_message_id {
    my ($self) = @_;
    $self->{last_message_id} += 1;
    return $self->{last_message_id};
}

sub addons {
    my ($self) = @_;
    return $self->{addons};
}

sub _convert_request_to_old_protocols {
    my ( $self, $original ) = @_;
    my $new;
    if ( $self->marionette_protocol() == _MARIONETTE_PROTOCOL_VERSION_3() ) {
        $new = $original;
    }
    else {
        $new->{ $self->{_old_protocols_key} } =
          $original->[ _OLD_PROTOCOL_NAME_INDEX() ];
        $new->{parameters} = $original->[ _OLD_PROTOCOL_PARAMETERS_INDEX() ];
        if (   ( ref $new->{parameters} )
            && ( ( ref $new->{parameters} ) eq 'HASH' ) )
        {
            if ( defined $new->{parameters}->{id} ) {
                $new->{parameters}->{element} = $new->{parameters}->{id};
            }
            foreach my $key (
                sort { $a cmp $b }
                keys %{ $original->[ _OLD_PROTOCOL_PARAMETERS_INDEX() ] }
              )
            {
                next if ( $key eq $self->{_old_protocols_key} );
                $new->{$key} = $new->{parameters}->{$key};
            }
        }
    }
    return $new;
}

sub _send_request {
    my ( $self, $object ) = @_;
    $object = $self->_convert_request_to_old_protocols($object);
    my $encoder = JSON->new()->convert_blessed()->ascii();
    if ( $self->debug() ) {
        $encoder->canonical(1);
    }
    my $json   = $encoder->encode($object);
    my $length = length $json;
    if ( $self->debug() ) {
        warn ">> $length:$json\n";
    }
    my $result;
    if ( $self->alive() ) {
        $result = syswrite $self->_socket(), "$length:$json";
    }
    if ( !defined $result ) {
        my $socket_error = $EXTENDED_OS_ERROR;
        if ( $self->alive() ) {
            Firefox::Marionette::Exception->throw(
                "Failed to send request to firefox:$socket_error");
        }
        else {
            my $error_message =
              $self->error_message() ? $self->error_message() : q[];
            Firefox::Marionette::Exception->throw($error_message);
        }
    }
    return;
}

sub _read_from_socket {
    my ($self) = @_;
    my $number_of_bytes_in_response;
    my $initial_buffer;
    while ( ( !defined $number_of_bytes_in_response ) && ( $self->alive() ) ) {
        my $number_of_bytes = sysread $self->_socket(), my $octet, 1;
        if ( defined $number_of_bytes ) {
            $initial_buffer .= $octet;
        }
        else {
            my $socket_error = $EXTENDED_OS_ERROR;
            if ( $self->alive() ) {
                Firefox::Marionette::Exception->throw(
"Failed to read size of response from socket to firefox:$socket_error"
                );
            }
            else {
                my $error_message =
                  $self->error_message() ? $self->error_message() : q[];
                Firefox::Marionette::Exception->throw($error_message);
            }
        }
        if ( $initial_buffer =~ s/^(\d+)://smx ) {
            ($number_of_bytes_in_response) = ($1);
        }
    }
    if ( !defined $self->{_initial_packet_size} ) {
        $self->{_initial_packet_size} = $number_of_bytes_in_response;
    }
    my $number_of_bytes_already_read = 0;
    my $json                         = q[];
    while (( defined $number_of_bytes_in_response )
        && ( $number_of_bytes_already_read < $number_of_bytes_in_response )
        && ( $self->alive() ) )
    {
        my $number_of_bytes_read = sysread $self->_socket(), my $buffer,
          $number_of_bytes_in_response - $number_of_bytes_already_read;
        if ( defined $number_of_bytes_read ) {
            $json .= $buffer;
            $number_of_bytes_already_read += $number_of_bytes_read;
        }
        else {
            my $socket_error = $EXTENDED_OS_ERROR;
            if ( $self->alive() ) {
                Firefox::Marionette::Exception->throw(
"Failed to read response from socket to firefox:$socket_error"
                );
            }
            else {
                my $error_message =
                  $self->error_message() ? $self->error_message() : q[];
                Firefox::Marionette::Exception->throw($error_message);
            }
        }
    }
    if ( ( $self->debug() ) && ( defined $number_of_bytes_in_response ) ) {
        warn "<< $number_of_bytes_in_response:$json\n";
    }
    return $self->_decode_json($json);
}

sub _decode_json {
    my ( $self, $json ) = @_;
    my $parameters;
    eval { $parameters = JSON::decode_json($json); } or do {
        if ( $self->alive() ) {
            if ($EVAL_ERROR) {
                chomp $EVAL_ERROR;
                die "$EVAL_ERROR\n";
            }
        }
        else {
            my $error_message =
              $self->error_message() ? $self->error_message() : q[];
            Firefox::Marionette::Exception->throw($error_message);
        }
    };
    return $parameters;
}

sub _socket {
    my ($self) = @_;
    return $self->{_socket};
}

sub _get_response {
    my ( $self, $message_id, $parameters, $options ) = @_;
    my $next_message = $self->_read_from_socket();
    my $response =
      Firefox::Marionette::Response->new( $next_message, $parameters,
        $options );
    if ( $self->marionette_protocol() == _MARIONETTE_PROTOCOL_VERSION_3() ) {
        while ( $response->message_id() < $message_id ) {
            $next_message = $self->_read_from_socket();
            $response =
              Firefox::Marionette::Response->new( $next_message, $parameters );
        }
    }
    return $response;
}

sub _signal_number {
    my ( $proto, $name ) = @_;
    my %signals_by_name;
    my $idx = 0;
    foreach my $sig_name (@sig_names) {
        $signals_by_name{$sig_name} = $sig_nums[$idx];
        $idx += 1;
    }
    return $signals_by_name{$name};
}

sub DESTROY {
    my ($self) = @_;
    local $CHILD_ERROR = 0;
    if (   ( defined $self->{creation_pid} )
        && ( $self->{creation_pid} == $PROCESS_ID ) )
    {
        if ( $self->{survive} ) {
            if ( $self->_session_id() ) {
                $self->delete_session();
            }
        }
        else {
            $self->quit();
            if ( $self->_ssh() ) {
                $self->_cleanup_remote_filesystem();
                $self->_terminate_master_control_via_ssh();
            }
            $self->_cleanup_local_filesystem();
        }
    }
    return;
}

sub _cleanup_local_filesystem {
    my ($self) = @_;
    if ( $self->ssh_local_directory() ) {
        File::Path::rmtree( $self->ssh_local_directory(), 0, 0 );
    }
    delete $self->{_ssh_local_directory};
    if ( $self->_ssh() ) {
    }
    else {
        if ( $self->{_root_directory} ) {
            File::Path::rmtree( $self->{_root_directory}, 0, 0 );
        }
        delete $self->{_root_directory};
    }
    return;
}

1;    # Magic true value required at end of module
__END__

=head1 NAME

Firefox::Marionette - Automate the Firefox browser with the Marionette protocol

=head1 VERSION

Version 1.22

=head1 SYNOPSIS

    use Firefox::Marionette();
    use v5.10;

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');

    say $firefox->find_tag('title')->property('innerHTML'); # same as $firefox->title();

    say $firefox->html();

    $firefox->find_class('container-fluid')->find_id('metacpan_search-input')->type('Test::More');

    say "Height of search box is " . $firefox->find_class('container-fluid')->css('height');

    my $file_handle = $firefox->selfie();

    $firefox->find('//button[@name="lucky"]')->click();

    $firefox->await(sub { $firefox->interactive() && $firefox->find_partial('Download') })->click();

=head1 DESCRIPTION

This is a client module to automate the Mozilla Firefox browser via the L

=head1 SUBROUTINES/METHODS

=head2 accept_alert

accepts a currently displayed modal message box

=head2 accept_connections

Enables or disables accepting new socket connections.  By calling this method with false the server will not accept any further connections, but existing connections will not be forcible closed. Use true to re-enable accepting connections.

Please note that when closing the connection via the client you can end-up in a non-recoverable state if it hasn't been enabled before.

=head2 active_element

returns the active element of the current browsing context's document element, if the document element is non-null.

=head2 add_certificate

accepts a hash as a parameter and adds the specified certificate to the Firefox database with the supplied or default trust.  Allowed keys are below;

=over 4

=item * path - a file system path to a single L.

=item * string - a string containg a single L

=item * trust - This is the L value for L.  If defaults to 'C,,';

=back

This method returns L to aid in chaining methods.

    use Firefox::Marionette();

    my $pem_encoded_string = <<'_PEM_';
    -----BEGIN CERTIFICATE-----
    MII..
    -----END CERTIFICATE-----
    _PEM_
    my $firefox = Firefox::Marionette->new()->add_certificate(string => $pem_encoded_string);

=head2 add_cookie

accepts a single L object as the first parameter and adds it to the current cookie jar.  This method returns L to aid in chaining methods.

This method throws an exception if you try to L.

=head2 add_header

accepts a hash of HTTP headers to include in every future HTTP Request.

    use Firefox::Marionette();
    use UUID();

    my $firefox = Firefox::Marionette->new();
    my $uuid = UUID::uuid();
    $firefox->add_header( 'Track-my-automated-tests' => $uuid );
    $firefox->go('https://metacpan.org/');

these headers are added to any existing headers.  To clear headers, see the L method

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->delete_header( 'Accept' )->add_header( 'Accept' => 'text/perl' )->go('https://metacpan.org/');

will only send out an L header that looks like C.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->add_header( 'Accept' => 'text/perl' )->go('https://metacpan.org/');

by itself, will send out an L header that may resemble C. This method returns L to aid in chaining methods.

=head2 add_login

accepts a hash of the following keys;

=over 4

=item * host - The scheme + hostname of the page where the login applies, for example 'https://www.example.org'.

=item * user - The username for the login.

=item * password - The password for the login.

=item * origin - The scheme + hostname that the form-based login L.  Forms with no L default to submitting to the URL of the page containing the login form, so that is stored here. This field should be omitted (it will be set to undef) for http auth type authentications and "" means to match against any form action.

=item * realm - The HTTP Realm for which the login was requested. When an HTTP server sends a 401 result, the WWW-Authenticate header includes a realm. See L.  If the realm is not specified, or it was blank, the hostname is used instead. For HTML form logins, this field should not be specified.

=item * user_field - The name attribute for the username input in a form. Non-form logins should not specify this field.

=item * password_field - The name attribute for the password input in a form. Non-form logins should not specify this field.

=back

or a L object as the first parameter and adds the login to the Firefox login database.

    use Firefox::Marionette();
    use UUID();

    my $firefox = Firefox::Marionette->new();

    # for http auth logins

    my $http_auth_login = Firefox::Marionette::Login->new(host => 'https://pause.perl.org', user => 'AUSER', password => 'qwerty', realm => 'PAUSE');
    $firefox->add_login($http_auth_login);
    $firefox->go('https://pause.perl.org/pause/authenquery')->accept_alert(); # this goes to the page and submits the http auth popup

    # for form based login

    $firefox->add_login(host => 'https://github.com', origin => 'https://github.com', user => 'me@example.org', password => 'qwerty', user_field => 'login', password_field => 'password');
    my $form_login = Firefox::Marionette::Login(host => 'https://github.com', user => 'me2@example.org', password => 'uiop[]', user_field => 'login', password_field => 'password');

    # or just directly

    $firefox->add_login(host => 'https://github.com', user => 'me2@example.org', password => 'uiop[]', user_field => 'login', password_field => 'password');

This method returns L to aid in chaining methods.

=head2 add_site_header

accepts a host name and a hash of HTTP headers to include in every future HTTP Request that is being sent to that particular host.

    use Firefox::Marionette();
    use UUID();

    my $firefox = Firefox::Marionette->new();
    my $uuid = UUID::uuid();
    $firefox->add_site_header( 'metacpan.org', 'Track-my-automated-tests' => $uuid );
    $firefox->go('https://metacpan.org/');

these headers are added to any existing headers going to the metacpan.org site, but no other site.  To clear site headers, see the L method

=head2 addons

returns if pre-existing addons (extensions/themes) are allowed to run.  This will be true for Firefox versions less than 55, as -safe-mode cannot be automated.

=head2 alert_text

Returns the message shown in a currently displayed modal message box

=head2 alive

This method returns true or false depending on if the Firefox process is still running.

=head2 application_type

returns the application type for the Marionette protocol.  Should be 'gecko'.

=head2 async_script 

accepts a scalar containing a javascript function that is executed in the browser.  This method returns L to aid in chaining methods.

The executing javascript is subject to the L timeout, which, by default is 30 seconds.

=head2 attribute 

accepts an L as the first parameter and a scalar attribute name as the second parameter.  It returns the initial value of the attribute with the supplied name.  This method will return the initial content, the L method will return the current content.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    my $element = $firefox->find_id('search_input');
    !defined $element->attribute('value') or die "attribute is not defined!");
    $element->type('Test::More');
    !defined $element->attribute('value') or die "attribute is still not defined!");

=head2 await

accepts a subroutine reference as a parameter and then executes the subroutine.  If a L exception is thrown, this method will sleep for L milliseconds and then execute the subroutine again.  When the subroutine executes successfully, it will return what the subroutine returns.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new(sleep_time_in_ms => 5)->go('https://metacpan.org/');

    $firefox->find_id('metacpan_search-input')->type('Test::More');

    $firefox->find_name('lucky')->click();

    $firefox->await(sub { $firefox->interactive() && $firefox->find_partial('Download') })->click();

=head2 back

causes the browser to traverse one step backward in the joint history of the current browsing context.  The browser will wait for the one step backward to complete or the session's L duration to elapse before returning, which, by default is 5 minutes.  This method returns L to aid in chaining methods.

=head2 debug

accept a boolean and return the current value of the debug setting.  This allows the dynamic setting of debug.

=head2 default_binary_name

just returns the string 'firefox'.  Only of interest when sub-classing.

=head2 browser_version

This method returns the current version of firefox.

=head2 bye

accepts a subroutine reference as a parameter and then executes the subroutine.  If the subroutine executes successfully, this method will sleep for L milliseconds and then execute the subroutine again.  When a L exception is thrown, this method will return L to aid in chaining methods.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');

    $firefox->find_id('metacpan_search-input')->type('Test::More');

    $firefox->find_name('lucky')->click();

    $firefox->bye(sub { $firefox->find_name('lucky') })->await(sub { $firefox->interactive() && $firefox->find_partial('Download') })->click();

=head2 capabilities

returns the L of the current firefox binary.  You can retrieve L or a L with this method.

=head2 certificate_as_pem

accepts a L as a parameter and returns a L as a string.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new();

    # Generating a ca-bundle.crt to STDOUT from the current firefox instance

    foreach my $certificate (sort { $a->display_name() cmp $b->display_name } $firefox->certificates()) {
        if ($certificate->is_ca_cert()) {
            print '# ' . $certificate->display_name() . "\n" . $firefox->certificate_as_pem($certificate) . "\n";
        }
    }

The L command that is provided as part of this distribution does this.

=head2 certificates

returns a list of all known L.

    use Firefox::Marionette();
    use v5.10;

    # Sometimes firefox can neglect old certificates.  See https://bugzilla.mozilla.org/show_bug.cgi?id=1710716

    my $firefox = Firefox::Marionette->new();
    foreach my $certificate (grep { $_->is_ca_cert() && $_->not_valid_after() < time } $firefox->certificates()) {
        say "The " . $certificate->display_name() " . certificate has expired and should be removed";
    }

This method returns L to aid in chaining methods.

=head2 child_error

This method returns the $? (CHILD_ERROR) for the Firefox process, or undefined if the process has not yet exited.

=head2 chrome

changes the scope of subsequent commands to chrome context.  This allows things like interacting with firefox menu's and buttons outside of the browser window.

    use Firefox::Marionette();
    use v5.10;

    my $firefox = Firefox::Marionette->new()->chrome();
    $firefox->script(...); # running script in chrome context
    $firefox->content();

See the L method for an alternative methods for changing the context.

=head2 chrome_window_handle

returns an server-assigned integer identifiers for the current chrome window that uniquely identifies it within this Marionette instance.  This can be used to switch to this window at a later point. This corresponds to a window that may itself contain tabs.  This method is replaced by L and appropriate L calls for L.

=head2 chrome_window_handles

returns identifiers for each open chrome window for tests interested in managing a set of chrome windows and tabs separately.  This method is replaced by L and appropriate L calls for L.

=head2 clear

accepts a L as the first parameter and clears any user supplied input

=head2 click

accepts a L as the first parameter and sends a 'click' to it.  The browser will wait for any page load to complete or the session's L duration to elapse before returning, which, by default is 5 minutes.  The L method is also used to choose an option in a select dropdown.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new(visible => 1)->go('https://ebay.com');
    my $select = $firefox->find_tag('select');
    foreach my $option ($select->find_tag('option')) {
        if ($option->property('value') == 58058) { # Computers/Tablets & Networking
            $option->click();
        }
    }

=head2 close_current_chrome_window_handle

closes the current chrome window (that is the entire window, not just the tabs).  It returns a list of still available chrome window handles. You will need to L to use another window.

=head2 close_current_window_handle

closes the current window/tab.  It returns a list of still available window/tab handles.

=head2 content

changes the scope of subsequent commands to browsing context.  This is the default for when firefox starts and restricts commands to operating in the browser window only.

    use Firefox::Marionette();
    use v5.10;

    my $firefox = Firefox::Marionette->new()->chrome();
    $firefox->script(...); # running script in chrome context
    $firefox->content();

See the L method for an alternative methods for changing the context.

=head2 context

accepts a string as the first parameter, which may be either 'content' or 'chrome'.  It returns the context type that is Marionette's current target for browsing context scoped commands.

    use Firefox::Marionette();
    use v5.10;

    my $firefox = Firefox::Marionette->new();
    if ($firefox->context() eq 'content') {
       say "I knew that was going to happen";
    }
    my $old_context = $firefox->context('chrome');
    $firefox->script(...); # running script in chrome context
    $firefox->context($old_context);

See the L and L methods for alternative methods for changing the context.

=head2 cookies

returns the L of the cookie jar in scalar or list context.

    use Firefox::Marionette();
    use v5.10;

    my $firefox = Firefox::Marionette->new()->go('https://github.com');
    foreach my $cookie ($firefox->cookies()) {
        if (defined $cookie->same_site()) {
            say "Cookie " . $cookie->name() . " has a SameSite of " . $cookie->same_site();
        } else {
            warn "Cookie " . $cookie->name() . " does not have the SameSite attribute defined";
        }
    }

=head2 css

accepts an L as the first parameter and a scalar CSS property name as the second parameter.  It returns the value of the computed style for that property.

    use Firefox::Marionette();
    use v5.10;

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    say $firefox->find_id('metacpan_search-input')->css('height');

=head2 current_chrome_window_handle 

see L.

=head2 delete_certificate

accepts a L as a parameter and deletes/distrusts the certificate from the Firefox database.

    use Firefox::Marionette();
    use v5.10;

    my $firefox = Firefox::Marionette->new();
    foreach my $certificate ($firefox->certificates()) {
        if ($certificate->is_ca_cert()) {
            $firefox->delete_certificate($certificate);
        } else {
            say "This " . $certificate->display_name() " certificate is NOT a certificate authority, therefore it is not being deleted";
        }
    }
    say "Good luck visiting a HTTPS website!";

This method returns L to aid in chaining methods.

=head2 delete_cookie

deletes a single cookie by name.  Accepts a scalar containing the cookie name as a parameter.  This method returns L to aid in chaining methods.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://github.com');
    foreach my $cookie ($firefox->cookies()) {
        warn "Cookie " . $cookie->name() . " is being deleted";
        $firefox->delete_cookie($cookie->name());
    }
    foreach my $cookie ($firefox->cookies()) {
        die "Should be no cookies here now";
    }

=head2 delete_cookies

here be cookie monsters! This method returns L to aid in chaining methods.

=head2 delete_header

accepts a list of HTTP header names to delete from future HTTP Requests.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new();
    $firefox->delete_header( 'User-Agent', 'Accept', 'Accept-Encoding' );

will remove the L, L and L headers from all future requests

This method returns L to aid in chaining methods.

=head2 delete_login

accepts a L as a parameter.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new();
    foreach my $login ($firefox->logins()) {
        if ($login->user() eq 'me@example.org') {
            $firefox->delete_login($login);
        }
    }

will remove the logins with the username matching 'me@example.org'.

This method returns L to aid in chaining methods.

=head2 delete_logins

This method empties the password database.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new();
    $firefox->delete_logins();

This method returns L to aid in chaining methods.

=head2 delete_session

deletes the current WebDriver session.

=head2 delete_site_header

accepts a host name and a list of HTTP headers names to delete from future HTTP Requests.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new();
    $firefox->delete_header( 'metacpan.org', 'User-Agent', 'Accept', 'Accept-Encoding' );

will remove the L, L and L headers from all future requests to metacpan.org.

This method returns L to aid in chaining methods.

=head2 developer

returns true if the L of firefox is a L (does the minor version number end with an 'b\d+'?) version.

=head2 dismiss_alert

dismisses a currently displayed modal message box

=head2 download

accepts a filesystem path and returns a matching filehandle.  This is trivial for locally running firefox, but sufficiently complex to justify the method for a remote firefox running over ssh.

    use Firefox::Marionette();
    use v5.10;

    my $firefox = Firefox::Marionette->new( host => '10.1.2.3' )->go('https://metacpan.org/');

    $firefox->find_class('container-fluid')->find_id('metacpan_search-input')->type('Test::More');

    $firefox->find('//button[@name="lucky"]')->click();

    $firefox->await(sub { $firefox->interactive() && $firefox->find_partial('Download') })->click();

    while(!$firefox->downloads()) { sleep 1 }

    foreach my $path ($firefox->downloads()) {

        my $handle = $firefox->download($path);

        # do something with downloaded file handle

    }

=head2 downloading

returns true if any files in L end in C<.part>

    use Firefox::Marionette();
    use v5.10;

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');

    $firefox->find_class('container-fluid')->find_id('metacpan_search-input')->type('Test::More');

    $firefox->find('//button[@name="lucky"]')->click();

    $firefox->await(sub { $firefox->interactive() && $firefox->find_partial('Download') })->click();

    while(!$firefox->downloads()) { sleep 1 }

    while($firefox->downloading()) { sleep 1 }

    foreach my $path ($firefox->downloads()) {
        say $path;
    }

=head2 downloads

returns a list of file paths (including partial downloads) of downloads during this Firefox session.

    use Firefox::Marionette();
    use v5.10;

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');

    $firefox->find_class('container-fluid')->find_id('metacpan_search-input')->type('Test::More');

    $firefox->find('//button[@name="lucky"]')->click();

    $firefox->await(sub { $firefox->interactive() && $firefox->find_partial('Download') })->click();

    while(!$firefox->downloads()) { sleep 1 }

    foreach my $path ($firefox->downloads()) {
        say $path;
    }

=head2 error_message

This method returns a human readable error message describing how the Firefox process exited (assuming it started okay).  On Win32 platforms this information is restricted to exit code.

=head2 execute

This utility method executes a command with arguments and returns STDOUT as a chomped string.  It is a simple method only intended for the Firefox::Marionette::* modules.

=head2 fill_login

This method searchs the L for an appropriate login for any form on the current page.  The form must match the host, the action attribute and the user and password field names.

    use Firefox::Marionette();
    use IO::Prompt();

    my $firefox = Firefox::Marionette->new();

    my $firefox = Firefox::Marionette->new();

    my $url = 'https://github.com';

    my $user = 'me@example.org';

    my $password = IO::Prompt::prompt(-echo => q[*], "Please enter the password for the $user account when logging into $url:");

    $firefox->add_login(host => $url, user => $user, password => 'qwerty', user_field => 'login', password_field => 'password');

    $firefox->go("$url/login");

    $firefox->fill_login();

=head2 find

accepts an L as the first parameter and returns the first L that matches this expression.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');

    $firefox->find('//input[@id="metacpan_search-input"]')->type('Test::More');

    # OR in list context 

    foreach my $element ($firefox->find('//input[@id="metacpan_search-input"]')) {
        $element->type('Test::More');
    }

If no elements are found, a L exception will be thrown.  For the same functionality that returns undef if no elements are found, see the L method.

=head2 find_id

accepts an L as the first parameter and returns the first L with a matching 'id' property.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');

    $firefox->find_id('metacpan_search-input')->type('Test::More');

    # OR in list context 

    foreach my $element ($firefox->find_id('metacpan_search-input')) {
        $element->type('Test::More');
    }

If no elements are found, a L exception will be thrown.  For the same functionality that returns undef if no elements are found, see the L method.

=head2 find_name

This method returns the first L with a matching 'name' property.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    $firefox->find_name('q')->type('Test::More');

    # OR in list context 

    foreach my $element ($firefox->find_name('q')) {
        $element->type('Test::More');
    }

If no elements are found, a L exception will be thrown.  For the same functionality that returns undef if no elements are found, see the L method.

=head2 find_class

accepts a L as the first parameter and returns the first L with a matching 'class' property.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    $firefox->find_class('form-control home-metacpan_search-input')->type('Test::More');

    # OR in list context 

    foreach my $element ($firefox->find_class('form-control home-metacpan_search-input')) {
        $element->type('Test::More');
    }

If no elements are found, a L exception will be thrown.  For the same functionality that returns undef if no elements are found, see the L method.

=head2 find_selector

accepts a L as the first parameter and returns the first L that matches that selector.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    $firefox->find_selector('input.home-metacpan_search-input')->type('Test::More');

    # OR in list context 

    foreach my $element ($firefox->find_selector('input.home-metacpan_search-input')) {
        $element->type('Test::More');
    }

If no elements are found, a L exception will be thrown.  For the same functionality that returns undef if no elements are found, see the L method.

=head2 find_tag

accepts a L as the first parameter and returns the first L with this tag name.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    my $element = $firefox->find_tag('input');

    # OR in list context 

    foreach my $element ($firefox->find_tag('input')) {
        # do something
    }

If no elements are found, a L exception will be thrown. For the same functionality that returns undef if no elements are found, see the L method.

=head2 find_link

accepts a text string as the first parameter and returns the first link L that has a matching link text.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    $firefox->find_link('API')->click();

    # OR in list context 

    foreach my $element ($firefox->find_link('API')) {
        $element->click();
    }

If no elements are found, a L exception will be thrown.  For the same functionality that returns undef if no elements are found, see the L method.

=head2 find_partial

accepts a text string as the first parameter and returns the first link L that has a partially matching link text.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    $firefox->find_partial('AP')->click();

    # OR in list context 

    foreach my $element ($firefox->find_partial('AP')) {
        $element->click();
    }

If no elements are found, a L exception will be thrown.  For the same functionality that returns undef if no elements are found, see the L method.

=head2 forward

causes the browser to traverse one step forward in the joint history of the current browsing context. The browser will wait for the one step forward to complete or the session's L duration to elapse before returning, which, by default is 5 minutes.  This method returns L to aid in chaining methods.

=head2 full_screen

full screens the firefox window. This method returns L to aid in chaining methods.

=head2 go

Navigates the current browsing context to the given L and waits for the document to load or the session's L duration to elapse before returning, which, by default is 5 minutes.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new();
    $firefox->go('https://metacpan.org/'); # will only return when metacpan.org is FULLY loaded (including all images / js / css)

To make the L method return quicker, you need to set the L L to an appropriate value, such as below;

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new( capabilities => Firefox::Marionette::Capabilities->new( page_load_strategy => 'eager' ));
    $firefox->go('https://metacpan.org/'); # will return once the main document has been loaded and parsed, but BEFORE sub-resources (images/stylesheets/frames) have been loaded.

When going directly to a URL that needs to be downloaded, please see L for a necessary workaround.

This method returns L to aid in chaining methods.

=head2 har

returns a hashref representing the L of the session.  This function is subject to the L timeout, which, by default is 30 seconds.  It is also possible for the function to hang (until the L timeout) if the original L window is closed.  The hashref has been designed to be accepted by the L module.  This function should be considered experimental.  Feedback welcome.

    use Firefox::Marionette();
    use Archive::Har();
    use v5.10;

    my $firefox = Firefox::Marionette->new(visible => 1, debug => 1, har => 1);

    $firefox->go("http://metacpan.org/");

    $firefox->find('//input[@id="metacpan_search-input"]')->type('Test::More');
    $firefox->find_name('lucky')->click();

    my $har = Archive::Har->new();
    $har->hashref($firefox->har());

    foreach my $entry ($har->entries()) {
        say $entry->request()->url() . " spent " . $entry->timings()->connect() . " ms establishing a TCP connection";
    }

=head2 has

accepts an L as the first parameter and returns the first L that matches this expression.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');

    if (my $element = $firefox->has('//input[@id="metacpan_search-input"]')) {
        $element->type('Test::More');
    }

If no elements are found, this method will return undef.  For the same functionality that throws a L exception, see the L method.

=head2 has_id

accepts an L as the first parameter and returns the first L with a matching 'id' property.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');

    if (my $element = $firefox->has_id('metacpan_search-input')) {
        $element->type('Test::More');
    }

If no elements are found, this method will return undef.  For the same functionality that throws a L exception, see the L method.

=head2 has_name

This method returns the first L with a matching 'name' property.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    if (my $element = $firefox->has_name('q')) {
        $element->type('Test::More');
    }

If no elements are found, this method will return undef.  For the same functionality that throws a L exception, see the L method.

=head2 has_class

accepts a L as the first parameter and returns the first L with a matching 'class' property.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    if (my $element = $firefox->has_class('form-control home-metacpan_search-input')) {
        $element->type('Test::More');
    }

If no elements are found, this method will return undef.  For the same functionality that throws a L exception, see the L method.

=head2 has_selector

accepts a L as the first parameter and returns the first L that matches that selector.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    if (my $element = $firefox->has_selector('input.home-metacpan_search-input')) {
        $element->type('Test::More');
    }

If no elements are found, this method will return undef.  For the same functionality that throws a L exception, see the L method.

=head2 has_tag

accepts a L as the first parameter and returns the first L with this tag name.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    if (my $element = $firefox->has_tag('input')) {
        # do something
    }

If no elements are found, this method will return undef.  For the same functionality that throws a L exception, see the L method.

=head2 has_link

accepts a text string as the first parameter and returns the first link L that has a matching link text.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    if (my $element = $firefox->has_link('API')) {
        $element->click();
    }

If no elements are found, this method will return undef.  For the same functionality that throws a L exception, see the L method.

=head2 has_partial

accepts a text string as the first parameter and returns the first link L that has a partially matching link text.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    if (my $element = $firefox->find_partial('AP')) {
        $element->click();
    }

If no elements are found, this method will return undef.  For the same functionality that throws a L exception, see the L method.

=head2 html

returns the page source of the content document.  This page source can be wrapped in html that firefox provides.  See the L method for an alternative when dealing with response content types such as application/json and L for an alterative when dealing with other non-html content types such as text/plain.

    use Firefox::Marionette();
    use v5.10;

    say Firefox::Marionette->new()->go('https://metacpan.org/')->html();

=head2 images

returns a list of all of the following elements;

=over 4

=item * L

=item * L

=back

as L objects.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    if (my $link = $firefox->images()) {
        say "Found a image with width " . $image->width() . "px and height " . $image->height() . "px from " . $image->URL();
    }

If no elements are found, this method will return undef.

=head2 install

accepts the following as the first parameter;

=over 4

=item * path to an L.

=item * path to a directory containing L.  This directory will be packaged up as an unsigned xpi file.

=item * path to a top level file (such as L) in a directory containing L.  This directory will be packaged up as an unsigned xpi file.

=back

and an optional true/false second parameter to indicate if the xpi file should be a L (just for the existance of this browser instance).  Unsigned xpi files L (except for L).  It returns the GUID for the addon which may be used as a parameter to the L method.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new();

    my $extension_id = $firefox->install('/full/path/to/gnu_terry_pratchett-0.4-an+fx.xpi');

    # OR downloading and installing source code

    system { 'git' } 'git', 'clone', 'https://github.com/kkapsner/CanvasBlocker.git';

    if ($firefox->nightly()) {

        $extension_id = $firefox->install('./CanvasBlocker'); # permanent install for unsigned packages in nightly firefox

    } else {

        $extension_id = $firefox->install('./CanvasBlocker', 1); # temp install for normal firefox

    }

=head2 interactive

returns true if C or if L is true

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    $firefox->find_id('search_input')->type('Type::More');
    $firefox->find('//button[@name="lucky"]')->click();
    while(!$firefox->interactive()) {
        # redirecting to Test::More page
    }

=head2 is_displayed

accepts an L as the first parameter.  This method returns true or false depending on if the element L.

=head2 is_enabled

accepts an L as the first parameter.  This method returns true or false depending on if the element L.

=head2 is_selected

accepts an L as the first parameter.  This method returns true or false depending on if the element L.  Note that this method only makes sense for L or L inputs or L elements in a L dropdown.

=head2 json

returns a L object that has been parsed from the page source of the content document.  This is a convenience method that wraps the L method.

    use Firefox::Marionette();
    use v5.10;

    say Firefox::Marionette->new()->go('https://fastapi.metacpan.org/v1/download_url/Firefox::Marionette")->json()->{version};

=head2 key_down

accepts a parameter describing a key and returns an action for use in the L method that corresponding with that key being depressed.

    use Firefox::Marionette();
    use Firefox::Marionette::Keys qw(:all);

    my $firefox = Firefox::Marionette->new();

    $firefox->chrome()->perform(
                                 $firefox->key_down(CONTROL()),
                                 $firefox->key_down('l'),
                               )->release()->content();

=head2 key_up

accepts a parameter describing a key and returns an action for use in the L method that corresponding with that key being released.

    use Firefox::Marionette();
    use Firefox::Marionette::Keys qw(:all);

    my $firefox = Firefox::Marionette->new();

    $firefox->chrome()->perform(
                                 $firefox->key_down(CONTROL()),
                                 $firefox->key_down('l'),
                                 $firefox->pause(20),
                                 $firefox->key_up('l'),
                                 $firefox->key_up(CONTROL())
                               )->content();

=head2 loaded

returns true if C

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    $firefox->find_id('search_input')->type('Type::More');
    $firefox->find('//button[@name="lucky"]')->click();
    while(!$firefox->loaded()) {
        # redirecting to Test::More page
    }

=head2 logins_from_csv

accepts a filehandle as a parameter and then reads the filehandle for exported logins as CSV.  This is known to work with the following formats;

=over 4

=item * L

=item * L

=item * L

=back

returns a list of L objects.

    use Firefox::Marionette();
    use FileHandle();

    my $handle = FileHandle->new('/path/to/last_pass.csv');
    my $firefox = Firefox::Marionette->new();
    foreach my $login (Firefox::Marionette->logins_from_csv($handle)) {
        $firefox->add_login($login);
    }

=head2 logins_from_zip

accepts a filehandle as a parameter and then reads the filehandle for exported logins as a zip file.  This is known to work with the following formats;

=over 4

=item * L<1Password Unencrypted Export format|https://support.1password.com/1pux-format/>

=back

returns a list of L objects.

    use Firefox::Marionette();
    use FileHandle();

    my $handle = FileHandle->new('/path/to/1Passwordv8.1pux');
    my $firefox = Firefox::Marionette->new();
    foreach my $login (Firefox::Marionette->logins_from_zip($handle)) {
        $firefox->add_login($login);
    }

=head2 links

returns a list of all of the following elements;

=over 4

=item * L

=item * L

=item * L

=item * L

=item * L

=back

as L objects.

This method is subject to the L timeout, which, by default is 0 seconds.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    if (my $link = $firefox->links()) {
        if ($link->tag() eq 'a') {
            warn "Found a hyperlink to " . $link->URL();
        }
    }

If no elements are found, this method will return undef.

=head2 macos_binary_paths

returns a list of filesystem paths that this module will check for binaries that it can automate when running on L.  Only of interest when sub-classing.

=head2 marionette_protocol

returns the version for the Marionette protocol.  Current most recent version is '3'.

=head2 maximise

maximises the firefox window. This method returns L to aid in chaining methods.

=head2 mime_types

returns a list of MIME types that will be downloaded by firefox and made available from the L method

    use Firefox::Marionette();
    use v5.10;

    my $firefox = Firefox::Marionette->new(mime_types => [ 'application/pkcs10' ])

    foreach my $mime_type ($firefox->mime_types()) {
        say $mime_type;
    }

=head2 minimise

minimises the firefox window. This method returns L to aid in chaining methods.

=head2 mouse_down

accepts a parameter describing which mouse button the method should apply to (L, L or L) and returns an action for use in the L method that corresponding with a mouse button being depressed.

=head2 mouse_move

accepts a L parameter, or a C<( x =E 0, y =E 0 )> type hash manually describing exactly where to move the mouse to and returns an action for use in the L method that corresponding with such a mouse movement, either to the specified co-ordinates or to the middle of the supplied L parameter.  Other parameters that may be passed are listed below;

=over 4

=item * origin - the origin of the C( 0, y =E 0)> co-ordinates.  Should be either C, C or an L.

=item * duration - Number of milliseconds over which to distribute the move. If not defined, the duration defaults to 0.

=back

This method returns L to aid in chaining methods.

=head2 mouse_up

accepts a parameter describing which mouse button the method should apply to (L, L or L) and returns an action for use in the L method that corresponding with a mouse button being released.

=head2 new
 
accepts an optional hash as a parameter.  Allowed keys are below;

=over 4

=item * addons - should any firefox extensions and themes be available in this session.  This defaults to "0".

=item * binary - use the specified path to the L binary, rather than the default path.

=item * capabilities - use the supplied L object, for example to set whether the browser should L or whether the browser should use a L.

=item * chatty - Firefox is extremely chatty on the network, including checking for the lastest malware/phishing sites, updates to firefox/etc.  This option is therefore off ("0") by default, however, it can be switched on ("1") if required.  Even with chatty switched off, L.  The only way to prevent this seems to be to set firefox.settings.services.mozilla.com to 127.0.0.1 via L.  NOTE: that this option only works when profile_name/profile is not specified.

=item * console - show the L when the browser is launched.  This defaults to "0" (off).

=item * debug - should firefox's debug to be available via STDERR. This defaults to "0". Any ssh connections will also be printed to STDERR.  This defaults to "0" (off).  This setting may be updated by the L method.

=item * developer - only allow a L to be launched. This defaults to "0" (off).

=item * devtools - begin the session with the L window opened in a separate window.

=item * height - set the L of the initial firefox window

=item * har - begin the session with the L window opened in a separate window.  The L addon will be loaded into the new session automatically, which means that -safe-mode will not be activated for this session AND this functionality will only be available for Firefox 61+.

=item * host - use L to create and automate firefox on the specified host.  See L.

=item * implicit - a shortcut to allow directly providing the L timeout, instead of needing to use timeouts from the capabilities parameter.  Overrides all longer ways.

=item * kiosk - start the browser in L mode.

=item * mime_types - any MIME types that Firefox will encounter during this session.  MIME types that are not specified will result in a hung browser (the File Download popup will appear).

=item * nightly - only allow a L to be launched.  This defaults to "0" (off).

=item * port - if the "host" parameter is also set, use L to create and automate firefox via the specified port.  See L.

=item * page_load - a shortcut to allow directly providing the L timeout, instead of needing to use timeouts from the capabilities parameter.  Overrides all longer ways.

=item * profile - create a new profile based on the supplied L.  NOTE: firefox ignores any changes made to the profile on the disk while it is running.

=item * profile_name - pick a specific existing profile to automate, rather than creating a new profile.  L refuses to allow more than one instance of a profile to run at the same time.  Profile names can be obtained by using the L method.  NOTE: firefox ignores any changes made to the profile on the disk while it is running.

=item * reconnect - an experimental parameter to allow a reconnection to firefox that a connection has been discontinued.  See the survive parameter.

=item * script - a shortcut to allow directly providing the L timeout, instead of needing to use timeouts from the capabilities parameter.  Overrides all longer ways.

=item * seer - this option is switched off "0" by default.  When it is switched on "1", it will activate the various speculative and pre-fetch options for firefox.  NOTE: that this option only works when profile_name/profile is not specified.

=item * sleep_time_in_ms - the amount of time (in milliseconds) that this module should sleep when unsuccessfully calling the subroutine provided to the L or L methods.  This defaults to "1" millisecond.

=item * survive - if this is set to a true value, firefox will not automatically exit when the object goes out of scope.  See the reconnect parameter for an experimental technique for reconnecting.

=item * trust - give a path to a L encoded as a L that will be trusted for this session.

=item * timeouts - a shortcut to allow directly providing a L object, instead of needing to use timeouts from the capabilities parameter.  Overrides the timeouts provided (if any) in the capabilities parameter.

=item * user - if the "host" parameter is also set, use L to create and automate firefox with the specified user.  See L.  The user will default to the current user name.

=item * visible - should firefox be visible on the desktop.  This defaults to "0".

=item * waterfox - only allow a binary that looks like a L to be launched.

=item * width - set the L of the initial firefox window

=back

This method returns a new C object, connected to an instance of L.  In a non MacOS/Win32/Cygwin environment, if necessary (no DISPLAY variable can be found and the visible parameter to the new method has been set to true) and possible (Xvfb can be executed successfully), this method will also automatically start an L instance.
 
    use Firefox::Marionette();

    my $remote_darwin_firefox = Firefox::Marionette->new(
                     debug => 1,
                     host => '10.1.2.3',
                     trust => '/path/to/root_ca.pem',
                     binary => '/Applications/Firefox.app/Contents/MacOS/firefox'
                                                        ); # start a temporary profile for a remote firefox and load a new CA into the temp profile
    ...

    foreach my $profile_name (Firefox::Marionette::Profile->names()) {
        my $firefox_with_existing_profile = Firefox::Marionette->new( profile_name => $profile_name, visible => 1 );
        ...
    }

=head2 new_window

accepts an optional hash as the parameter.  Allowed keys are below;

=over 4

=item * focus - a boolean field representing if the new window be opened in the foreground (focused) or background (not focused). Defaults to false.

=item * private - a boolean field representing if the new window should be a private window. Defaults to false.

=item * type - the type of the new window. Can be one of 'tab' or 'window'. Defaults to 'tab'.

=back

Returns the window handle for the new window.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new();

    my $window_handle = $firefox->new_window(type => 'tab');

    $firefox->switch_to_window($window_handle);

=head2 new_session

creates a new WebDriver session.  It is expected that the caller performs the necessary checks on the requested capabilities to be WebDriver conforming.  The WebDriver service offered by Marionette does not match or negotiate capabilities beyond type and bounds checks.

=head2 nightly

returns true if the L of firefox is a L (does the minor version number end with an 'a1'?)

=head2 paper_sizes 

returns a list of all the recognised names for paper sizes, such as A4 or LEGAL.

=head2 pause

accepts a parameter in milliseconds and returns a corresponding action for the L method that will cause a pause in the chain of actions given to the L method.

=head2 pdf

accepts a optional hash as the first parameter with the following allowed keys;

=over 4

=item * landscape - Paper orientation.  Boolean value.  Defaults to false

=item * margin - A hash describing the margins.  The hash may have the following optional keys, 'top', 'left', 'right' and 'bottom'.  All these keys are in cm and default to 1 (~0.4 inches)

=item * page - A hash describing the page.  The hash may have the following keys; 'height' and 'width'.  Both keys are in cm and default to US letter size.  See the 'size' key.

=item * page_ranges - A list of the pages to print. Available for L and after.

=item * print_background - Print background graphics.  Boolean value.  Defaults to false. 

=item * raw - rather than a file handle containing the PDF, the binary PDF will be returned.

=item * scale - Scale of the webpage rendering.  Defaults to 1.

=item * size - The desired size (width and height) of the pdf, specified by name.  See the page key for an alternative and the L method for a list of accepted page size names. 

=item * shrink_to_fit - Whether or not to override page size as defined by CSS.  Boolean value.  Defaults to true. 

=back

returns a L object containing a PDF encoded version of the current page for printing.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    my $handle = $firefox->pdf();
    foreach my $paper_size ($firefox->paper_sizes()) {
	    $handle = $firefox->pdf(size => $paper_size, landscape => 1, margin => { top => 0.5, left => 1.5 });
            ...
	    print $firefox->pdf(page => { width => 21, height => 27 }, raw => 1);
            ...
    }

=head2 perform

accepts a list of actions (see L, L, L, L, L and L) and performs these actions in sequence.  This allows fine control over interactions, including sending right clicks to the browser and sending Control, Alt and other special keys.  The L method will complete outstanding actions (such as L or L actions).

    use Firefox::Marionette();
    use Firefox::Marionette::Keys qw(:all);
    use Firefox::Marionette::Buttons qw(:all);

    my $firefox = Firefox::Marionette->new();

    $firefox->chrome()->perform(
                                 $firefox->key_down(CONTROL()),
                                 $firefox->key_down('l'),
                                 $firefox->key_up('l'),
                                 $firefox->key_up(CONTROL())
                               )->content();

    $firefox->go('https://metacpan.org');
    my $help_button = $firefox->find_class('btn search-btn help-btn');
    $firefox->perform(
			          $firefox->mouse_move($help_button),
			          $firefox->mouse_down(RIGHT_BUTTON()),
			          $firefox->pause(4),
			          $firefox->mouse_up(RIGHT_BUTTON()),
		);

See the L method for an alternative for manually specifying all the L and L methods

=head2 profile_directory

returns the profile directory used by the current instance of firefox.  This is mainly intended for debugging firefox.  Firefox is not designed to cope with these files being altered while firefox is running.

=head2 property

accepts an L as the first parameter and a scalar attribute name as the second parameter.  It returns the current value of the property with the supplied name.  This method will return the current content, the L method will return the initial content.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    my $element = $firefox->find_id('search_input');
    $element->property('value') eq '' or die "Initial property is the empty string";
    $element->type('Test::More');
    $element->property('value') eq 'Test::More' or die "This property should have changed!";

    # OR getting the innerHTML property

    my $title = $firefox->find_tag('title')->property('innerHTML'); # same as $firefox->title();

=head2 pwd_mgr_lock

Accepts a new L and locks the L with it.

    use Firefox::Marionette();
    use IO::Prompt();

    my $firefox = Firefox::Marionette->new();
    my $password = IO::Prompt::prompt(-echo => q[*], "Please enter the password for the Firefox Password Manager:");
    $firefox->pwd_mgr_lock($password);
    $firefox->pwd_mgr_logout();
    # now no-one can access the Password Manager Database without the value in $password

This method returns L to aid in chaining methods.

=head2 pwd_mgr_login

Accepts the L and allows the user to access the L.

    use Firefox::Marionette();
    use IO::Prompt();

    my $firefox = Firefox::Marionette->new( profile_name => 'default' );
    my $password = IO::Prompt::prompt(-echo => q[*], "Please enter the password for the Firefox Password Manager:");
    $firefox->pwd_mgr_login($password);
    ...
    # access the Password Database.
    ...
    $firefox->pwd_mgr_logout();
    ...
    # no longer able to access the Password Database.

This method returns L to aid in chaining methods.

=head2 pwd_mgr_logout

Logs the user out of being able to access the L.

    use Firefox::Marionette();
    use IO::Prompt();

    my $firefox = Firefox::Marionette->new( profile_name => 'default' );
    my $password = IO::Prompt::prompt(-echo => q[*], "Please enter the password for the Firefox Password Manager:");
    $firefox->pwd_mgr_login($password);
    ...
    # access the Password Database.
    ...
    $firefox->pwd_mgr_logout();
    ...
    # no longer able to access the Password Database.

This method returns L to aid in chaining methods.

=head2 pwd_mgr_needs_login

returns true or false if the L has been locked and needs a L to access it.

    use Firefox::Marionette();
    use IO::Prompt();

    my $firefox = Firefox::Marionette->new( profile_name => 'default' );
    if ($firefox->pwd_mgr_needs_login()) {
      my $password = IO::Prompt::prompt(-echo => q[*], "Please enter the password for the Firefox Password Manager:");
      $firefox->pwd_mgr_login($password);
    }

=head2 quit

Marionette will stop accepting new connections before ending the current session, and finally attempting to quit the application.  This method returns the $? (CHILD_ERROR) value for the Firefox process

=head2 rect

accepts a L as the first parameter and returns the current L of the L

=head2 refresh

refreshes the current page.  The browser will wait for the page to completely refresh or the session's L duration to elapse before returning, which, by default is 5 minutes.  This method returns L to aid in chaining methods.

=head2 release

completes any outstanding actions issued by the L method.

    use Firefox::Marionette();
    use Firefox::Marionette::Keys qw(:all);
    use Firefox::Marionette::Buttons qw(:all);

    my $firefox = Firefox::Marionette->new();

    $firefox->chrome()->perform(
                                 $firefox->key_down(CONTROL()),
                                 $firefox->key_down('l'),
                               )->release()->content();

    $firefox->go('https://metacpan.org');
    my $help_button = $firefox->find_class('btn search-btn help-btn');
    $firefox->perform(
			          $firefox->mouse_move($help_button),
			          $firefox->mouse_down(RIGHT_BUTTON()),
			          $firefox->pause(4),
		)->release();

=head2 restart

restarts the browser.  After the restart, L should be restored.  The same profile settings should be applied, but the current state of the browser (such as the L will be reset (like after a normal browser restart).  This method is primarily intended for use by the L method.  Not sure if this is useful by itself.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new();

    $firefox->restart(); # but why?

This method returns L to aid in chaining methods.

=head2 root_directory

this is the root directory for the current instance of firefox.  The directory may exist on a remote server.  For debugging purposes only.

=head2 screen_orientation

returns the current browser orientation.  This will be one of the valid primary orientation values 'portrait-primary', 'landscape-primary', 'portrait-secondary', or 'landscape-secondary'.  This method is only currently available on Android (Fennec).

=head2 script 

accepts a scalar containing a javascript function body that is executed in the browser, and an optional hash as a second parameter.  Allowed keys are below;

=over 4

=item * args - The reference to a list is the arguments passed to the function body.

=item * filename - Filename of the client's program where this script is evaluated.

=item * line - Line in the client's program where this script is evaluated.

=item * new - Forces the script to be evaluated in a fresh sandbox.  Note that if it is undefined, the script will normally be evaluted in a fresh sandbox.

=item * sandbox - Name of the sandbox to evaluate the script in.  The sandbox is cached for later re-use on the same L object if C is false.  If he parameter is undefined, the script is evaluated in a mutable sandbox.  If the parameter is "system", it will be evaluted in a sandbox with elevated system privileges, equivalent to chrome space.

=item * timeout - A timeout to override the default L timeout, which, by default is 30 seconds.

=back

Returns the result of the javascript function.  When a parameter is an L (such as being returned from a L type operation), the L method will automatically translate that into a javascript object.  Likewise, when the result being returned in a L method is an L it will be automatically translated into a L.

    use Firefox::Marionette();
    use v5.10;

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');

    if (my $element = $firefox->script('return document.getElementsByName("lucky")[0];')) {
        say "Lucky find is a " . $element->tag_name() . " element";
    }

    my $search_input = $firefox->find_id('metacpan_search-input');

    $firefox->script('arguments[0].style.backgroundColor = "red"', args => [ $search_input ]); # turn the search input box red

The executing javascript is subject to the L timeout, which, by default is 30 seconds.

=head2 selfie

returns a L object containing a lossless PNG image screenshot.  If an L is passed as a parameter, the screenshot will be restricted to the element.  

If an L is not passed as a parameter and the current L is 'chrome', a screenshot of the current viewport will be returned.

If an L is not passed as a parameter and the current L is 'content', a screenshot of the current frame will be returned.

The parameters after the L parameter are taken to be a optional hash with the following allowed keys;

=over 4

=item * hash - return a SHA256 hex encoded digest of the PNG image rather than the image itself

=item * full - take a screenshot of the whole document unless the first L parameter has been supplied.

=item * raw - rather than a file handle containing the screenshot, the binary PNG image will be returned.

=item * scroll - scroll to the L supplied

=item * highlights - a reference to a list containing L to draw a highlight around.  Not available in L onwards.

=back

=head2 send_alert_text

sends keys to the input field of a currently displayed modal message box

=head2 shadow_root

accepts an L as a parameter and returns it's L as a L object or throws an exception.

    use Firefox::Marionette();
    use Cwd();

    my $firefox = Firefox::Marionette->new()->go('file://' . Cwd::cwd() . '/t/data/elements.html');

    $firefox->find_class('add')->click();
    my $custom_square = $firefox->find_tag('custom-square');
    my $shadow_root = $firefox->shadow_root($custom_square);

    foreach my $element (@{$firefox->script('return arguments[0].children', args => [ $shadow_root ])}) {
        warn $element->tag_name();
    }

=head2 shadowy

accepts an L as a parameter and returns true if the element has a L or false otherwise.

    use Firefox::Marionette();
    use Cwd();

    my $firefox = Firefox::Marionette->new()->go('file://' . Cwd::cwd() . '/t/data/elements.html');
    $firefox->find_class('add')->click();
    my $custom_square = $firefox->find_tag('custom-square');
    if ($firefox->shadowy($custom_square)) {
        my $shadow_root = $firefox->find_tag('custom-square')->shadow_root();
        warn $firefox->script('return arguments[0].innerHTML', args => [ $shadow_root ]);
        ...
    }

This function will probably be used to see if the L method can be called on this element without raising an exception.

=head2 sleep_time_in_ms

accepts a new time to sleep in L or L methods and returns the previous time.  The default time is "1" millisecond.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new(sleep_time_in_ms => 5); # setting default time to 5 milliseconds

    my $old_time_in_ms = $firefox->sleep_time_in_ms(8); # setting default time to 8 milliseconds, returning 5 (milliseconds)

=head2 ssh_local_directory

returns the path to the local directory for the ssh connection (if any). For debugging purposes only.

=head2 strip

returns the page source of the content document after an attempt has been made to remove typical firefox html wrappers of non html content types such as text/plain and application/json.  See the L method for an alternative when dealing with response content types such as application/json and L for an alterative when dealing with html content types.  This is a convenience method that wraps the L method.

    use Firefox::Marionette();
    use JSON();
    use v5.10;

    say JSON::decode_json(Firefox::Marionette->new()->go("https://fastapi.metacpan.org/v1/download_url/Firefox::Marionette")->strip())->{version};

Note that this method will assume the bytes it receives from the L method are UTF-8 encoded and will translate accordingly, throwing an exception in the process if the bytes are not UTF-8 encoded.

=head2 switch_to_frame

accepts a L as a parameter and switches to it within the current window.

=head2 switch_to_parent_frame

set the current browsing context for future commands to the parent of the current browsing context

=head2 switch_to_window

accepts a window handle (either the result of L or a window name as a parameter and switches focus to this window.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new();
    $firefox->version
    my $original_window_uuid = $firefox->window_handle();
    $firefox->new_window( type => 'tab' );
    $firefox->new_window( type => 'window' );
    $firefox->switch_to_window($original_window_uuid);
    $firefox->go('https://metacpan.org');

=head2 tag_name

accepts a L object as the first parameter and returns the relevant tag name.  For example 'L' or 'L'.

=head2 text

accepts a L as the first parameter and returns the text that is contained by that element (if any)

=head2 timeouts

returns the current L for page loading, searching, and scripts.

=head2 title

returns the current L of the window.

=head2 type

accepts an L as the first parameter and a string as the second parameter.  It sends the string to the specified L in the current page, such as filling out a text box. This method returns L to aid in chaining methods.

=head2 update

queries the Update Services and applies any available updates.  L the browser if necessary to complete the update.  This function is experimental and currently has not been successfully tested on Win32 or MacOS.

    use Firefox::Marionette();
    use v5.10;

    my $firefox = Firefox::Marionette->new();

    my $update = $firefox->update();

    while($update->successful()) {
        $update = $firefox->update();
    }

    say "Updated to " . $update->display_version() . " - Build ID " . $update->build_id();

    $firefox->quit();

returns a L object that contains useful information about any updates that occurred.

=head2 uninstall

accepts the GUID for the addon to uninstall.  The GUID is returned when from the L method.  This method returns L to aid in chaining methods.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new();

    my $extension_id = $firefox->install('/full/path/to/gnu_terry_pratchett-0.4-an+fx.xpi');

    # do something

    $firefox->uninstall($extension_id); # not recommended to uninstall this extension IRL.

=head2 uri

returns the current L of current top level browsing context for Desktop.  It is equivalent to the javascript C

=head2 win32_organisation

accepts a parameter of a Win32 product name and returns the matching organisation.  Only of interest when sub-classing.

=head2 win32_product_names

returns a hash of known Windows product names (such as 'Mozilla Firefox') with priority orders.  The lower the priority will determine the order that this module will check for the existance of this product.  Only of interest when sub-classing.

=head2 window_handle

returns the current window's handle. On desktop this typically corresponds to the currently selected tab.  returns an opaque server-assigned identifier to this window that uniquely identifies it within this Marionette instance.  This can be used to switch to this window at a later point.

    use Firefox::Marionette();
    use 5.010;

    my $firefox = Firefox::Marionette->new();
    my $original_window_uuid = $firefox->window_handle();

=head2 window_handles

returns a list of top-level browsing contexts. On desktop this typically corresponds to the set of open tabs for browser windows, or the window itself for non-browser chrome windows.  Each window handle is assigned by the server and is guaranteed unique, however the return array does not have a specified ordering.

    use Firefox::Marionette();
    use 5.010;

    my $firefox = Firefox::Marionette->new();
    my $original_window_uuid = $firefox->window_handle();
    $firefox->new_window( type => 'tab' );
    $firefox->new_window( type => 'window' );
    say "There are " . $firefox->window_handles() . " tabs open in total";
    say "Across " . $firefox->chrome()->window_handles()->content() . " chrome windows";

=head2 window_rect

accepts an optional L as a parameter, sets the current browser window to that position and size and returns the previous L of the browser window.  If no parameter is supplied, it returns the current  L of the browser window.

=head2 window_type

returns the current window's type.  This should be 'navigator:browser'.

=head2 xvfb_pid

returns the pid of the xvfb process if it exists.

=head2 xvfb_display

returns the value for the DISPLAY environment variable if one has been generated for the xvfb environment.

=head2 xvfb_xauthority

returns the value for the XAUTHORITY environment variable if one has been generated for the xvfb environment

=head1 AUTOMATING THE FIREFOX PASSWORD MANAGER

This module allows you to login to a website without ever directly handling usernames and password details.  The Password Manager may be preloaded with appropriate passwords and locked, like so;

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new( profile_name => 'locked' ); # using a pre-built profile called 'locked'
    if ($firefox->pwd_mgr_needs_login()) {
        my $new_password = IO::Prompt::prompt(-echo => q[*], 'Enter the password for the locked profile:');
        $firefox->pwd_mgr_login($password);
    } else {
        my $new_password = IO::Prompt::prompt(-echo => q[*], 'Enter the new password for the locked profile:');
        $firefox->pwd_mgr_lock($password);
    }
    ...
    $firefox->pwd_mgr_logout();

Usernames and passwords (for both HTTP Authentication popups and HTML Form based logins) may be added, viewed and deleted.

    use WebService::HIBP();

    my $hibp = WebService::HIBP->new();

    $firefox->add_login(host => 'https://github.com', user => 'me@example.org', password => 'qwerty', user_field => 'login', password_field => 'password');
    $firefox->add_login(host => 'https://pause.perl.org', user => 'AUSER', password => 'qwerty', realm => 'PAUSE');
    ...
    foreach my $login ($firefox->logins()) {
        if ($hibp->password($login->password())) { # does NOT send the password to the HIBP webservice
            warn "HIBP reports that your password for the " . $login->user() " account at " . $login->host() . " has been found in a data breach";
            $firefox->delete_login($login); # how could this possibly help?
        }
    }

And used to fill in login prompts without explicitly knowing the account details.

    $firefox->go('https://pause.perl.org/pause/authenquery')->accept_alert(); # this goes to the page and submits the http auth popup

    $firefox->go('https://github.com/login')->fill_login(); # fill the login and password fields without needing to see them

=head1 REMOTE AUTOMATION OF FIREFOX VIA SSH

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new( host => 'remote.example.org', debug => 1 );
    $firefox->go('https://metacpan.org/');

    # OR specify a different user to login as ...
    
    my $firefox = Firefox::Marionette->new( host => 'remote.example.org', user => 'R2D2', debug => 1 );
    $firefox->go('https://metacpan.org/');

    # OR specify a different port to connect to
    
    my $firefox = Firefox::Marionette->new( host => 'remote.example.org', port => 2222, debug => 1 );
    $firefox->go('https://metacpan.org/');

This module has support for creating and automating an instance of Firefox on a remote node.  It has been tested against a number of operating systems, including recent version of L, OS X, and Linux and BSD distributions.  It expects to be able to login to the remote node via public key authentication.  It can be further secured via the L option in the L L file such as;

    no-agent-forwarding,no-pty,no-X11-forwarding,permitopen="127.0.0.1:*",command="/usr/local/bin/ssh-auth-cmd-marionette" ssh-rsa AAAA ... == user@server

As an example, the L command is provided as part of this distribution.

When using ssh, Firefox::Marionette will attempt to pass the L environment variable across the ssh connection to make cleanups easier.  In order to allow this, the L setting in the remote L should be set to allow TMPDIR, which will look like;

    AcceptEnv TMPDIR

This module uses L functionality when using L, for a useful speedup of executing remote commands.  Unfortunately, when using ssh to move from a L, L node to a remote environment, we cannot use L, because at this time, Windows L and therefore this type of automation is still possible, but slower than other client platforms.

=head1 DIAGNOSTICS

=over
 
=item C<< Failed to correctly setup the Firefox process >>

The module was unable to retrieve a session id and capabilities from Firefox when it requests a L as part of the initial setup of the connection to Firefox.

=item C<< Failed to correctly determined the Firefox process id through the initial connection capabilities >>
 
The module was found that firefox is reporting through it's L object a different process id than this module was using.  This is probably a bug in this module's logic.  Please report as described in the BUGS AND LIMITATIONS section below.
 
=item C<< '%s --version' did not produce output that could be parsed.  Assuming modern Marionette is available:%s >>
 
The Firefox binary did not produce a version number that could be recognised as a Firefox version number.
 
=item C<< Failed to create process from '%s':%s >>
 
The module was to start Firefox process in a Win32 environment.  Something is seriously wrong with your environment.
 
=item C<< Failed to redirect %s to %s:%s >>
 
The module was unable to redirect a file handle's output.  Something is seriously wrong with your environment.
 
=item C<< Failed to exec %s:%s >>
 
The module was unable to run the Firefox binary.  Check the path is correct and the current user has execute permissions.
 
=item C<< Failed to fork:%s >>
 
The module was unable to fork itself, prior to executing a command.  Check the current C for max number of user processes.
 
=item C<< Failed to open directory '%s':%s >>
 
The module was unable to open a directory.  Something is seriously wrong with your environment.
 
=item C<< Failed to close directory '%s':%s >>
 
The module was unable to close a directory.  Something is seriously wrong with your environment.
 
=item C<< Failed to open '%s' for writing:%s >>
 
The module was unable to create a file in your temporary directory.  Maybe your disk is full?
 
=item C<< Failed to open temporary file for writing:%s >>
 
The module was unable to create a file in your temporary directory.  Maybe your disk is full?
 
=item C<< Failed to close '%s':%s >>
 
The module was unable to close a file in your temporary directory.  Maybe your disk is full?
 
=item C<< Failed to close temporary file:%s >>
 
The module was unable to close a file in your temporary directory.  Maybe your disk is full?
 
=item C<< Failed to create temporary directory:%s >>
 
The module was unable to create a directory in your temporary directory.  Maybe your disk is full?
 
=item C<< Failed to clear the close-on-exec flag on a temporary file:%s >>
 
The module was unable to call fcntl using F_SETFD for a file in your temporary directory.  Something is seriously wrong with your environment.
 
=item C<< Failed to seek to start of temporary file:%s >>
 
The module was unable to seek to the start of a file in your temporary directory.  Something is seriously wrong with your environment.
 
=item C<< Failed to create a socket:%s >>
 
The module was unable to even create a socket.  Something is seriously wrong with your environment.
 
=item C<< Failed to connect to %s on port %d:%s >>
 
The module was unable to connect to the Marionette port.  This is probably a bug in this module's logic.  Please report as described in the BUGS AND LIMITATIONS section below.
 
=item C<< Firefox killed by a %s signal (%d) >>
 
Firefox crashed after being hit with a signal.  
 
=item C<< Firefox exited with a %d >>
 
Firefox has exited with an error code
 
=item C<< Failed to bind socket:%s >>
 
The module was unable to bind a socket to any port.  Something is seriously wrong with your environment.
 
=item C<< Failed to close random socket:%s >>
 
The module was unable to close a socket without any reads or writes being performed on it.  Something is seriously wrong with your environment.
 
=item C<< moz:headless has not been determined correctly >>
 
The module was unable to correctly determine whether Firefox is running in "headless" or not.  This is probably a bug in this module's logic.  Please report as described in the BUGS AND LIMITATIONS section below.
 
=item C<< %s method requires a Firefox::Marionette::Element parameter >>
 
This function was called incorrectly by your code.  Please supply a L parameter when calling this function.
 
=item C<< Failed to write to temporary file:%s >>
 
The module was unable to write to a file in your temporary directory.  Maybe your disk is full?

=item C<< Failed to close socket to firefox:%s >>
 
The module was unable to even close a socket.  Something is seriously wrong with your environment.
 
=item C<< Failed to send request to firefox:%s >>
 
The module was unable to perform a syswrite on the socket connected to firefox.  Maybe firefox crashed?
 
=item C<< Failed to read size of response from socket to firefox:%s >>
 
The module was unable to read from the socket connected to firefox.  Maybe firefox crashed?
 
=item C<< Failed to read response from socket to firefox:%s >>
 
The module was unable to read from the socket connected to firefox.  Maybe firefox crashed?
 
=back

=head1 CONFIGURATION AND ENVIRONMENT

Firefox::Marionette requires no configuration files or environment variables.  It will however use the DISPLAY and XAUTHORITY environment variables to try to connect to an X Server.
It will also use the HTTP_PROXY, HTTPS_PROXY, FTP_PROXY and ALL_PROXY environment variables as defaults if the session L do not specify proxy information.

=head1 DEPENDENCIES

Firefox::Marionette requires the following non-core Perl modules
 
=over
 
=item *
L
 
=item *
L

=item *
L
 
=back

=head1 INCOMPATIBILITIES

None reported.  Always interested in any products with marionette support that this module could be patched to work with.


=head1 BUGS AND LIMITATIONS

=head2 DOWNLOADING USING GO METHOD

When using the L method to go directly to a URL containing a downloadable file, Firefox can hang.  You can work around this by setting the L to C like below;

    #! /usr/bin/perl

    use strict;
    use warnings;
    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new( capabilities => Firefox::Marionette::Capabilities->new( page_load_strategy => 'none' ) );
    $firefox->go("https://github.com/david-dick/firefox-marionette/archive/refs/heads/master.zip");
    while(!$firefox->downloads()) { sleep 1 }
    while($firefox->downloading()) { sleep 1 }
    foreach my $path ($firefox->downloads()) {
        warn "$path has been downloaded";
    }
    $firefox->quit();

=head2 MISSING METHODS

Currently the following Marionette methods have not been implemented;

=over
 
=item * WebDriver:SetScreenOrientation

=back

To report a bug, or view the current list of bugs, please visit L

=head1 SEE ALSO

=over

=item *
L

=item *
L

=item *
L

=item *
L

=item *
L

=back

=head1 AUTHOR

David Dick  C<<  >>

=head1 ACKNOWLEDGEMENTS
 
Thanks to the entire Mozilla organisation for a great browser and to the team behind Marionette for providing an interface for automation.
 
Thanks to L for creating the L extension for Firefox.

Thanks to L for his L describing importing certificates into Firefox.

Thanks also to the authors of the documentation in the following sources;

=over 4

=item * L

=item * L

=item * L

=item * L

=item * L

=back

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2021, David Dick C<<  >>. All rights reserved.

This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself. See L.

The L module includes the L
extension which is licensed under the L.

=head1 DISCLAIMER OF WARRANTY

BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
NECESSARY SERVICING, REPAIR, OR CORRECTION.

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
Firefox-Marionette-1.22/lib/Waterfox/0000755000175000017500000000000014175144502016135 5ustar  davedaveFirefox-Marionette-1.22/lib/Waterfox/Marionette/0000755000175000017500000000000014175144502020244 5ustar  davedaveFirefox-Marionette-1.22/lib/Waterfox/Marionette/Profile.pm0000644000175000017500000001666514175143706022225 0ustar  davedavepackage Waterfox::Marionette::Profile;

use strict;
use warnings;
use English qw( -no_match_vars );
use File::Spec();
use parent qw(Firefox::Marionette::Profile);

BEGIN {
    if ( $OSNAME eq 'MSWin32' ) {
        require Win32;
    }
}
our $VERSION = '1.22';

sub profile_ini_directory {
    my ($class) = @_;
    my $profile_ini_directory;
    if ( $OSNAME eq 'darwin' ) {
        my $home_directory =
          ( getpwuid $EFFECTIVE_USER_ID )
          [ $class->SUPER::_GETPWUID_DIR_INDEX() ];
        defined $home_directory
          or Firefox::Marionette::Exception->throw(
            "Failed to execute getpwuid for $OSNAME:$EXTENDED_OS_ERROR");
        $profile_ini_directory = File::Spec->catdir( $home_directory, 'Library',
            'Application Support', 'Waterfox' );
    }
    elsif ( $OSNAME eq 'MSWin32' ) {
        $profile_ini_directory =
          File::Spec->catdir( Win32::GetFolderPath( Win32::CSIDL_APPDATA() ),
            'Waterfox', 'Waterfox' );
    }
    elsif ( $OSNAME eq 'cygwin' ) {
        $profile_ini_directory =
          File::Spec->catdir( $ENV{APPDATA}, 'Waterfox', 'Waterfox' );
    }
    else {
        my $home_directory =
          ( getpwuid $EFFECTIVE_USER_ID )
          [ $class->SUPER::_GETPWUID_DIR_INDEX() ];
        defined $home_directory
          or Firefox::Marionette::Exception->throw(
            "Failed to execute getpwuid for $OSNAME:$EXTENDED_OS_ERROR");
        $profile_ini_directory =
          File::Spec->catdir( $home_directory, '.waterfox' );
    }
    return $profile_ini_directory;
}

sub new {
    my ( $class, %parameters ) = @_;
    my $profile = bless { comments => q[], keys => {} }, $class;
    $profile->set_value( 'bookmarks.initialized.pref', 'true', 0 );
    $profile->set_value( 'browser.bookmarks.restore_default_bookmarks',
        'false', 0 );
    $profile->set_value( 'browser.download.useDownloadDir', 'true', 0 );
    $profile->set_value( 'browser.download.folderList',     2,      0 )
      ;    # the last folder specified for a download
    $profile->set_value( 'browser.places.importBookmarksHTML',  'true',  0 );
    $profile->set_value( 'browser.reader.detectedFirstArticle', 'true',  0 );
    $profile->set_value( 'browser.shell.checkDefaultBrowser',   'false', 0 );
    $profile->set_value( 'browser.showQuitWarning',             'false', 0 );
    $profile->set_value( 'browser.startup.homepage', 'about:blank',      1 );
    $profile->set_value( 'browser.startup.homepage_override.mstone',
        'ignore', 1 );
    $profile->set_value( 'browser.startup.page',           '0',      0 );
    $profile->set_value( 'browser.tabs.warnOnClose',       'false',  0 );
    $profile->set_value( 'browser.warnOnQuit',             'false',  0 );
    $profile->set_value( 'devtools.jsonview.enabled',      'false',  0 );
    $profile->set_value( 'devtools.netmonitor.persistlog', 'true',   0 );
    $profile->set_value( 'devtools.toolbox.host',          'window', 1 );
    $profile->set_value( 'dom.disable_open_click_delay',   0,        0 );
    $profile->set_value( 'extensions.installDistroAddons', 'false',  0 );
    $profile->set_value( 'focusmanager.testmode',          'true',   0 );
    $profile->set_value( 'marionette.port', $class->SUPER::ANY_PORT() );
    $profile->set_value( 'network.http.prompt-temp-redirect',    'false', 0 );
    $profile->set_value( 'network.http.request.max-start-delay', '0',     0 );
    $profile->set_value( 'security.osclientcerts.autoload',      'true',  0 );
    $profile->set_value( 'signon.autofillForms',                 'false', 0 );
    $profile->set_value( 'signon.rememberSignons',               'false', 0 );
    $profile->set_value( 'startup.homepage_welcome_url', 'about:blank',   1 );
    $profile->set_value( 'startup.homepage_welcome_url.additional',
        'about:blank', 1 );

    if ( !$parameters{chatty} ) {
        $profile->set_value( 'app.update.auto',             'false', 0 );
        $profile->set_value( 'app.update.staging.enabled',  'false', 0 );
        $profile->set_value( 'app.update.checkInstallTime', 'false', 0 );
    }

    return $profile;
}

1;    # Magic true value required at end of module
__END__

=head1 NAME

Waterfox::Marionette::Profile - Represents a prefs.js Waterfox Profile

=head1 VERSION

Version 1.22

=head1 SYNOPSIS

    use Waterfox::Marionette();
    use v5.10;

    my $profile = Waterfox::Marionette::Profile->new();

    $profile->set_value('browser.startup.homepage', 'https://duckduckgo.com');

    my $firefox = Waterfox::Marionette->new(profile => $profile);
	
    $firefox->quit();
	
    foreach my $profile_name (Waterfox::Marionette::Profile->names()) {
        # start firefox using a specific existing profile
        $firefox = Waterfox::Marionette->new(profile_name => $profile_name);
        $firefox->quit();

        # OR start a new browser with a copy of a specific existing profile

        $profile = Waterfox::Marionette::Profile->existing($profile_name);
        $firefox = Waterox::Marionette->new(profile => $profile);
        $firefox->quit();
    }

=head1 DESCRIPTION

This module handles the implementation of a C Waterfox Profile.  This module inherits from L.

=head1 SUBROUTINES/METHODS

For a full list of methods available, see L

=head2 new

returns a new L.

=head2 profile_ini_directory

returns the base directory for profiles.

=head1 DIAGNOSTICS

See L.

=head1 CONFIGURATION AND ENVIRONMENT

Waterfox::Marionette::Profile requires no configuration files or environment variables.

=head1 DEPENDENCIES

Waterfox::Marionette::Profile requires no non-core Perl modules
 
=head1 INCOMPATIBILITIES

None reported.

=head1 BUGS AND LIMITATIONS

To report a bug, or view the current list of bugs, please visit L

=head1 AUTHOR

David Dick  C<<  >>

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2021, David Dick C<<  >>. All rights reserved.

This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself. See L.

=head1 DISCLAIMER OF WARRANTY

BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
NECESSARY SERVICING, REPAIR, OR CORRECTION.

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
Firefox-Marionette-1.22/lib/Waterfox/Marionette.pm0000644000175000017500000001303514175143706020611 0ustar  davedavepackage Waterfox::Marionette;

use warnings;
use strict;
use English qw( -no_match_vars );
use Waterfox::Marionette::Profile();
use base qw(Firefox::Marionette);
Firefox::Marionette->import(qw(:all));

our @EXPORT_OK =
  qw(BY_XPATH BY_ID BY_NAME BY_TAG BY_CLASS BY_SELECTOR BY_LINK BY_PARTIAL);
our %EXPORT_TAGS = ( all => \@EXPORT_OK );

our $VERSION = '1.22';

sub default_binary_name {
    return 'waterfox';
}

sub macos_binary_paths {
    my ($self) = @_;
    return (
        '/Applications/Waterfox Current.app/Contents/MacOS/waterfox',
        '/Applications/Waterfox Classic.app/Contents/MacOS/waterfox',
    );
}

my %_known_win32_organisations = (
    'Waterfox'         => 'Waterfox',
    'Waterfox Current' => 'Waterfox',
    'Waterfox Classic' => 'Waterfox',
);

sub win32_organisation {
    my ( $self, $name ) = @_;
    return $_known_win32_organisations{$name};
}

sub win32_product_names {
    my ($self) = @_;
    my %known_win32_preferred_names = (
        'Waterfox'         => 1,
        'Waterfox Current' => 2,
        'Waterfox Classic' => 3,
    );
    return %known_win32_preferred_names;
}

1;    # Magic true value required at end of module
__END__
=head1 NAME

Waterfox::Marionette - Automate the Waterfox browser with the Marionette protocol

=head1 VERSION

Version 1.22

=head1 SYNOPSIS

    use Waterfox::Marionette();
    use v5.10;

    my $waterfox = Waterfox::Marionette->new()->go('https://metacpan.org/');

    say $waterfox->find_tag('title')->property('innerHTML'); # same as $waterfox->title();

    say $waterfox->html();

    $waterfox->find_class('container-fluid')->find_id('search-input')->type('Test::More');

    say "Height of search box is " . $waterfox->find_class('container-fluid')->css('height');

    my $file_handle = $waterfox->selfie();

    $waterfox->find('//button[@name="lucky"]')->click();

    $waterfox->await(sub { $waterfox->interactive() && $waterfox->find_partial('Download') })->click();

=head1 DESCRIPTION

This is a client module to automate the Waterfox browser via the L.

It inherits most of it's methods from L.

=head1 SUBROUTINES/METHODS

For a full list of methods available, see L

=head2 default_binary_name

just returns the string 'waterfox'.  See L.

=head2 macos_binary_paths

returns a list of filesystem paths that this module will check for binaries that it can automate when running on L.  See L.

=head2 win32_organisation

accepts a parameter of a Win32 product name and returns the matching organisation.  See L.

=head2 win32_product_names

returns a hash of known Windows product names (such as 'Waterfox') with priority orders.  See L.

=head1 DIAGNOSTICS

For diagnostics, see L

=head1 CONFIGURATION AND ENVIRONMENT

For configuration, see L

=head1 DEPENDENCIES

For dependencies, see L
 
=head1 INCOMPATIBILITIES

None reported.  Always interested in any products with marionette support that this module could be patched to work with.

=head1 BUGS AND LIMITATIONS

See L

=head1 AUTHOR

David Dick  C<<  >>

=head1 ACKNOWLEDGEMENTS
 
Thanks for the L
 
=head1 LICENSE AND COPYRIGHT

Copyright (c) 2021, David Dick C<<  >>. All rights reserved.

This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself. See L.

The L module includes the L
extension which is licensed under the L.

=head1 DISCLAIMER OF WARRANTY

BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
NECESSARY SERVICING, REPAIR, OR CORRECTION.

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
Firefox-Marionette-1.22/README0000644000175000017500000027506314175143706014472 0ustar  davedaveNAME

    Firefox::Marionette - Automate the Firefox browser with the Marionette
    protocol

VERSION

    Version 1.22

SYNOPSIS

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    
        say $firefox->find_tag('title')->property('innerHTML'); # same as $firefox->title();
    
        say $firefox->html();
    
        $firefox->find_class('container-fluid')->find_id('metacpan_search-input')->type('Test::More');
    
        say "Height of search box is " . $firefox->find_class('container-fluid')->css('height');
    
        my $file_handle = $firefox->selfie();
    
        $firefox->find('//button[@name="lucky"]')->click();
    
        $firefox->await(sub { $firefox->interactive() && $firefox->find_partial('Download') })->click();

DESCRIPTION

    This is a client module to automate the Mozilla Firefox browser via the
    Marionette protocol
    

SUBROUTINES/METHODS

 accept_alert

    accepts a currently displayed modal message box

 accept_connections

    Enables or disables accepting new socket connections. By calling this
    method with false the server will not accept any further connections,
    but existing connections will not be forcible closed. Use true to
    re-enable accepting connections.

    Please note that when closing the connection via the client you can
    end-up in a non-recoverable state if it hasn't been enabled before.

 active_element

    returns the active element of the current browsing context's document
    element, if the document element is non-null.

 add_certificate

    accepts a hash as a parameter and adds the specified certificate to the
    Firefox database with the supplied or default trust. Allowed keys are
    below;

      * path - a file system path to a single PEM encoded X.509 certificate
      .

      * string - a string containg a single PEM encoded X.509 certificate
      

      * trust - This is the trustargs
       value for NSS
      . If defaults to 'C,,';

    This method returns itself to aid in chaining methods.

        use Firefox::Marionette();
    
        my $pem_encoded_string = <<'_PEM_';
        -----BEGIN CERTIFICATE-----
        MII..
        -----END CERTIFICATE-----
        _PEM_
        my $firefox = Firefox::Marionette->new()->add_certificate(string => $pem_encoded_string);

 add_cookie

    accepts a single cookie object as the first parameter and adds it to
    the current cookie jar. This method returns itself to aid in chaining
    methods.

    This method throws an exception if you try to add a cookie for a
    different domain than the current document
    .

 add_header

    accepts a hash of HTTP headers to include in every future HTTP Request.

        use Firefox::Marionette();
        use UUID();
    
        my $firefox = Firefox::Marionette->new();
        my $uuid = UUID::uuid();
        $firefox->add_header( 'Track-my-automated-tests' => $uuid );
        $firefox->go('https://metacpan.org/');

    these headers are added to any existing headers. To clear headers, see
    the delete_header method

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->delete_header( 'Accept' )->add_header( 'Accept' => 'text/perl' )->go('https://metacpan.org/');

    will only send out an Accept
    
    header that looks like Accept: text/perl.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->add_header( 'Accept' => 'text/perl' )->go('https://metacpan.org/');

    by itself, will send out an Accept
    
    header that may resemble Accept:
    text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8,
    text/perl. This method returns itself to aid in chaining methods.

 add_login

    accepts a hash of the following keys;

      * host - The scheme + hostname of the page where the login applies,
      for example 'https://www.example.org'.

      * user - The username for the login.

      * password - The password for the login.

      * origin - The scheme + hostname that the form-based login was
      submitted to
      .
      Forms with no action attribute default to submitting to the URL of
      the page containing the login form, so that is stored here. This
      field should be omitted (it will be set to undef) for http auth type
      authentications and "" means to match against any form action.

      * realm - The HTTP Realm for which the login was requested. When an
      HTTP server sends a 401 result, the WWW-Authenticate header includes
      a realm. See RFC 2617
      . If the realm is not
      specified, or it was blank, the hostname is used instead. For HTML
      form logins, this field should not be specified.

      * user_field - The name attribute for the username input in a form.
      Non-form logins should not specify this field.

      * password_field - The name attribute for the password input in a
      form. Non-form logins should not specify this field.

    or a Firefox::Marionette::Login object as the first parameter and adds
    the login to the Firefox login database.

        use Firefox::Marionette();
        use UUID();
    
        my $firefox = Firefox::Marionette->new();
    
        # for http auth logins
    
        my $http_auth_login = Firefox::Marionette::Login->new(host => 'https://pause.perl.org', user => 'AUSER', password => 'qwerty', realm => 'PAUSE');
        $firefox->add_login($http_auth_login);
        $firefox->go('https://pause.perl.org/pause/authenquery')->accept_alert(); # this goes to the page and submits the http auth popup
    
        # for form based login
    
        $firefox->add_login(host => 'https://github.com', origin => 'https://github.com', user => 'me@example.org', password => 'qwerty', user_field => 'login', password_field => 'password');
        my $form_login = Firefox::Marionette::Login(host => 'https://github.com', user => 'me2@example.org', password => 'uiop[]', user_field => 'login', password_field => 'password');
    
        # or just directly
    
        $firefox->add_login(host => 'https://github.com', user => 'me2@example.org', password => 'uiop[]', user_field => 'login', password_field => 'password');

    This method returns itself to aid in chaining methods.

 add_site_header

    accepts a host name and a hash of HTTP headers to include in every
    future HTTP Request that is being sent to that particular host.

        use Firefox::Marionette();
        use UUID();
    
        my $firefox = Firefox::Marionette->new();
        my $uuid = UUID::uuid();
        $firefox->add_site_header( 'metacpan.org', 'Track-my-automated-tests' => $uuid );
        $firefox->go('https://metacpan.org/');

    these headers are added to any existing headers going to the
    metacpan.org site, but no other site. To clear site headers, see the
    delete_site_header method

 addons

    returns if pre-existing addons (extensions/themes) are allowed to run.
    This will be true for Firefox versions less than 55, as -safe-mode
    cannot be automated.

 alert_text

    Returns the message shown in a currently displayed modal message box

 alive

    This method returns true or false depending on if the Firefox process
    is still running.

 application_type

    returns the application type for the Marionette protocol. Should be
    'gecko'.

 async_script

    accepts a scalar containing a javascript function that is executed in
    the browser. This method returns itself to aid in chaining methods.

    The executing javascript is subject to the script timeout, which, by
    default is 30 seconds.

 attribute

    accepts an element as the first parameter and a scalar attribute name
    as the second parameter. It returns the initial value of the attribute
    with the supplied name. This method will return the initial content,
    the property method will return the current content.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        my $element = $firefox->find_id('search_input');
        !defined $element->attribute('value') or die "attribute is not defined!");
        $element->type('Test::More');
        !defined $element->attribute('value') or die "attribute is still not defined!");

 await

    accepts a subroutine reference as a parameter and then executes the
    subroutine. If a not found exception is thrown, this method will sleep
    for sleep_time_in_ms milliseconds and then execute the subroutine
    again. When the subroutine executes successfully, it will return what
    the subroutine returns.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new(sleep_time_in_ms => 5)->go('https://metacpan.org/');
    
        $firefox->find_id('metacpan_search-input')->type('Test::More');
    
        $firefox->find_name('lucky')->click();
    
        $firefox->await(sub { $firefox->interactive() && $firefox->find_partial('Download') })->click();

 back

    causes the browser to traverse one step backward in the joint history
    of the current browsing context. The browser will wait for the one step
    backward to complete or the session's page_load duration to elapse
    before returning, which, by default is 5 minutes. This method returns
    itself to aid in chaining methods.

 debug

    accept a boolean and return the current value of the debug setting.
    This allows the dynamic setting of debug.

 default_binary_name

    just returns the string 'firefox'. Only of interest when sub-classing.

 browser_version

    This method returns the current version of firefox.

 bye

    accepts a subroutine reference as a parameter and then executes the
    subroutine. If the subroutine executes successfully, this method will
    sleep for sleep_time_in_ms milliseconds and then execute the subroutine
    again. When a not found exception is thrown, this method will return
    itself to aid in chaining methods.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    
        $firefox->find_id('metacpan_search-input')->type('Test::More');
    
        $firefox->find_name('lucky')->click();
    
        $firefox->bye(sub { $firefox->find_name('lucky') })->await(sub { $firefox->interactive() && $firefox->find_partial('Download') })->click();

 capabilities

    returns the capabilities of the current firefox binary. You can
    retrieve timeouts or a proxy with this method.

 certificate_as_pem

    accepts a certificate stored in the Firefox database as a parameter and
    returns a PEM encoded X.509 certificate
     as a string.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new();
    
        # Generating a ca-bundle.crt to STDOUT from the current firefox instance
    
        foreach my $certificate (sort { $a->display_name() cmp $b->display_name } $firefox->certificates()) {
            if ($certificate->is_ca_cert()) {
                print '# ' . $certificate->display_name() . "\n" . $firefox->certificate_as_pem($certificate) . "\n";
            }
        }

    The ca-bundle-for-firefox command that is provided as part of this
    distribution does this.

 certificates

    returns a list of all known certificates in the Firefox database.

        use Firefox::Marionette();
        use v5.10;
    
        # Sometimes firefox can neglect old certificates.  See https://bugzilla.mozilla.org/show_bug.cgi?id=1710716
    
        my $firefox = Firefox::Marionette->new();
        foreach my $certificate (grep { $_->is_ca_cert() && $_->not_valid_after() < time } $firefox->certificates()) {
            say "The " . $certificate->display_name() " . certificate has expired and should be removed";
        }

    This method returns itself to aid in chaining methods.

 child_error

    This method returns the $? (CHILD_ERROR) for the Firefox process, or
    undefined if the process has not yet exited.

 chrome

    changes the scope of subsequent commands to chrome context. This allows
    things like interacting with firefox menu's and buttons outside of the
    browser window.

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new()->chrome();
        $firefox->script(...); # running script in chrome context
        $firefox->content();

    See the context method for an alternative methods for changing the
    context.

 chrome_window_handle

    returns an server-assigned integer identifiers for the current chrome
    window that uniquely identifies it within this Marionette instance.
    This can be used to switch to this window at a later point. This
    corresponds to a window that may itself contain tabs. This method is
    replaced by window_handle and appropriate context calls for Firefox 94
    and after
    .

 chrome_window_handles

    returns identifiers for each open chrome window for tests interested in
    managing a set of chrome windows and tabs separately. This method is
    replaced by window_handles and appropriate context calls for Firefox 94
    and after
    .

 clear

    accepts a element as the first parameter and clears any user supplied
    input

 click

    accepts a element as the first parameter and sends a 'click' to it. The
    browser will wait for any page load to complete or the session's
    page_load duration to elapse before returning, which, by default is 5
    minutes. The click method is also used to choose an option in a select
    dropdown.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new(visible => 1)->go('https://ebay.com');
        my $select = $firefox->find_tag('select');
        foreach my $option ($select->find_tag('option')) {
            if ($option->property('value') == 58058) { # Computers/Tablets & Networking
                $option->click();
            }
        }

 close_current_chrome_window_handle

    closes the current chrome window (that is the entire window, not just
    the tabs). It returns a list of still available chrome window handles.
    You will need to switch_to_window to use another window.

 close_current_window_handle

    closes the current window/tab. It returns a list of still available
    window/tab handles.

 content

    changes the scope of subsequent commands to browsing context. This is
    the default for when firefox starts and restricts commands to operating
    in the browser window only.

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new()->chrome();
        $firefox->script(...); # running script in chrome context
        $firefox->content();

    See the context method for an alternative methods for changing the
    context.

 context

    accepts a string as the first parameter, which may be either 'content'
    or 'chrome'. It returns the context type that is Marionette's current
    target for browsing context scoped commands.

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new();
        if ($firefox->context() eq 'content') {
           say "I knew that was going to happen";
        }
        my $old_context = $firefox->context('chrome');
        $firefox->script(...); # running script in chrome context
        $firefox->context($old_context);

    See the content and chrome methods for alternative methods for changing
    the context.

 cookies

    returns the contents of the cookie jar in scalar or list context.

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new()->go('https://github.com');
        foreach my $cookie ($firefox->cookies()) {
            if (defined $cookie->same_site()) {
                say "Cookie " . $cookie->name() . " has a SameSite of " . $cookie->same_site();
            } else {
                warn "Cookie " . $cookie->name() . " does not have the SameSite attribute defined";
            }
        }

 css

    accepts an element as the first parameter and a scalar CSS property
    name as the second parameter. It returns the value of the computed
    style for that property.

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        say $firefox->find_id('metacpan_search-input')->css('height');

 current_chrome_window_handle

    see chrome_window_handle.

 delete_certificate

    accepts a certificate stored in the Firefox database as a parameter and
    deletes/distrusts the certificate from the Firefox database.

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new();
        foreach my $certificate ($firefox->certificates()) {
            if ($certificate->is_ca_cert()) {
                $firefox->delete_certificate($certificate);
            } else {
                say "This " . $certificate->display_name() " certificate is NOT a certificate authority, therefore it is not being deleted";
            }
        }
        say "Good luck visiting a HTTPS website!";

    This method returns itself to aid in chaining methods.

 delete_cookie

    deletes a single cookie by name. Accepts a scalar containing the cookie
    name as a parameter. This method returns itself to aid in chaining
    methods.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://github.com');
        foreach my $cookie ($firefox->cookies()) {
            warn "Cookie " . $cookie->name() . " is being deleted";
            $firefox->delete_cookie($cookie->name());
        }
        foreach my $cookie ($firefox->cookies()) {
            die "Should be no cookies here now";
        }

 delete_cookies

    here be cookie monsters! This method returns itself to aid in chaining
    methods.

 delete_header

    accepts a list of HTTP header names to delete from future HTTP
    Requests.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new();
        $firefox->delete_header( 'User-Agent', 'Accept', 'Accept-Encoding' );

    will remove the User-Agent
    ,
    Accept
     and
    Accept-Encoding
    
    headers from all future requests

    This method returns itself to aid in chaining methods.

 delete_login

    accepts a login as a parameter.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new();
        foreach my $login ($firefox->logins()) {
            if ($login->user() eq 'me@example.org') {
                $firefox->delete_login($login);
            }
        }

    will remove the logins with the username matching 'me@example.org'.

    This method returns itself to aid in chaining methods.

 delete_logins

    This method empties the password database.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new();
        $firefox->delete_logins();

    This method returns itself to aid in chaining methods.

 delete_session

    deletes the current WebDriver session.

 delete_site_header

    accepts a host name and a list of HTTP headers names to delete from
    future HTTP Requests.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new();
        $firefox->delete_header( 'metacpan.org', 'User-Agent', 'Accept', 'Accept-Encoding' );

    will remove the User-Agent
    ,
    Accept
     and
    Accept-Encoding
    
    headers from all future requests to metacpan.org.

    This method returns itself to aid in chaining methods.

 developer

    returns true if the current version of firefox is a developer edition
     (does the minor
    version number end with an 'b\d+'?) version.

 dismiss_alert

    dismisses a currently displayed modal message box

 download

    accepts a filesystem path and returns a matching filehandle. This is
    trivial for locally running firefox, but sufficiently complex to
    justify the method for a remote firefox running over ssh.

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new( host => '10.1.2.3' )->go('https://metacpan.org/');
    
        $firefox->find_class('container-fluid')->find_id('metacpan_search-input')->type('Test::More');
    
        $firefox->find('//button[@name="lucky"]')->click();
    
        $firefox->await(sub { $firefox->interactive() && $firefox->find_partial('Download') })->click();
    
        while(!$firefox->downloads()) { sleep 1 }
    
        foreach my $path ($firefox->downloads()) {
    
            my $handle = $firefox->download($path);
    
            # do something with downloaded file handle
    
        }

 downloading

    returns true if any files in downloads end in .part

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    
        $firefox->find_class('container-fluid')->find_id('metacpan_search-input')->type('Test::More');
    
        $firefox->find('//button[@name="lucky"]')->click();
    
        $firefox->await(sub { $firefox->interactive() && $firefox->find_partial('Download') })->click();
    
        while(!$firefox->downloads()) { sleep 1 }
    
        while($firefox->downloading()) { sleep 1 }
    
        foreach my $path ($firefox->downloads()) {
            say $path;
        }

 downloads

    returns a list of file paths (including partial downloads) of downloads
    during this Firefox session.

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    
        $firefox->find_class('container-fluid')->find_id('metacpan_search-input')->type('Test::More');
    
        $firefox->find('//button[@name="lucky"]')->click();
    
        $firefox->await(sub { $firefox->interactive() && $firefox->find_partial('Download') })->click();
    
        while(!$firefox->downloads()) { sleep 1 }
    
        foreach my $path ($firefox->downloads()) {
            say $path;
        }

 error_message

    This method returns a human readable error message describing how the
    Firefox process exited (assuming it started okay). On Win32 platforms
    this information is restricted to exit code.

 execute

    This utility method executes a command with arguments and returns
    STDOUT as a chomped string. It is a simple method only intended for the
    Firefox::Marionette::* modules.

 fill_login

    This method searchs the Password Manager
    
    for an appropriate login for any form on the current page. The form
    must match the host, the action attribute and the user and password
    field names.

        use Firefox::Marionette();
        use IO::Prompt();
    
        my $firefox = Firefox::Marionette->new();
    
        my $firefox = Firefox::Marionette->new();
    
        my $url = 'https://github.com';
    
        my $user = 'me@example.org';
    
        my $password = IO::Prompt::prompt(-echo => q[*], "Please enter the password for the $user account when logging into $url:");
    
        $firefox->add_login(host => $url, user => $user, password => 'qwerty', user_field => 'login', password_field => 'password');
    
        $firefox->go("$url/login");
    
        $firefox->fill_login();

 find

    accepts an xpath expression  as
    the first parameter and returns the first element that matches this
    expression.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    
        $firefox->find('//input[@id="metacpan_search-input"]')->type('Test::More');
    
        # OR in list context 
    
        foreach my $element ($firefox->find('//input[@id="metacpan_search-input"]')) {
            $element->type('Test::More');
        }

    If no elements are found, a not found exception will be thrown. For the
    same functionality that returns undef if no elements are found, see the
    has method.

 find_id

    accepts an id
    
    as the first parameter and returns the first element with a matching
    'id' property.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    
        $firefox->find_id('metacpan_search-input')->type('Test::More');
    
        # OR in list context 
    
        foreach my $element ($firefox->find_id('metacpan_search-input')) {
            $element->type('Test::More');
        }

    If no elements are found, a not found exception will be thrown. For the
    same functionality that returns undef if no elements are found, see the
    has_id method.

 find_name

    This method returns the first element with a matching 'name' property.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        $firefox->find_name('q')->type('Test::More');
    
        # OR in list context 
    
        foreach my $element ($firefox->find_name('q')) {
            $element->type('Test::More');
        }

    If no elements are found, a not found exception will be thrown. For the
    same functionality that returns undef if no elements are found, see the
    has_name method.

 find_class

    accepts a class name
    
    as the first parameter and returns the first element with a matching
    'class' property.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        $firefox->find_class('form-control home-metacpan_search-input')->type('Test::More');
    
        # OR in list context 
    
        foreach my $element ($firefox->find_class('form-control home-metacpan_search-input')) {
            $element->type('Test::More');
        }

    If no elements are found, a not found exception will be thrown. For the
    same functionality that returns undef if no elements are found, see the
    has_class method.

 find_selector

    accepts a CSS Selector
     as the
    first parameter and returns the first element that matches that
    selector.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        $firefox->find_selector('input.home-metacpan_search-input')->type('Test::More');
    
        # OR in list context 
    
        foreach my $element ($firefox->find_selector('input.home-metacpan_search-input')) {
            $element->type('Test::More');
        }

    If no elements are found, a not found exception will be thrown. For the
    same functionality that returns undef if no elements are found, see the
    has_selector method.

 find_tag

    accepts a tag name
     as
    the first parameter and returns the first element with this tag name.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        my $element = $firefox->find_tag('input');
    
        # OR in list context 
    
        foreach my $element ($firefox->find_tag('input')) {
            # do something
        }

    If no elements are found, a not found exception will be thrown. For the
    same functionality that returns undef if no elements are found, see the
    has_tag method.

 find_link

    accepts a text string as the first parameter and returns the first link
    element that has a matching link text.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        $firefox->find_link('API')->click();
    
        # OR in list context 
    
        foreach my $element ($firefox->find_link('API')) {
            $element->click();
        }

    If no elements are found, a not found exception will be thrown. For the
    same functionality that returns undef if no elements are found, see the
    has_link method.

 find_partial

    accepts a text string as the first parameter and returns the first link
    element that has a partially matching link text.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        $firefox->find_partial('AP')->click();
    
        # OR in list context 
    
        foreach my $element ($firefox->find_partial('AP')) {
            $element->click();
        }

    If no elements are found, a not found exception will be thrown. For the
    same functionality that returns undef if no elements are found, see the
    has_partial method.

 forward

    causes the browser to traverse one step forward in the joint history of
    the current browsing context. The browser will wait for the one step
    forward to complete or the session's page_load duration to elapse
    before returning, which, by default is 5 minutes. This method returns
    itself to aid in chaining methods.

 full_screen

    full screens the firefox window. This method returns itself to aid in
    chaining methods.

 go

    Navigates the current browsing context to the given URI and waits for
    the document to load or the session's page_load duration to elapse
    before returning, which, by default is 5 minutes.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new();
        $firefox->go('https://metacpan.org/'); # will only return when metacpan.org is FULLY loaded (including all images / js / css)

    To make the go method return quicker, you need to set the page load
    strategy capability to an appropriate value, such as below;

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new( capabilities => Firefox::Marionette::Capabilities->new( page_load_strategy => 'eager' ));
        $firefox->go('https://metacpan.org/'); # will return once the main document has been loaded and parsed, but BEFORE sub-resources (images/stylesheets/frames) have been loaded.

    When going directly to a URL that needs to be downloaded, please see
    BUGS AND LIMITATIONS for a necessary workaround.

    This method returns itself to aid in chaining methods.

 har

    returns a hashref representing the http archive
     of the session. This
    function is subject to the script timeout, which, by default is 30
    seconds. It is also possible for the function to hang (until the script
    timeout) if the original devtools
     window is closed. The
    hashref has been designed to be accepted by the Archive::Har module.
    This function should be considered experimental. Feedback welcome.

        use Firefox::Marionette();
        use Archive::Har();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new(visible => 1, debug => 1, har => 1);
    
        $firefox->go("http://metacpan.org/");
    
        $firefox->find('//input[@id="metacpan_search-input"]')->type('Test::More');
        $firefox->find_name('lucky')->click();
    
        my $har = Archive::Har->new();
        $har->hashref($firefox->har());
    
        foreach my $entry ($har->entries()) {
            say $entry->request()->url() . " spent " . $entry->timings()->connect() . " ms establishing a TCP connection";
        }

 has

    accepts an xpath expression  as
    the first parameter and returns the first element that matches this
    expression.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    
        if (my $element = $firefox->has('//input[@id="metacpan_search-input"]')) {
            $element->type('Test::More');
        }

    If no elements are found, this method will return undef. For the same
    functionality that throws a not found exception, see the find method.

 has_id

    accepts an id
    
    as the first parameter and returns the first element with a matching
    'id' property.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    
        if (my $element = $firefox->has_id('metacpan_search-input')) {
            $element->type('Test::More');
        }

    If no elements are found, this method will return undef. For the same
    functionality that throws a not found exception, see the find_id
    method.

 has_name

    This method returns the first element with a matching 'name' property.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        if (my $element = $firefox->has_name('q')) {
            $element->type('Test::More');
        }

    If no elements are found, this method will return undef. For the same
    functionality that throws a not found exception, see the find_name
    method.

 has_class

    accepts a class name
    
    as the first parameter and returns the first element with a matching
    'class' property.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        if (my $element = $firefox->has_class('form-control home-metacpan_search-input')) {
            $element->type('Test::More');
        }

    If no elements are found, this method will return undef. For the same
    functionality that throws a not found exception, see the find_class
    method.

 has_selector

    accepts a CSS Selector
     as the
    first parameter and returns the first element that matches that
    selector.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        if (my $element = $firefox->has_selector('input.home-metacpan_search-input')) {
            $element->type('Test::More');
        }

    If no elements are found, this method will return undef. For the same
    functionality that throws a not found exception, see the find_selector
    method.

 has_tag

    accepts a tag name
     as
    the first parameter and returns the first element with this tag name.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        if (my $element = $firefox->has_tag('input')) {
            # do something
        }

    If no elements are found, this method will return undef. For the same
    functionality that throws a not found exception, see the find_tag
    method.

 has_link

    accepts a text string as the first parameter and returns the first link
    element that has a matching link text.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        if (my $element = $firefox->has_link('API')) {
            $element->click();
        }

    If no elements are found, this method will return undef. For the same
    functionality that throws a not found exception, see the find_link
    method.

 has_partial

    accepts a text string as the first parameter and returns the first link
    element that has a partially matching link text.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        if (my $element = $firefox->find_partial('AP')) {
            $element->click();
        }

    If no elements are found, this method will return undef. For the same
    functionality that throws a not found exception, see the find_partial
    method.

 html

    returns the page source of the content document. This page source can
    be wrapped in html that firefox provides. See the json method for an
    alternative when dealing with response content types such as
    application/json and strip for an alterative when dealing with other
    non-html content types such as text/plain.

        use Firefox::Marionette();
        use v5.10;
    
        say Firefox::Marionette->new()->go('https://metacpan.org/')->html();

 images

    returns a list of all of the following elements;

      * img 

      * image inputs
      

    as Firefox::Marionette::Image objects.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        if (my $link = $firefox->images()) {
            say "Found a image with width " . $image->width() . "px and height " . $image->height() . "px from " . $image->URL();
        }

    If no elements are found, this method will return undef.

 install

    accepts the following as the first parameter;

      * path to an xpi file
      .

      * path to a directory containing firefox extension source code
      .
      This directory will be packaged up as an unsigned xpi file.

      * path to a top level file (such as manifest.json
      )
      in a directory containing firefox extension source code
      .
      This directory will be packaged up as an unsigned xpi file.

    and an optional true/false second parameter to indicate if the xpi file
    should be a temporary extension
    
    (just for the existance of this browser instance). Unsigned xpi files
    may only be loaded temporarily
     (except for
    nightly firefox installations
    ). It
    returns the GUID for the addon which may be used as a parameter to the
    uninstall method.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new();
    
        my $extension_id = $firefox->install('/full/path/to/gnu_terry_pratchett-0.4-an+fx.xpi');
    
        # OR downloading and installing source code
    
        system { 'git' } 'git', 'clone', 'https://github.com/kkapsner/CanvasBlocker.git';
    
        if ($firefox->nightly()) {
    
            $extension_id = $firefox->install('./CanvasBlocker'); # permanent install for unsigned packages in nightly firefox
    
        } else {
    
            $extension_id = $firefox->install('./CanvasBlocker', 1); # temp install for normal firefox
    
        }

 interactive

    returns true if document.readyState === "interactive" or if loaded is
    true

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        $firefox->find_id('search_input')->type('Type::More');
        $firefox->find('//button[@name="lucky"]')->click();
        while(!$firefox->interactive()) {
            # redirecting to Test::More page
        }

 is_displayed

    accepts an element as the first parameter. This method returns true or
    false depending on if the element is displayed
    .

 is_enabled

    accepts an element as the first parameter. This method returns true or
    false depending on if the element is enabled
    .

 is_selected

    accepts an element as the first parameter. This method returns true or
    false depending on if the element is selected
    . Note that
    this method only makes sense for checkbox
    
    or radio
    
    inputs or option
    
    elements in a select
    
    dropdown.

 json

    returns a JSON object that has been parsed from the page source of the
    content document. This is a convenience method that wraps the strip
    method.

        use Firefox::Marionette();
        use v5.10;
    
        say Firefox::Marionette->new()->go('https://fastapi.metacpan.org/v1/download_url/Firefox::Marionette")->json()->{version};

 key_down

    accepts a parameter describing a key and returns an action for use in
    the perform method that corresponding with that key being depressed.

        use Firefox::Marionette();
        use Firefox::Marionette::Keys qw(:all);
    
        my $firefox = Firefox::Marionette->new();
    
        $firefox->chrome()->perform(
                                     $firefox->key_down(CONTROL()),
                                     $firefox->key_down('l'),
                                   )->release()->content();

 key_up

    accepts a parameter describing a key and returns an action for use in
    the perform method that corresponding with that key being released.

        use Firefox::Marionette();
        use Firefox::Marionette::Keys qw(:all);
    
        my $firefox = Firefox::Marionette->new();
    
        $firefox->chrome()->perform(
                                     $firefox->key_down(CONTROL()),
                                     $firefox->key_down('l'),
                                     $firefox->pause(20),
                                     $firefox->key_up('l'),
                                     $firefox->key_up(CONTROL())
                                   )->content();

 loaded

    returns true if document.readyState === "complete"

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        $firefox->find_id('search_input')->type('Type::More');
        $firefox->find('//button[@name="lucky"]')->click();
        while(!$firefox->loaded()) {
            # redirecting to Test::More page
        }

 logins_from_csv

    accepts a filehandle as a parameter and then reads the filehandle for
    exported logins as CSV. This is known to work with the following
    formats;

      * Bitwarden CSV
      

      * LastPass CSV
      

      * KeePass CSV 

    returns a list of Firefox::Marionette::Login objects.

        use Firefox::Marionette();
        use FileHandle();
    
        my $handle = FileHandle->new('/path/to/last_pass.csv');
        my $firefox = Firefox::Marionette->new();
        foreach my $login (Firefox::Marionette->logins_from_csv($handle)) {
            $firefox->add_login($login);
        }

 logins_from_zip

    accepts a filehandle as a parameter and then reads the filehandle for
    exported logins as a zip file. This is known to work with the following
    formats;

      * 1Password Unencrypted Export format
      

    returns a list of Firefox::Marionette::Login objects.

        use Firefox::Marionette();
        use FileHandle();
    
        my $handle = FileHandle->new('/path/to/1Passwordv8.1pux');
        my $firefox = Firefox::Marionette->new();
        foreach my $login (Firefox::Marionette->logins_from_zip($handle)) {
            $firefox->add_login($login);
        }

 links

    returns a list of all of the following elements;

      * anchor
      

      * area
      

      * frame
      

      * iframe
      

      * meta
      

    as Firefox::Marionette::Link objects.

    This method is subject to the implicit timeout, which, by default is 0
    seconds.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        if (my $link = $firefox->links()) {
            if ($link->tag() eq 'a') {
                warn "Found a hyperlink to " . $link->URL();
            }
        }

    If no elements are found, this method will return undef.

 macos_binary_paths

    returns a list of filesystem paths that this module will check for
    binaries that it can automate when running on MacOS
    . Only of interest when
    sub-classing.

 marionette_protocol

    returns the version for the Marionette protocol. Current most recent
    version is '3'.

 maximise

    maximises the firefox window. This method returns itself to aid in
    chaining methods.

 mime_types

    returns a list of MIME types that will be downloaded by firefox and
    made available from the downloads method

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new(mime_types => [ 'application/pkcs10' ])
    
        foreach my $mime_type ($firefox->mime_types()) {
            say $mime_type;
        }

 minimise

    minimises the firefox window. This method returns itself to aid in
    chaining methods.

 mouse_down

    accepts a parameter describing which mouse button the method should
    apply to (left, middle or right) and returns an action for use in the
    perform method that corresponding with a mouse button being depressed.

 mouse_move

    accepts a element parameter, or a ( x => 0, y => 0 ) type hash manually
    describing exactly where to move the mouse to and returns an action for
    use in the perform method that corresponding with such a mouse
    movement, either to the specified co-ordinates or to the middle of the
    supplied element parameter. Other parameters that may be passed are
    listed below;

      * origin - the origin of the C( 0, y => 0)> co-ordinates. Should
      be either viewport, pointer or an element.

      * duration - Number of milliseconds over which to distribute the
      move. If not defined, the duration defaults to 0.

    This method returns itself to aid in chaining methods.

 mouse_up

    accepts a parameter describing which mouse button the method should
    apply to (left, middle or right) and returns an action for use in the
    perform method that corresponding with a mouse button being released.

 new

    accepts an optional hash as a parameter. Allowed keys are below;

      * addons - should any firefox extensions and themes be available in
      this session. This defaults to "0".

      * binary - use the specified path to the Firefox
       binary, rather than the default path.

      * capabilities - use the supplied capabilities object, for example to
      set whether the browser should accept insecure certs or whether the
      browser should use a proxy.

      * chatty - Firefox is extremely chatty on the network, including
      checking for the lastest malware/phishing sites, updates to
      firefox/etc. This option is therefore off ("0") by default, however,
      it can be switched on ("1") if required. Even with chatty switched
      off, connections to firefox.settings.services.mozilla.com will still
      be made .
      The only way to prevent this seems to be to set
      firefox.settings.services.mozilla.com to 127.0.0.1 via /etc/hosts
      . NOTE: that this option
      only works when profile_name/profile is not specified.

      * console - show the browser console
      
      when the browser is launched. This defaults to "0" (off).

      * debug - should firefox's debug to be available via STDERR. This
      defaults to "0". Any ssh connections will also be printed to STDERR.
      This defaults to "0" (off). This setting may be updated by the debug
      method.

      * developer - only allow a developer edition
       to be launched.
      This defaults to "0" (off).

      * devtools - begin the session with the devtools
       window opened in a
      separate window.

      * height - set the height
      
      of the initial firefox window

      * har - begin the session with the devtools
       window opened in a
      separate window. The HAR Export Trigger
      
      addon will be loaded into the new session automatically, which means
      that -safe-mode will not be activated for this session AND this
      functionality will only be available for Firefox 61+.

      * host - use ssh  to create and
      automate firefox on the specified host. See REMOTE AUTOMATION OF
      FIREFOX VIA SSH.

      * implicit - a shortcut to allow directly providing the implicit
      timeout, instead of needing to use timeouts from the capabilities
      parameter. Overrides all longer ways.

      * kiosk - start the browser in kiosk
      
      mode.

      * mime_types - any MIME types that Firefox will encounter during this
      session. MIME types that are not specified will result in a hung
      browser (the File Download popup will appear).

      * nightly - only allow a nightly release
       to
      be launched. This defaults to "0" (off).

      * port - if the "host" parameter is also set, use ssh
       to create and automate firefox via
      the specified port. See REMOTE AUTOMATION OF FIREFOX VIA SSH.

      * page_load - a shortcut to allow directly providing the page_load
      timeout, instead of needing to use timeouts from the capabilities
      parameter. Overrides all longer ways.

      * profile - create a new profile based on the supplied profile. NOTE:
      firefox ignores any changes made to the profile on the disk while it
      is running.

      * profile_name - pick a specific existing profile to automate, rather
      than creating a new profile. Firefox  refuses to
      allow more than one instance of a profile to run at the same time.
      Profile names can be obtained by using the
      Firefox::Marionette::Profile::names() method. NOTE: firefox ignores
      any changes made to the profile on the disk while it is running.

      * reconnect - an experimental parameter to allow a reconnection to
      firefox that a connection has been discontinued. See the survive
      parameter.

      * script - a shortcut to allow directly providing the script timeout,
      instead of needing to use timeouts from the capabilities parameter.
      Overrides all longer ways.

      * seer - this option is switched off "0" by default. When it is
      switched on "1", it will activate the various speculative and
      pre-fetch options for firefox. NOTE: that this option only works when
      profile_name/profile is not specified.

      * sleep_time_in_ms - the amount of time (in milliseconds) that this
      module should sleep when unsuccessfully calling the subroutine
      provided to the await or bye methods. This defaults to "1"
      millisecond.

      * survive - if this is set to a true value, firefox will not
      automatically exit when the object goes out of scope. See the
      reconnect parameter for an experimental technique for reconnecting.

      * trust - give a path to a root certificate
       encoded as a PEM
      encoded X.509 certificate
       that will
      be trusted for this session.

      * timeouts - a shortcut to allow directly providing a timeout object,
      instead of needing to use timeouts from the capabilities parameter.
      Overrides the timeouts provided (if any) in the capabilities
      parameter.

      * user - if the "host" parameter is also set, use ssh
       to create and automate firefox with
      the specified user. See REMOTE AUTOMATION OF FIREFOX VIA SSH. The
      user will default to the current user name.

      * visible - should firefox be visible on the desktop. This defaults
      to "0".

      * waterfox - only allow a binary that looks like a waterfox version
       to be launched.

      * width - set the width
      
      of the initial firefox window

    This method returns a new Firefox::Marionette object, connected to an
    instance of firefox . In a non MacOS/Win32/Cygwin
    environment, if necessary (no DISPLAY variable can be found and the
    visible parameter to the new method has been set to true) and possible
    (Xvfb can be executed successfully), this method will also
    automatically start an Xvfb 
    instance.

        use Firefox::Marionette();
    
        my $remote_darwin_firefox = Firefox::Marionette->new(
                         debug => 1,
                         host => '10.1.2.3',
                         trust => '/path/to/root_ca.pem',
                         binary => '/Applications/Firefox.app/Contents/MacOS/firefox'
                                                            ); # start a temporary profile for a remote firefox and load a new CA into the temp profile
        ...
    
        foreach my $profile_name (Firefox::Marionette::Profile->names()) {
            my $firefox_with_existing_profile = Firefox::Marionette->new( profile_name => $profile_name, visible => 1 );
            ...
        }

 new_window

    accepts an optional hash as the parameter. Allowed keys are below;

      * focus - a boolean field representing if the new window be opened in
      the foreground (focused) or background (not focused). Defaults to
      false.

      * private - a boolean field representing if the new window should be
      a private window. Defaults to false.

      * type - the type of the new window. Can be one of 'tab' or 'window'.
      Defaults to 'tab'.

    Returns the window handle for the new window.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new();
    
        my $window_handle = $firefox->new_window(type => 'tab');
    
        $firefox->switch_to_window($window_handle);

 new_session

    creates a new WebDriver session. It is expected that the caller
    performs the necessary checks on the requested capabilities to be
    WebDriver conforming. The WebDriver service offered by Marionette does
    not match or negotiate capabilities beyond type and bounds checks.

 nightly

    returns true if the current version of firefox is a nightly release
     (does
    the minor version number end with an 'a1'?)

 paper_sizes

    returns a list of all the recognised names for paper sizes, such as A4
    or LEGAL.

 pause

    accepts a parameter in milliseconds and returns a corresponding action
    for the perform method that will cause a pause in the chain of actions
    given to the perform method.

 pdf

    accepts a optional hash as the first parameter with the following
    allowed keys;

      * landscape - Paper orientation. Boolean value. Defaults to false

      * margin - A hash describing the margins. The hash may have the
      following optional keys, 'top', 'left', 'right' and 'bottom'. All
      these keys are in cm and default to 1 (~0.4 inches)

      * page - A hash describing the page. The hash may have the following
      keys; 'height' and 'width'. Both keys are in cm and default to US
      letter size. See the 'size' key.

      * page_ranges - A list of the pages to print. Available for Firefox
      96
      
      and after.

      * print_background - Print background graphics. Boolean value.
      Defaults to false.

      * raw - rather than a file handle containing the PDF, the binary PDF
      will be returned.

      * scale - Scale of the webpage rendering. Defaults to 1.

      * size - The desired size (width and height) of the pdf, specified by
      name. See the page key for an alternative and the paper_sizes method
      for a list of accepted page size names.

      * shrink_to_fit - Whether or not to override page size as defined by
      CSS. Boolean value. Defaults to true.

    returns a File::Temp object containing a PDF encoded version of the
    current page for printing.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        my $handle = $firefox->pdf();
        foreach my $paper_size ($firefox->paper_sizes()) {
                $handle = $firefox->pdf(size => $paper_size, landscape => 1, margin => { top => 0.5, left => 1.5 });
                ...
                print $firefox->pdf(page => { width => 21, height => 27 }, raw => 1);
                ...
        }

 perform

    accepts a list of actions (see mouse_up, mouse_down, mouse_move, pause,
    key_down and key_up) and performs these actions in sequence. This
    allows fine control over interactions, including sending right clicks
    to the browser and sending Control, Alt and other special keys. The
    release method will complete outstanding actions (such as mouse_up or
    key_up actions).

        use Firefox::Marionette();
        use Firefox::Marionette::Keys qw(:all);
        use Firefox::Marionette::Buttons qw(:all);
    
        my $firefox = Firefox::Marionette->new();
    
        $firefox->chrome()->perform(
                                     $firefox->key_down(CONTROL()),
                                     $firefox->key_down('l'),
                                     $firefox->key_up('l'),
                                     $firefox->key_up(CONTROL())
                                   )->content();
    
        $firefox->go('https://metacpan.org');
        my $help_button = $firefox->find_class('btn search-btn help-btn');
        $firefox->perform(
                                      $firefox->mouse_move($help_button),
                                      $firefox->mouse_down(RIGHT_BUTTON()),
                                      $firefox->pause(4),
                                      $firefox->mouse_up(RIGHT_BUTTON()),
                    );

    See the release method for an alternative for manually specifying all
    the mouse_up and key_up methods

 profile_directory

    returns the profile directory used by the current instance of firefox.
    This is mainly intended for debugging firefox. Firefox is not designed
    to cope with these files being altered while firefox is running.

 property

    accepts an element as the first parameter and a scalar attribute name
    as the second parameter. It returns the current value of the property
    with the supplied name. This method will return the current content,
    the attribute method will return the initial content.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        my $element = $firefox->find_id('search_input');
        $element->property('value') eq '' or die "Initial property is the empty string";
        $element->type('Test::More');
        $element->property('value') eq 'Test::More' or die "This property should have changed!";
    
        # OR getting the innerHTML property
    
        my $title = $firefox->find_tag('title')->property('innerHTML'); # same as $firefox->title();

 pwd_mgr_lock

    Accepts a new primary password
    
    and locks the Password Manager
    
    with it.

        use Firefox::Marionette();
        use IO::Prompt();
    
        my $firefox = Firefox::Marionette->new();
        my $password = IO::Prompt::prompt(-echo => q[*], "Please enter the password for the Firefox Password Manager:");
        $firefox->pwd_mgr_lock($password);
        $firefox->pwd_mgr_logout();
        # now no-one can access the Password Manager Database without the value in $password

    This method returns itself to aid in chaining methods.

 pwd_mgr_login

    Accepts the primary password
    
    and allows the user to access the Password Manager
    .

        use Firefox::Marionette();
        use IO::Prompt();
    
        my $firefox = Firefox::Marionette->new( profile_name => 'default' );
        my $password = IO::Prompt::prompt(-echo => q[*], "Please enter the password for the Firefox Password Manager:");
        $firefox->pwd_mgr_login($password);
        ...
        # access the Password Database.
        ...
        $firefox->pwd_mgr_logout();
        ...
        # no longer able to access the Password Database.

    This method returns itself to aid in chaining methods.

 pwd_mgr_logout

    Logs the user out of being able to access the Password Manager
    .

        use Firefox::Marionette();
        use IO::Prompt();
    
        my $firefox = Firefox::Marionette->new( profile_name => 'default' );
        my $password = IO::Prompt::prompt(-echo => q[*], "Please enter the password for the Firefox Password Manager:");
        $firefox->pwd_mgr_login($password);
        ...
        # access the Password Database.
        ...
        $firefox->pwd_mgr_logout();
        ...
        # no longer able to access the Password Database.

    This method returns itself to aid in chaining methods.

 pwd_mgr_needs_login

    returns true or false if the Password Manager
    
    has been locked and needs a primary password
    
    to access it.

        use Firefox::Marionette();
        use IO::Prompt();
    
        my $firefox = Firefox::Marionette->new( profile_name => 'default' );
        if ($firefox->pwd_mgr_needs_login()) {
          my $password = IO::Prompt::prompt(-echo => q[*], "Please enter the password for the Firefox Password Manager:");
          $firefox->pwd_mgr_login($password);
        }

 quit

    Marionette will stop accepting new connections before ending the
    current session, and finally attempting to quit the application. This
    method returns the $? (CHILD_ERROR) value for the Firefox process

 rect

    accepts a element as the first parameter and returns the current
    position and size of the element

 refresh

    refreshes the current page. The browser will wait for the page to
    completely refresh or the session's page_load duration to elapse before
    returning, which, by default is 5 minutes. This method returns itself
    to aid in chaining methods.

 release

    completes any outstanding actions issued by the perform method.

        use Firefox::Marionette();
        use Firefox::Marionette::Keys qw(:all);
        use Firefox::Marionette::Buttons qw(:all);
    
        my $firefox = Firefox::Marionette->new();
    
        $firefox->chrome()->perform(
                                     $firefox->key_down(CONTROL()),
                                     $firefox->key_down('l'),
                                   )->release()->content();
    
        $firefox->go('https://metacpan.org');
        my $help_button = $firefox->find_class('btn search-btn help-btn');
        $firefox->perform(
                                      $firefox->mouse_move($help_button),
                                      $firefox->mouse_down(RIGHT_BUTTON()),
                                      $firefox->pause(4),
                    )->release();

 restart

    restarts the browser. After the restart, capabilities should be
    restored. The same profile settings should be applied, but the current
    state of the browser (such as the uri will be reset (like after a
    normal browser restart). This method is primarily intended for use by
    the update method. Not sure if this is useful by itself.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new();
    
        $firefox->restart(); # but why?

    This method returns itself to aid in chaining methods.

 root_directory

    this is the root directory for the current instance of firefox. The
    directory may exist on a remote server. For debugging purposes only.

 screen_orientation

    returns the current browser orientation. This will be one of the valid
    primary orientation values 'portrait-primary', 'landscape-primary',
    'portrait-secondary', or 'landscape-secondary'. This method is only
    currently available on Android (Fennec).

 script

    accepts a scalar containing a javascript function body that is executed
    in the browser, and an optional hash as a second parameter. Allowed
    keys are below;

      * args - The reference to a list is the arguments passed to the
      function body.

      * filename - Filename of the client's program where this script is
      evaluated.

      * line - Line in the client's program where this script is evaluated.

      * new - Forces the script to be evaluated in a fresh sandbox. Note
      that if it is undefined, the script will normally be evaluted in a
      fresh sandbox.

      * sandbox - Name of the sandbox to evaluate the script in. The
      sandbox is cached for later re-use on the same window
       object if
      new is false. If he parameter is undefined, the script is evaluated
      in a mutable sandbox. If the parameter is "system", it will be
      evaluted in a sandbox with elevated system privileges, equivalent to
      chrome space.

      * timeout - A timeout to override the default script timeout, which,
      by default is 30 seconds.

    Returns the result of the javascript function. When a parameter is an
    element (such as being returned from a find type operation), the script
    method will automatically translate that into a javascript object.
    Likewise, when the result being returned in a script method is an
    element  it will be
    automatically translated into a perl object.

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    
        if (my $element = $firefox->script('return document.getElementsByName("lucky")[0];')) {
            say "Lucky find is a " . $element->tag_name() . " element";
        }
    
        my $search_input = $firefox->find_id('metacpan_search-input');
    
        $firefox->script('arguments[0].style.backgroundColor = "red"', args => [ $search_input ]); # turn the search input box red

    The executing javascript is subject to the script timeout, which, by
    default is 30 seconds.

 selfie

    returns a File::Temp object containing a lossless PNG image screenshot.
    If an element is passed as a parameter, the screenshot will be
    restricted to the element.

    If an element is not passed as a parameter and the current context is
    'chrome', a screenshot of the current viewport will be returned.

    If an element is not passed as a parameter and the current context is
    'content', a screenshot of the current frame will be returned.

    The parameters after the element parameter are taken to be a optional
    hash with the following allowed keys;

      * hash - return a SHA256 hex encoded digest of the PNG image rather
      than the image itself

      * full - take a screenshot of the whole document unless the first
      element parameter has been supplied.

      * raw - rather than a file handle containing the screenshot, the
      binary PNG image will be returned.

      * scroll - scroll to the element supplied

      * highlights - a reference to a list containing elements to draw a
      highlight around. Not available in Firefox 70
      
      onwards.

 send_alert_text

    sends keys to the input field of a currently displayed modal message
    box

 shadow_root

    accepts an element as a parameter and returns it's ShadowRoot
     as a
    shadow root object or throws an exception.

        use Firefox::Marionette();
        use Cwd();
    
        my $firefox = Firefox::Marionette->new()->go('file://' . Cwd::cwd() . '/t/data/elements.html');
    
        $firefox->find_class('add')->click();
        my $custom_square = $firefox->find_tag('custom-square');
        my $shadow_root = $firefox->shadow_root($custom_square);
    
        foreach my $element (@{$firefox->script('return arguments[0].children', args => [ $shadow_root ])}) {
            warn $element->tag_name();
        }

 shadowy

    accepts an element as a parameter and returns true if the element has a
    ShadowRoot
     or false
    otherwise.

        use Firefox::Marionette();
        use Cwd();
    
        my $firefox = Firefox::Marionette->new()->go('file://' . Cwd::cwd() . '/t/data/elements.html');
        $firefox->find_class('add')->click();
        my $custom_square = $firefox->find_tag('custom-square');
        if ($firefox->shadowy($custom_square)) {
            my $shadow_root = $firefox->find_tag('custom-square')->shadow_root();
            warn $firefox->script('return arguments[0].innerHTML', args => [ $shadow_root ]);
            ...
        }

    This function will probably be used to see if the shadow_root method
    can be called on this element without raising an exception.

 sleep_time_in_ms

    accepts a new time to sleep in await or bye methods and returns the
    previous time. The default time is "1" millisecond.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new(sleep_time_in_ms => 5); # setting default time to 5 milliseconds
    
        my $old_time_in_ms = $firefox->sleep_time_in_ms(8); # setting default time to 8 milliseconds, returning 5 (milliseconds)

 ssh_local_directory

    returns the path to the local directory for the ssh connection (if
    any). For debugging purposes only.

 strip

    returns the page source of the content document after an attempt has
    been made to remove typical firefox html wrappers of non html content
    types such as text/plain and application/json. See the json method for
    an alternative when dealing with response content types such as
    application/json and html for an alterative when dealing with html
    content types. This is a convenience method that wraps the html method.

        use Firefox::Marionette();
        use JSON();
        use v5.10;
    
        say JSON::decode_json(Firefox::Marionette->new()->go("https://fastapi.metacpan.org/v1/download_url/Firefox::Marionette")->strip())->{version};

    Note that this method will assume the bytes it receives from the html
    method are UTF-8 encoded and will translate accordingly, throwing an
    exception in the process if the bytes are not UTF-8 encoded.

 switch_to_frame

    accepts a frame as a parameter and switches to it within the current
    window.

 switch_to_parent_frame

    set the current browsing context for future commands to the parent of
    the current browsing context

 switch_to_window

    accepts a window handle (either the result of window_handles or a
    window name as a parameter and switches focus to this window.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new();
        $firefox->version
        my $original_window_uuid = $firefox->window_handle();
        $firefox->new_window( type => 'tab' );
        $firefox->new_window( type => 'window' );
        $firefox->switch_to_window($original_window_uuid);
        $firefox->go('https://metacpan.org');

 tag_name

    accepts a Firefox::Marionette::Element object as the first parameter
    and returns the relevant tag name. For example 'a
    ' or
    'input
    '.

 text

    accepts a element as the first parameter and returns the text that is
    contained by that element (if any)

 timeouts

    returns the current timeouts for page loading, searching, and scripts.

 title

    returns the current title
     of
    the window.

 type

    accepts an element as the first parameter and a string as the second
    parameter. It sends the string to the specified element in the current
    page, such as filling out a text box. This method returns itself to aid
    in chaining methods.

 update

    queries the Update Services and applies any available updates. Restarts
    the browser if necessary to complete the update. This function is
    experimental and currently has not been successfully tested on Win32 or
    MacOS.

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new();
    
        my $update = $firefox->update();
    
        while($update->successful()) {
            $update = $firefox->update();
        }
    
        say "Updated to " . $update->display_version() . " - Build ID " . $update->build_id();
    
        $firefox->quit();

    returns a status object that contains useful information about any
    updates that occurred.

 uninstall

    accepts the GUID for the addon to uninstall. The GUID is returned when
    from the install method. This method returns itself to aid in chaining
    methods.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new();
    
        my $extension_id = $firefox->install('/full/path/to/gnu_terry_pratchett-0.4-an+fx.xpi');
    
        # do something
    
        $firefox->uninstall($extension_id); # not recommended to uninstall this extension IRL.

 uri

    returns the current URI of current top level browsing context for
    Desktop. It is equivalent to the javascript document.location.href

 win32_organisation

    accepts a parameter of a Win32 product name and returns the matching
    organisation. Only of interest when sub-classing.

 win32_product_names

    returns a hash of known Windows product names (such as 'Mozilla
    Firefox') with priority orders. The lower the priority will determine
    the order that this module will check for the existance of this
    product. Only of interest when sub-classing.

 window_handle

    returns the current window's handle. On desktop this typically
    corresponds to the currently selected tab. returns an opaque
    server-assigned identifier to this window that uniquely identifies it
    within this Marionette instance. This can be used to switch to this
    window at a later point.

        use Firefox::Marionette();
        use 5.010;
    
        my $firefox = Firefox::Marionette->new();
        my $original_window_uuid = $firefox->window_handle();

 window_handles

    returns a list of top-level browsing contexts. On desktop this
    typically corresponds to the set of open tabs for browser windows, or
    the window itself for non-browser chrome windows. Each window handle is
    assigned by the server and is guaranteed unique, however the return
    array does not have a specified ordering.

        use Firefox::Marionette();
        use 5.010;
    
        my $firefox = Firefox::Marionette->new();
        my $original_window_uuid = $firefox->window_handle();
        $firefox->new_window( type => 'tab' );
        $firefox->new_window( type => 'window' );
        say "There are " . $firefox->window_handles() . " tabs open in total";
        say "Across " . $firefox->chrome()->window_handles()->content() . " chrome windows";

 window_rect

    accepts an optional position and size as a parameter, sets the current
    browser window to that position and size and returns the previous
    position, size and state of the browser window. If no parameter is
    supplied, it returns the current position, size and state of the
    browser window.

 window_type

    returns the current window's type. This should be 'navigator:browser'.

 xvfb_pid

    returns the pid of the xvfb process if it exists.

 xvfb_display

    returns the value for the DISPLAY environment variable if one has been
    generated for the xvfb environment.

 xvfb_xauthority

    returns the value for the XAUTHORITY environment variable if one has
    been generated for the xvfb environment

AUTOMATING THE FIREFOX PASSWORD MANAGER

    This module allows you to login to a website without ever directly
    handling usernames and password details. The Password Manager may be
    preloaded with appropriate passwords and locked, like so;

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new( profile_name => 'locked' ); # using a pre-built profile called 'locked'
        if ($firefox->pwd_mgr_needs_login()) {
            my $new_password = IO::Prompt::prompt(-echo => q[*], 'Enter the password for the locked profile:');
            $firefox->pwd_mgr_login($password);
        } else {
            my $new_password = IO::Prompt::prompt(-echo => q[*], 'Enter the new password for the locked profile:');
            $firefox->pwd_mgr_lock($password);
        }
        ...
        $firefox->pwd_mgr_logout();

    Usernames and passwords (for both HTTP Authentication popups and HTML
    Form based logins) may be added, viewed and deleted.

        use WebService::HIBP();
    
        my $hibp = WebService::HIBP->new();
    
        $firefox->add_login(host => 'https://github.com', user => 'me@example.org', password => 'qwerty', user_field => 'login', password_field => 'password');
        $firefox->add_login(host => 'https://pause.perl.org', user => 'AUSER', password => 'qwerty', realm => 'PAUSE');
        ...
        foreach my $login ($firefox->logins()) {
            if ($hibp->password($login->password())) { # does NOT send the password to the HIBP webservice
                warn "HIBP reports that your password for the " . $login->user() " account at " . $login->host() . " has been found in a data breach";
                $firefox->delete_login($login); # how could this possibly help?
            }
        }

    And used to fill in login prompts without explicitly knowing the
    account details.

        $firefox->go('https://pause.perl.org/pause/authenquery')->accept_alert(); # this goes to the page and submits the http auth popup
    
        $firefox->go('https://github.com/login')->fill_login(); # fill the login and password fields without needing to see them

REMOTE AUTOMATION OF FIREFOX VIA SSH

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new( host => 'remote.example.org', debug => 1 );
        $firefox->go('https://metacpan.org/');
    
        # OR specify a different user to login as ...
        
        my $firefox = Firefox::Marionette->new( host => 'remote.example.org', user => 'R2D2', debug => 1 );
        $firefox->go('https://metacpan.org/');
    
        # OR specify a different port to connect to
        
        my $firefox = Firefox::Marionette->new( host => 'remote.example.org', port => 2222, debug => 1 );
        $firefox->go('https://metacpan.org/');

    This module has support for creating and automating an instance of
    Firefox on a remote node. It has been tested against a number of
    operating systems, including recent version of Windows 10 or Windows
    Server 2019
    ,
    OS X, and Linux and BSD distributions. It expects to be able to login
    to the remote node via public key authentication. It can be further
    secured via the command
     option in the OpenSSH
     authorized_keys
     file such
    as;

        no-agent-forwarding,no-pty,no-X11-forwarding,permitopen="127.0.0.1:*",command="/usr/local/bin/ssh-auth-cmd-marionette" ssh-rsa AAAA ... == user@server

    As an example, the ssh-auth-cmd-marionette command is provided as part
    of this distribution.

    When using ssh, Firefox::Marionette will attempt to pass the TMPDIR
     environment variable across the
    ssh connection to make cleanups easier. In order to allow this, the
    AcceptEnv  setting in
    the remote sshd configuration should be set to allow TMPDIR, which will
    look like;

        AcceptEnv TMPDIR

    This module uses ControlMaster
     functionality when
    using ssh, for a useful speedup of executing remote commands.
    Unfortunately, when using ssh to move from a cygwin
    , Windows 10 or
    Windows Server 2019
    
    node to a remote environment, we cannot use ControlMaster, because at
    this time, Windows does not support ControlMaster
     and
    therefore this type of automation is still possible, but slower than
    other client platforms.

DIAGNOSTICS

    Failed to correctly setup the Firefox process

      The module was unable to retrieve a session id and capabilities from
      Firefox when it requests a new_session as part of the initial setup
      of the connection to Firefox.

    Failed to correctly determined the Firefox process id through the
    initial connection capabilities

      The module was found that firefox is reporting through it's
      Capabilities object a different process id than this module was
      using. This is probably a bug in this module's logic. Please report
      as described in the BUGS AND LIMITATIONS section below.

    '%s --version' did not produce output that could be parsed. Assuming
    modern Marionette is available:%s

      The Firefox binary did not produce a version number that could be
      recognised as a Firefox version number.

    Failed to create process from '%s':%s

      The module was to start Firefox process in a Win32 environment.
      Something is seriously wrong with your environment.

    Failed to redirect %s to %s:%s

      The module was unable to redirect a file handle's output. Something
      is seriously wrong with your environment.

    Failed to exec %s:%s

      The module was unable to run the Firefox binary. Check the path is
      correct and the current user has execute permissions.

    Failed to fork:%s

      The module was unable to fork itself, prior to executing a command.
      Check the current ulimit for max number of user processes.

    Failed to open directory '%s':%s

      The module was unable to open a directory. Something is seriously
      wrong with your environment.

    Failed to close directory '%s':%s

      The module was unable to close a directory. Something is seriously
      wrong with your environment.

    Failed to open '%s' for writing:%s

      The module was unable to create a file in your temporary directory.
      Maybe your disk is full?

    Failed to open temporary file for writing:%s

      The module was unable to create a file in your temporary directory.
      Maybe your disk is full?

    Failed to close '%s':%s

      The module was unable to close a file in your temporary directory.
      Maybe your disk is full?

    Failed to close temporary file:%s

      The module was unable to close a file in your temporary directory.
      Maybe your disk is full?

    Failed to create temporary directory:%s

      The module was unable to create a directory in your temporary
      directory. Maybe your disk is full?

    Failed to clear the close-on-exec flag on a temporary file:%s

      The module was unable to call fcntl using F_SETFD for a file in your
      temporary directory. Something is seriously wrong with your
      environment.

    Failed to seek to start of temporary file:%s

      The module was unable to seek to the start of a file in your
      temporary directory. Something is seriously wrong with your
      environment.

    Failed to create a socket:%s

      The module was unable to even create a socket. Something is seriously
      wrong with your environment.

    Failed to connect to %s on port %d:%s

      The module was unable to connect to the Marionette port. This is
      probably a bug in this module's logic. Please report as described in
      the BUGS AND LIMITATIONS section below.

    Firefox killed by a %s signal (%d)

      Firefox crashed after being hit with a signal.

    Firefox exited with a %d

      Firefox has exited with an error code

    Failed to bind socket:%s

      The module was unable to bind a socket to any port. Something is
      seriously wrong with your environment.

    Failed to close random socket:%s

      The module was unable to close a socket without any reads or writes
      being performed on it. Something is seriously wrong with your
      environment.

    moz:headless has not been determined correctly

      The module was unable to correctly determine whether Firefox is
      running in "headless" or not. This is probably a bug in this module's
      logic. Please report as described in the BUGS AND LIMITATIONS section
      below.

    %s method requires a Firefox::Marionette::Element parameter

      This function was called incorrectly by your code. Please supply a
      Firefox::Marionette::Element parameter when calling this function.

    Failed to write to temporary file:%s

      The module was unable to write to a file in your temporary directory.
      Maybe your disk is full?

    Failed to close socket to firefox:%s

      The module was unable to even close a socket. Something is seriously
      wrong with your environment.

    Failed to send request to firefox:%s

      The module was unable to perform a syswrite on the socket connected
      to firefox. Maybe firefox crashed?

    Failed to read size of response from socket to firefox:%s

      The module was unable to read from the socket connected to firefox.
      Maybe firefox crashed?

    Failed to read response from socket to firefox:%s

      The module was unable to read from the socket connected to firefox.
      Maybe firefox crashed?

CONFIGURATION AND ENVIRONMENT

    Firefox::Marionette requires no configuration files or environment
    variables. It will however use the DISPLAY and XAUTHORITY environment
    variables to try to connect to an X Server. It will also use the
    HTTP_PROXY, HTTPS_PROXY, FTP_PROXY and ALL_PROXY environment variables
    as defaults if the session capabilities do not specify proxy
    information.

DEPENDENCIES

    Firefox::Marionette requires the following non-core Perl modules

      * JSON

      * URI

      * XML::Parser

INCOMPATIBILITIES

    None reported. Always interested in any products with marionette
    support that this module could be patched to work with.

BUGS AND LIMITATIONS

 DOWNLOADING USING GO METHOD

    When using the go method to go directly to a URL containing a
    downloadable file, Firefox can hang. You can work around this by
    setting the page_load_strategy to none like below;

        #! /usr/bin/perl
    
        use strict;
        use warnings;
        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new( capabilities => Firefox::Marionette::Capabilities->new( page_load_strategy => 'none' ) );
        $firefox->go("https://github.com/david-dick/firefox-marionette/archive/refs/heads/master.zip");
        while(!$firefox->downloads()) { sleep 1 }
        while($firefox->downloading()) { sleep 1 }
        foreach my $path ($firefox->downloads()) {
            warn "$path has been downloaded";
        }
        $firefox->quit();

 MISSING METHODS

    Currently the following Marionette methods have not been implemented;

      * WebDriver:SetScreenOrientation

    To report a bug, or view the current list of bugs, please visit
    https://github.com/david-dick/firefox-marionette/issues

SEE ALSO

      * MozRepl

      * Selenium::Firefox

      * Firefox::Application

      * Mozilla::Mechanize

      * Gtk2::MozEmbed

AUTHOR

    David Dick 

ACKNOWLEDGEMENTS

    Thanks to the entire Mozilla organisation for a great browser and to
    the team behind Marionette for providing an interface for automation.

    Thanks to Jan Odvarko  for
    creating the HAR Export Trigger
     extension for
    Firefox.

    Thanks to Mike Kaply  for his post
    
    describing importing certificates into Firefox.

    Thanks also to the authors of the documentation in the following
    sources;

      * Marionette Protocol
      

      * Marionette Documentation
      

      * Marionette driver.js
      

      * about:config 

      * nsIPrefService interface
      

LICENSE AND COPYRIGHT

    Copyright (c) 2021, David Dick . All rights reserved.

    This module is free software; you can redistribute it and/or modify it
    under the same terms as Perl itself. See "perlartistic" in
    perlartistic.

    The Firefox::Marionette::Extension::HarExportTrigger module includes
    the HAR Export Trigger
     extension
    which is licensed under the Mozilla Public License 2.0
    .

DISCLAIMER OF WARRANTY

    BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
    FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT
    WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER
    PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND,
    EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
    ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
    YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
    NECESSARY SERVICING, REPAIR, OR CORRECTION.

    IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
    WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
    REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE
    TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR
    CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
    SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
    RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
    FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
    SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
    DAMAGES.

Firefox-Marionette-1.22/ssh-auth-cmd-marionette0000755000175000017500000003407414175143706020175 0ustar  davedave#! /usr/bin/perl -wT

use strict;
use warnings;
use Sys::Syslog();
use Fcntl();
use Symbol();
use Getopt::Long();
use English qw( -no_match_vars );
use File::Spec();

delete $ENV{PATH};

local $ENV{PATH} = '/usr/bin:/bin:/usr/sbin:/sbin';

our $VERSION = '1.22';

my $binary  = 'firefox';
my $ident   = 'ssh-auth-cmd-marionette';
my %options = ( facility => 'LOG_LOCAL0' );

eval {
    Getopt::Long::GetOptions( \%options, 'help', 'version', 'facility:s',
        'allow-binary:s@', 'force-binary:s' );
    if ( $options{help} ) {
        require Pod::Simple::Text;
        my $parser = Pod::Simple::Text->new();
        $parser->parse_from_file($PROGRAM_NAME);
        exit 0;
    }
    elsif ( $options{version} ) {
        print "$VERSION\n"
          or die "Failed to print to STDOUT:$EXTENDED_OS_ERROR\n";
        exit 0;
    }
    %options = _validate_parameters(%options);
    my $tmp_directory = $ENV{TMPDIR} || '/tmp';
    $tmp_directory =~ s/\/$//smx;    # remove trailing / for darwin
    my $root_dir_regex;
    my $quoted_tmp_directory;
    if ( $tmp_directory =~
        s/^(.*firefox_marionette_remote\w+)(?:\/tmp)?$/$1/smx )
    {
        $quoted_tmp_directory = quotemeta $tmp_directory;
        $root_dir_regex       = qr/$quoted_tmp_directory/smx;
    }
    else {
        $quoted_tmp_directory = quotemeta $tmp_directory;
        $root_dir_regex =
          qr/${quoted_tmp_directory}\/firefox_marionette_remote\w+/smx;
    }
    my $allowed_binaries = q[(?:]
      . ( join q[|], map { quotemeta } @{ $options{'allow-binary'} } ) . q[)];
    my @allowed_binary_directories;
    foreach my $binary ( @{ $options{'allow-binary'} } ) {
        if ( $binary eq 'firefox' ) {
            push @allowed_binary_directories, '/usr/bin';
        }
        else {
            my ( $volume, $directories ) = File::Spec->splitpath($binary);
            push @allowed_binary_directories,
              File::Spec->catdir( $volume, $directories );
        }
    }
    my $allowed_binary_directories = q[(?:]
      . ( join q[|], map { quotemeta } @allowed_binary_directories ) . q[)];
    my $allowed_binary_directories_regex = qr/$allowed_binary_directories/smx;
    my $allowed_binary_regex             = qr/$allowed_binaries/smx;
    my $sub_directory_regex = qr/(?:profile|downloads|tmp|addons|certs)/smx;
    my $profile_file_regex =
      qr/profile\/(?:bookmarks[.]html|prefs[.]js|mimeTypes[.]rdf)/smx;
    my $file_regex            = qr/[+\w\-()]{1,255}(?:[.][+\w\-()]{1,255})*/smx;
    my $downloads_regex       = qr/downloads\/$file_regex/smx;
    my $ca_name_regex         = qr/Firefox::Marionette[ ]Root[ ]CA/smx;
    my $version_updates_regex = q[(?:]
      . (
        join q[|], qr/active\-update[.]xml/smx, qr/application[.]ini/smx,
        qr/updates\/\d+\/update[.]status/smx
      ) . q[)];
    my $certutil_arguments_regex = join q[],
      qr/[ ]\-A/smx,
      qr/[ ]\-d[ ](?:dbm|sql):$root_dir_regex\/profile/smx,
      qr/[ ]\-i[ ]$root_dir_regex\/certs\/root_ca_\d{1,10}[.]cer/smx,
      qr/[ ]\-n[ ]$ca_name_regex[ ]\d{1,10}[ ]\-t[ ]TC,,/smx;
    my $firefox_arguments_regex = join q[],
      qr/[ ]\-marionette/smx,
      qr/(?:[ ]\-width[ ]\d{1,8})?/smx,
      qr/(?:[ ]\-height[ ]\d{1,8})?/smx,
      qr/(?:[ ]-safe\-mode)?/smx,
      qr/(?:[ ]\-headless)?/smx,
      qr/[ ]\-profile[ ]$root_dir_regex\/profile/smx,
      qr/[ ]\-\-no\-remote/smx,
      qr/[ ]\-\-new\-instance/smx,
      qr/(?:[ ]\-\-devtools)?/smx;
    my $prefs_grep_patterns_regex = join q[],
      qr/\-e[ ]marionette\\.port[ ]/smx,
      qr/\-e[ ]security\\.sandbox\\.content\\.tempDirSuffix[ ]/smx,
      qr/\-e[ ]security\\.sandbox\\.plugin\\.tempDirSuffix[ ]/smx;
    my @darwin_regexes;

    if ( $OSNAME eq 'darwin' ) {
        my $plist_prefix_regex =
          _get_plist_prefix_regex( @{ $options{'allow-binary'} } );
        @darwin_regexes = (
            qr/ls[ ]-1[ ]"$allowed_binary_regex"/smx,
qr/plutil[ ]-convert[ ]json[ ]-o[ ]-[ ]"(?:$plist_prefix_regex)\/Info[.]plist"/smx,
        );
    }

    my $allowed_commands_regex = join q[|],
      qr/"$allowed_binary_regex"[ ]\-\-version/smx,
      @darwin_regexes,
      qr/uname[ ][|][|][ ]ver/smx,
      qr/echo[ ]"TMPDIR=\\"\$TMPDIR\\""/smx,
      qr/echo[ ]"TMP=\\"\$TMP\\""/smx,
      qr/mkdir[ ](?:\-m[ ]700[ ])?$root_dir_regex/smx,
      qr/mkdir[ ](?:\-m[ ]700[ ])?$root_dir_regex\/$sub_directory_regex/smx,
      qr/scp[ ]\-p[ ]\-[tf][ ]"$root_dir_regex\/$profile_file_regex"/smx,
qr/scp[ ]\-p[ ]\-t[ ]"$root_dir_regex\/(?:addons|profile)\/$file_regex"/smx,
      qr/scp[ ]\-p[ ]\-t[ ]"$root_dir_regex\/certs\/root_ca_\d{1,10}[.]cer"/smx,
qr/scp[ ]\-p[ ]\-[fT][ ](?:\-P[ ])?"$allowed_binary_directories\/$version_updates_regex"/smx,
      qr/scp[ ]\-p[ ]\-[tf][ ]"$root_dir_regex\/$downloads_regex"/smx,
      qr/kill[ ]\-0[ ]\d{1,8}/smx,
      qr/which[ ]$allowed_binary_regex/smx,
      qr/readlink[ ]\-f[ ]$allowed_binary_regex/smx,
qr/rm[ ]\-Rf[ ]$root_dir_regex(?:[ ]$quoted_tmp_directory\/Temp\-[\d\-a-f]{1,255})*/smx,
      qr/ls[ ]-1[ ]"$allowed_binary_directories(?:\/updates(?:\/\d+)?)?"/smx,
      qr/ls[ ]-1[ ]"$root_dir_regex\/downloads"/smx,
      qr/certutil$certutil_arguments_regex/smx,
      qr/"$allowed_binary_regex"$firefox_arguments_regex/smx,
qr/grep[ ]$prefs_grep_patterns_regex$root_dir_regex\/profile\/prefs[.]js/smx;

    if ( $ENV{SSH_ORIGINAL_COMMAND} =~ m/^($allowed_commands_regex)$/smx ) {
        my ($command_and_arguments) = ($1);
        if ( $options{'force-binary'} ) {
            $command_and_arguments =~
              s/^"$allowed_binary_regex"/"$options{'force-binary'}"/smx;
        }
        Sys::Syslog::openlog( $ident, 'cons', $options{facility} );
        Sys::Syslog::syslog( Sys::Syslog::LOG_INFO(),
            "Executing '$command_and_arguments'" );
        Sys::Syslog::closelog();
        exec $command_and_arguments
          or die "Failed to '$command_and_arguments':$EXTENDED_OS_ERROR\n";
    }
    else {
        die 'Unrecognisable command "'
          . $ENV{SSH_ORIGINAL_COMMAND}
          . "\" with a quoted TMPDIR of \"$quoted_tmp_directory\" and a root directory regex of \"$root_dir_regex\"\n";
    }
    1;
} or do {
    my $eval_error = $EVAL_ERROR;
    chomp $eval_error;
    Sys::Syslog::openlog( $ident, 'cons', $options{facility} );
    Sys::Syslog::syslog( Sys::Syslog::LOG_ERR(), $eval_error );
    Sys::Syslog::closelog();
    warn "$eval_error\n";
    exit 1;
};

sub _validate_parameters {
    my (%parameters) = @_;
    my $facility = $parameters{facility};
    eval { $parameters{facility} = Sys::Syslog->$facility(); } or do {
        my $original = $parameters{facility};
        $parameters{facility} = Sys::Syslog::LOG_LOCAL0();
        Sys::Syslog::openlog( $ident, 'cons', $parameters{facility} );
        Sys::Syslog::syslog( Sys::Syslog::LOG_WARNING(),
            "Failed to parse --facility argument of '$original':$EVAL_ERROR" );
        Sys::Syslog::closelog();
    };
    if ( !defined $ENV{SSH_ORIGINAL_COMMAND} ) {
        die
"$PROGRAM_NAME requires the SSH_ORIGINAL_COMMAND environment variable to be defined\n";
    }
    if ( !defined $parameters{'allow-binary'} ) {
        $parameters{'allow-binary'} = ['firefox'];
        if ( $OSNAME eq 'darwin' ) {
            push @{ $parameters{'allow-binary'} },
              '/Applications/Firefox.app/Contents/MacOS/firefox',
'/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox',
              '/Applications/Firefox Nightly.app/Contents/MacOS/firefox',
              '/Applications/Waterfox Current.app/Contents/MacOS/waterfox';
        }
    }
    if (   ( defined $parameters{'force-binary'} )
        && ( $parameters{'force-binary'} =~ /^(.*)$/smx ) )
    {
        my ($untainted) = ($1);
        $parameters{'force-binary'} =
          $untainted; # passed in on the command line from .authorized_keys file
        Sys::Syslog::openlog( $ident, 'cons', $parameters{facility} );
        Sys::Syslog::syslog( Sys::Syslog::LOG_DEBUG(),
            "Untainting --force-binary of '$parameters{'force-binary'}'" );
        Sys::Syslog::closelog();
    }
    return %parameters;
}

sub _get_plist_prefix_regex {
    my (@allow_binaries) = @_;
    my %allowed_plist_prefixes;
    foreach my $binary (@allow_binaries) {
        my $prefix = $binary;
        if ( $prefix =~ s/Contents\/MacOS\/(?:water|fire)fox$/Contents/smx ) {
            $allowed_plist_prefixes{$prefix} = 1;
        }
    }
    my $regex = join q[|],
      map { quotemeta } sort { $a cmp $b } keys %allowed_plist_prefixes;
    return $regex;
}

__END__
=head1 NAME

ssh-auth-cmd-marionette - ssh ~/.ssh/authorized_keys command for Firefox::Marionette

=head1 VERSION

Version 1.22

=head1 USAGE

~/.ssh/authorized_keys entry to allow the remote user to run a default firefox as user@server (all syslog entries go to LOG_LOCAL0)

   no-agent-forwarding,no-pty,no-X11-forwarding,permitopen="127.0.0.1:*",command="/usr/local/bin/ssh-auth-cmd-marionette" ssh-rsa AAAA ... == user@server

~/.ssh/authorized_keys entry to allow the remote user to run a default firefox as user@server (all syslog entries go to LOG_LOCAL1)

   no-agent-forwarding,no-pty,no-X11-forwarding,permitopen="127.0.0.1:*",command="/usr/local/bin/ssh-auth-cmd-marionette --facility=LOG_LOCAL1" ssh-rsa AAAA ... == user@server

~/.ssh/authorized_keys entry to force the remote user to run /path/to/firefox when logging in as user@server (all syslog entries go to LOG_LOCAL0)

   no-agent-forwarding,no-pty,no-X11-forwarding,permitopen="127.0.0.1:*",command="/usr/local/bin/ssh-auth-cmd-marionette --force-binary=/path/to/firefox" ssh-rsa AAAA ... == user@server

~/.ssh/authorized_keys entry to allow the remote user to run /path/to/firefox or /path/to/waterfox when logging in as user@server (all syslog entries go to LOG_LOCAL0)

   no-agent-forwarding,no-pty,no-X11-forwarding,permitopen="127.0.0.1:*",command="/usr/local/bin/ssh-auth-cmd-marionette --allow-binary=/path/to/firefox --allow-binary=/path/to/waterfox" ssh-rsa AAAA ... == user@server

=head1 DESCRIPTION

This program is intended to allow secure remote usage of the perl Firefox::Marionette library via ssh.  It allows a list
of pre-defined commands that can be permitted via ssh public key authentication.

Be default, it will log all commands that the remote perl library requests to run on this machine to the LOG_LOCAL0 syslog
facility.  If desired, syslog messages can be sent to a facility of your choosing, using the syslog(3) documentation for
a list of allowed facilities and the --facility argument for this program.

An example .ssh/authorized_keys file using this program would look like this 

   no-agent-forwarding,no-pty,no-X11-forwarding,permitopen="127.0.0.1:*",command="/usr/local/bin/ssh-auth-cmd-marionette" ssh-rsa AAAA ... == user@server

By default, the only firefox version that may be used will be present in the PATH environment variable.  However, the remote user may be permitted to specify the
path to a different firefox binary with (multiple) --allow-binary parameters, or simply forced to use the firefox that the local user is setup for with the 
--force-binary parameter.

=head1 REQUIRED ARGUMENTS

None

=head1 OPTIONS

Option names can be abbreviated to uniqueness and can be stated with singe or double dashes, and option values can be separated from the option name by a space or '=' (as with Getopt::Long). Option names are also case-
sensitive.

=over 4

=item * --help - This page.

=item * --version - print the version of ssh-auth-cmd-marionette.

=item * --facility - use L for L messages, instead of the default LOG_LOCAL0.

=item * --allow-binary - allow this path to be used in calls to L.  This option may be specified multiple times

=item * --force-binary - regardless of the settings that L requests, send all requests for the firefox binary to the path requested.

=back

=head1 CONFIGURATION

ssh-auth-cmd-marionette requires no configuration files or environment variables.

=head1 DEPENDENCIES

ssh-auth-cmd-marionette requires the following non-core Perl modules
 
=over
 
=item *
L
 
=back

=head1 DIAGNOSTICS

Check syslog for any errors

=head1 INCOMPATIBILITIES

This program depends on L and hence will not work in a Windows environment.  Always interested in any ssh incompatibilities.  Patches welcome.

=head1 EXIT STATUS

This program will either L a permitted program or exit with a 1.

=head1 BUGS AND LIMITATIONS

To report a bug, or view the current list of bugs, please visit L

=head1 AUTHOR

David Dick  C<<  >>

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2021, David Dick C<<  >>. All rights reserved.

This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself. See L.

=head1 DISCLAIMER OF WARRANTY

BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
NECESSARY SERVICING, REPAIR, OR CORRECTION.

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
Firefox-Marionette-1.22/check-firefox-certificate-authorities0000755000175000017500000001323214175143706023057 0ustar  davedave#! /usr/bin/perl

use strict;
use warnings;
use Getopt::Long();
use English qw( -no_match_vars );
use Firefox::Marionette();

our $VERSION = '1.22';

sub _NUMBER_OF_SPACES_FOR_CODE_QUOTING_IN_MARKDOWN { return 4 }

sub _write_common_output {
    my ($certificate) = @_;
    my $indent = q[ ] x _NUMBER_OF_SPACES_FOR_CODE_QUOTING_IN_MARKDOWN();
    warn $indent . $certificate->subject_name() . "\n";
    warn $indent
      . 'DB Key                   : '
      . $certificate->db_key() . "\n";
    warn $indent
      . 'Valid to                 : '
      . POSIX::strftime( '%d/%m/%Y', gmtime $certificate->not_valid_after() )
      . "\n";
    warn $indent
      . 'Certificate Serial Number: '
      . $certificate->serial_number() . "\n";
    warn $indent
      . 'SHA-1 Fingerprint        : '
      . $certificate->sha1_fingerprint() . "\n";
    warn $indent
      . 'SHA-256 Fingerprint      : '
      . $certificate->sha256_fingerprint() . "\n";
    return;
}

MAIN: {
    my %options;
    Getopt::Long::GetOptions( \%options, 'help', 'version', 'binary:s' );
    if ( $options{help} ) {
        require Pod::Simple::Text;
        my $parser = Pod::Simple::Text->new();
        $parser->parse_from_file($PROGRAM_NAME);
        exit 0;
    }
    elsif ( $options{version} ) {
        print "$VERSION\n"
          or die "Failed to print to STDOUT:$EXTENDED_OS_ERROR\n";
        exit 0;
    }
    my %parameters;
    if ( $options{binary} ) {
        $parameters{binary} = $options{binary};
    }
    my $firefox   = Firefox::Marionette->new(%parameters);
    my $now       = time;
    my $exit_code = 0;
    foreach my $certificate ( sort { $a->display_name() cmp $b->display_name }
        $firefox->certificates() )
    {
        if ( $certificate->is_ca_cert() ) {
            if ( $certificate->not_valid_after() < $now ) {
                warn $certificate->display_name()
                  . ' expired on '
                  . ( localtime $certificate->not_valid_after() ) . "\n";
                _write_common_output($certificate);
                $exit_code = 1;
            }
            elsif ( $certificate->not_valid_before > $now ) {
                warn $certificate->display_name()
                  . ' is not valid until '
                  . ( localtime $certificate->not_valid_before() ) . "\n";
                _write_common_output($certificate);
                $exit_code = 1;
            }
        }
    }
    $firefox->quit();
    exit $exit_code;
}

__END__
=head1 NAME

check-firefox-certificate-authorities - check the CA certificates in firefox for expired certificates

=head1 VERSION

Version 1.22

=head1 USAGE

  $ check-firefox-certificate-authorities 

  $ check-firefox-certificate-authorities --binary=/path/to/new/firefox

=head1 DESCRIPTION

This program is intended to easily check firefox for expired CA certificates.

By default, the only firefox version that may be used will be present in the PATH environment variable.  However, the user may specify a different path with
the --binary parameter.

It will print out the display name of any CA certificates that are expired or not yet valid and if it finds expired certificates, it will exit with a non-zero exit code.

=head1 REQUIRED ARGUMENTS

None

=head1 OPTIONS

Option names can be abbreviated to uniqueness and can be stated with singe or double dashes, and option values can be separated from the option name by a space or '=' (as with Getopt::Long). Option names are also case-
sensitive.

=over 4

=item * --help - This page.

=item * --binary - Use this firefox binary instead of the default firefox instance

=back

=head1 CONFIGURATION

check-firefox-certificate-authorities requires no configuration files or environment variables.

=head1 DEPENDENCIES

check-firefox-certificate-authorities requires the following non-core Perl modules
 
=over
 
=item *
L
 
=back

=head1 DIAGNOSTICS

None.

=head1 INCOMPATIBILITIES

None known.

=head1 EXIT STATUS

This program will exit with a zero after successfully completing.

=head1 BUGS AND LIMITATIONS

To report a bug, or view the current list of bugs, please visit L

=head1 AUTHOR

David Dick  C<<  >>

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2021, David Dick C<<  >>. All rights reserved.

This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself. See L.

=head1 DISCLAIMER OF WARRANTY

BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
NECESSARY SERVICING, REPAIR, OR CORRECTION.

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
Firefox-Marionette-1.22/t/0000755000175000017500000000000014175144502014033 5ustar  davedaveFirefox-Marionette-1.22/t/data/0000755000175000017500000000000014175144502014744 5ustar  davedaveFirefox-Marionette-1.22/t/data/1Passwordv7.csv0000644000175000017500000000116714175143706017633 0ustar  davedave"First One-time Password","Notes","Password","Title","Type","URL","Username",
,,"Jz*YMy.CpkLLxb.QzuJg7sDMj3","A Title for whatever","Login",,"whatever",
,,"TGe3xQxzZ8t4nfzQ-vpY6@D4GnCQaFTuD3hDe72D3btt!","Some sort of Title","Login","https://au.example.com","tésting@au.example.org",
,,"N7A@,vD\\/zHuzx:'CBnkocCZw\"s_\RE7","Another Title for Login","Login","https://example.org","random@example.com",
,"Follow these steps to get started.",,"🎉 Welcome to 1Password!","Secure Note",,,
,"It’s you! 🖠Select Edit to fill in more details, like your address and contact information.",,"Some sort of title of David","Identity",,,
Firefox-Marionette-1.22/t/data/keepass.csv0000644000175000017500000000134314175143706017122 0ustar  davedave"Account","Login Name","Password","Web Site","Comments"
"Sample Entry Title","Greg","ycXfARD2G1AOBzLlhtbn","http://www.somepage.net","Some notes..."
"Yet Another Sample Entry","Michael","qgyXFZ1iGgNqzg+eZter","http://www.anotherpage.org","More notes..."
"Entry To Test Special Characters","!\"§$%&/()=?´`_#²³{[]}\\","öäüÖÄÜ߀@<>µ©®","http://www.website.com","The user name and password fields contain special characters."
"Multi-Line Test Entry","User","bBbescXqkgGF21PK09gV","http://www.web.com","This is a multi-line comment.
This is a multi-line comment.
This is a multi-line comment.
This is a multi-line comment.
This is a multi-line comment.
This is a multi-line comment.
This is a multi-line comment."
Firefox-Marionette-1.22/t/data/1Passwordv8.1pux0000644000175000017500000000702714175143706017737 0ustar  davedavePK!f–æX^export.attributes%‹7€0w^aef ˆ&vv%2¤È¶(Bü6Þéî,ÔŠÄ>E5B]¾ì-ù,¿Sz6Ì["KÄhéÈ‚¦='õ
ⲘŸ\wÚJ7}qÝPK!SL,c”export.dataí]OoÛÈ¿çSpÕC"‘-’’’\ÇN²Î&»IœÝm¶Y,†äq8CÎI‘‹zê¥-Z @{Y=õôÞo²_ ù’²L)”DÙ²-ËÔ!1É÷¨ÇÇßû3ófž~¼£(`š4"‚w*¿•ÇŠòcño~E–Ÿ>=qFü%ð¡¼Ð91²”#dz»gDdéUX~½zúáü¤eIúG6à"?Óµý*U!+'=}õæñãoÞ½yòòÍ“§¯ûÕ¯ž=ÿÍ7z•Ú¢ò$§w…øÃ½=?íöÀyB™Õ5©¿×™P8eëÄ ÂeT²@)3’ybŒ1ÑÃ!z†F£Œë¡¡ª&©JVJ¹9§…9ý0‡xȵ™>2CO÷ÜpŒA’vâÌóMµcˆiàC"æID$_w*ç?T‰:H@öéç50%ûD	3ŠÈ,_£¶?p˜“Å.HøÔ„æ¤*¸lŽ%ç~Íe“A  u äõÞ@ïé=mЫ#Œk†ð~Ы!pæ’ÚsX÷òFeéדÚßïÕInA!qÊk•!/cê òAl}ªÕÓϵg,â¨xaâ¿ÿæçˆºpüÃ.eN<ÎRäÅ×OÁqÈŠ¿RÚ¹ìo'Ày»˜NÂ9DÉÌké?Ü=§"Þ>ƒÚøõ8ûî¾Ð‰½¾ïŽôgäð5xú6:ÒÜ#8T4CˆÏ.®žS_ÑP=¯«gzãzõÔœý¾îÖÍü–çÀ–@B¿87帰ü•*Áˆx’-§¶§ˆÿþ‚Oxª©ÏÒëoYƒ§!‹LY&Œ©Ö±¯NÄð9ôñrÔÉWƒUEª.Š”é³žP*œ2¡P[y[œ]ô˜K¤©W¹^zûûõüëÕÁ { éZ=Ãd`{¨Á;«žuöxçˆX®8®;Œ˜ªý$
­14	ØÒ7±´áƒû½f«§>ØPÄÒ/±êÑD¨€üž¤WÇâçßý+)>S>þô·?+'KǤ<±PUl„±‚ˆâS•É×ßU0ò`ÎÃ`Yr®b)&%H^DlÊüÂav;—åûŽ-™%!™¥c^éü„ÊÅâ,iV,1.”¥_V0”¯¹ ^M|C—‰kU0TØ`™¾w–PX.\nâD¾`Ö[I…Ú‰³
tç¶¿”Ô—	:’a.6…
µ%ñôȤq4b@$ˆÄ[૵åÁÔ Ròbäí|ì°ô-äVJ+‘»eÓ"‰|éÛYxmÉ{k†OD@7g#Òµ¡¹9TöZT,;€JÖpš9ñeøÌ|²ccàT[p,;N.3Ű\IÖ‘rö'Ú¬£ÔZ,,Ââ)ãõ Ñ@L¸J>ðhÊ‚z5qChZå›$Ƨނ³`Ù	pRÓŒ‚岂¡gSêkÌ1û-:–ãrê+$mÌF¤×ˆÊA‹Ê‚ePiÁ0QSñ›c@Ö:Ô׈Ía‹Í‚e°9¢†"V¦ô%4%qÚkæý˜ËsÁ•õÆ5ÎSø”¥‰Õ¥°ŠpcÕUßTP—öÔˆ´¡9ÞkÃC·¶,P²lÈ¢¸ë˜ðúR‘Bp%p)Ͳ»mCŒ–wjíÔÂŒStº´Yu §ÛjT¶•’e'PiBܨ–šÓm5*ÛAɲ5‚ˆËÐ,Ï”´[
̶>P²l0¯fàv,ó`F PŽ&ËÝVŽàЄã
†p+—]ä¥m5£½Æ9‘vW²ì„×gÐÏß/SÂò¦5¸S¦ðÆ/óZ¢¤[R@xÙ:[Qµ…hɲ-÷36fµAùéÊvˆ·ä3Òâ}4˜7ߤ&Ðà¨ÙB°F¤×èBõw¡Ñ›·”LJ¯í.0·7åì·ñ¼dÙ	@r/ßãÛ’
oÀ¢¯”µL[ʃ¯^ì¿l´ù7~µWÊZ¦-å;àRÚ’éj°ҫe-Ó–òåÉ—M éóíÝEð dɲ€´)‹|¥ì	±fûüsɱ½“ìû-DK–[TgýøÓ_þ©!+ïä¡x„&¿^]i›a:b‚h™e£wÂ(ñ	Tê_Aíµ‰©9qq¬ga8v¨æÑpŒ5kè6¾Þ«I{#Ť쓒+©l…2Yq+diš*rö-䯾tÛl-Ó–Å”&Èö²±âlìªé0HƒÌ=uÕfÑF=×4ŠGA@™˜kºh2h!qÏ”¯èÞã÷rèK›ØkK¾m0Ú¶îpµ}L+´8‹SçD&d­úT«»uºµgó?ÊÖLµœ\Ò mY{¶ýî‚«“Ælš?UðeÇïÌÆh¨Úž©{#+†JS
ÇÂûè̦÷4í~Ó^¢÷o~/Ñ/‡îÆGïåg/û<ÊÆyø˜xÔ<ü.yßá?¼ÿæÉpq¶í­3ïžS-Lf\Ô´ªÇaÁ´íÝU»™[Ù>ôüv35þÃRS¨xæÂɨ=}Ðoþ,a
á}eÍM«]V7bÈç!_ò{A”Ud®öE_UEÎØ(/r§½4Tשdqôîíï_yüÔ•Sõ"ˆoéÙê–
½tÈýtñ·qgÔ…„牿ÚÅão“ΨO)Æ4Q$&¸LãxÞÕBäi¡uy½M{ÿûÏŸ~þý¿”gòËä÷+ øgâ‰Ø‹#ûÃa”͆™é#q}O
`a‚U[@CÙ(ž½¡¦s69;pµhB0+¶¬·30íL-Ù¤ì™pÀ ÒIŒF6cߥaºé¥LÇDú9¡x6ÇeÂ&2>}—¡U6`>E9ï¶ó/íüËù‹j%æœÁ‘e£	_²\„toXÒ&aé$GW¹òVÏQžF¤k†‰&–°#?ú‰`±å8}{[†E¾ms¡ÅýHÎã«–mÚ¶ÅbÆf´a3îu•9™¯´×R™ ¦LÎ"(“ý!Ý6µñ¨"ÆzñÈtuqcÌAß±"ÃT7½~YíVBQ’‡À½i*€}:Í^D¤iÖÆ¢Ý1Ö›½²p?Іq53a.SÈÁNL6í¦µ®rˆeźŒyÅ$ôÛîUž°Ü¢$JŸ$QOg2óÂ5æ¿ëµ:‡Š3â(Nmn AÂLŒt#r2:J·$‡Â8ìCÕ7ú´Ïûªã
R׌=¬“K™Z®_Ç–Ú”ÿH¸„Ç^›Lµá¦"ÆZˆFs‡H|_ÅÜÏtnźՠ6nNêÇíw•r­Ÿ’¯õ“G3«ZÛDªGZxþ×(/ `ä,_„†Fê8Ǻûö0ÕyáÀÆ‘¾%AHV×8Õ´?àdd`&<ñ](ÓC	%êÄÈ‚t²ìœ	dâvyyfÅ˜Âø[ Lw‚˜&Î4jŽÈÐ,?)fƳýKYs.Gî"2Š%N{SÇðM)lp”%Ÿ[è¼\9IÇUðåihÝ
œ”Ô$f¡o;éÕÚ±|劕å¸À*–{˜:MÀšÉ€⻉£ßL£Ìî«}}è_˜s©.
ÉZ‹ä’e'|8ù!ø¨QŽ$ž‰>ÈtÕq;
ƒ tu>yòÓõ÷"¾APï~O³vÛÕlc;ïZäËÜÞöñ§?þAùb3ßæ&¥™&ìŸÝžÝm"¦áŽFÂêÐ4‘ãAìz	7³»m0¨i°T»»íAM½a»ÛW
/[d¸í›·îžóÉŸg¿z÷2íÞ‹c£û:‹ž;C~ôr¤ÝÜÍ|Ê­Üȶù±Ôz*»œ*û›–³¬ö×½ýe»××»ûšÞ°Ôoç͆×uÛ•£3=VôWÕUÁ>u\S®…™ÈÐi™q¤jp{~‚+‡¿YÌ8¼ó¢ÇÏùèÉwþKé6ç
¦b ãjê¦ø{§Ì·Žîˆ©
VÎÏà¢Sbv@§ê(ÏäGßßùðPK!files/PK.!f–æX^¤export.attributesPK.!SL,c”¤‡export.dataPK.!íA1
files/PK¬U
Firefox-Marionette-1.22/t/data/last_pass_example.csv0000644000175000017500000000112314175143706021167 0ustar  davedaveurl,username,password,totp,extra,name,grouping,fav
http://www.example.com,user@cpan.org,57s9PrkbznnwLCi,,,Name here,Folder here,0
https://twitter.com/login,user@example.com,57s9PrkbznnwLCi,,,Twitter,Social,0
http://sn,user,57s9PrkbznnwLCi,,"NoteType:Server
Language:en-US
Hostname:example.com
Username:user
Password:57s9PrkbznnwLCi
Notes:",Hostname for SSH,Folder here,0
http://sn,user,57s9PrkbznnwLCi,,"NoteType:Database
Language:en-US
Type:MSSQL
Hostname:example.com
Port:1433
Database:foodb
Username:user
Password:57s9PrkbznnwLCi
SID:SIDME
Alias:Alias
Notes:",Database Name,Database Folder,0
Firefox-Marionette-1.22/t/data/logins.json0000644000175000017500000000120214175143706017132 0ustar  davedave{"nextId":2,"logins":[{"id":2,"hostname":"https://example.com","httpRealm":null,"formSubmitURL":"","usernameField":"","passwordField":"","encryptedUsername":"MEIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECI1vTUub5qjtBBhdieMaf+vMu64Nu2KdOstDc0r+7NHfmt0=","encryptedPassword":"MEIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECH7c411QOWrPBBg3FnRNJRrM92VIdQiPAzF7Yxx4eNBVxp0=","guid":"{3ce34a17-7761-42f6-b6a5-e2de239bd7a9}","encType":1,"timeCreated":1626697301765,"timeLastUsed":1626697301765,"timePasswordChanged":1626697301765,"timesUsed":1}],"version":3,"disabledHosts":[],"potentiallyVulnerablePasswords":[],"dismissedBreachAlertsByLoginGUID":{}}
Firefox-Marionette-1.22/t/data/bitwarden_export_org.csv0000644000175000017500000000072514175143706021721 0ustar  davedavecollections,type,name,notes,fields,login_uri,login_username,login_password,login_totp
"Social,Marketing",login,Twitter,,,twitter.com,me@example.com,password123,
"Finance",login,My Bank,Bank PIN is 1234,"PIN: 1234
Question 1: Blue",https://www.wellsfargo.com/home.jhtml,john.smith,password123456,
,login,EVGA,,,https://www.evga.com/support/login.asp,hello@bitwarden.com,fakepassword,TOTPSEED123
,note,My Note,"This is a secure note.

Notes can span multiple lines.",,,,,Firefox-Marionette-1.22/t/data/elements.html0000644000175000017500000000556614175143706017467 0ustar  davedave

  
    
    
    Life cycle callbacks test
    
  
  
    

Elements

Firefox-Marionette-1.22/t/data/iframe.html0000644000175000017500000000024014175143706017076 0ustar davedave Iframe test

iframe

Firefox-Marionette-1.22/t/data/key4.db0000644000175000017500000110000014175143706016124 0ustar davedaveSQLite format 3€@  .O| øz<{îÃ{©{a{zÛz…ÊJó dÁš ÍY ´GI=©vK=ƒøŽzW¡»# 0 *†H†÷  0 *†H†÷   ´ ÂÝŒQ}+JPÊ(žÖë'¤&µß‹XÐy䌟%4‚passwordEú¸Mq,×hÏø"§&ì²p™00m *†H†÷  0`0A *†H†÷  04 «ý ¡l Ä%h·(b¥:®Ÿ¥>„×v)2¦U^";P 0 *†H†÷  0 `†He*忳ùS¥Ó«VÉ^RégÁb³3y?áR¦¬ ÖôÖ?sig_key_3ad42748_00000011  passwordFirefox-Marionette-1.22/t/author/0000755000175000017500000000000014175144502015335 5ustar davedaveFirefox-Marionette-1.22/t/author/bulk_test.pl0000755000175000017500000002124514175143706017702 0ustar davedave#! /usr/bin/perl use strict; use warnings; use DirHandle(); use File::HomeDir(); use File::Spec(); use File::Temp(); use Cwd(); use POSIX(); if (exists $ENV{COUNT}) { $0 = "Test run number $ENV{COUNT}"; } $ENV{RELEASE_TESTING} = 1; $ENV{FIREFOX_ALARM} = 1; my $cwd = Cwd::cwd(); our $pid; if ($pid = fork) { } elsif (defined $pid) { eval { exec { 'ping' } 'ping', '8.8.8.8' or die "Failed to exec ping:$!"; } or do { print STDERR $@; }; exit 1; } system { 'cover' } 'cover', '-delete' and die "Failed to 'cover' for " . ($ENV{FIREFOX_BINARY} || 'firefox'); my $path = File::Spec->catdir(File::HomeDir::my_home(), 'den'); my $initial_upgrade_package = 'firefox-52.0esr.tar.bz2'; my $initial_upgrade_directory = 'firefox-upgrade'; sub setup_upgrade { if (-e "$path/$initial_upgrade_package") { my $result = system "rm -Rf $path/firefox && rm -Rf $path/$initial_upgrade_directory && tar --directory $path -jxf $path/$initial_upgrade_package && mv $path/firefox $path/$initial_upgrade_directory"; $result == 0 or die "Failed to setup $initial_upgrade_directory"; } } setup_upgrade(); my $handle = DirHandle->new($path) or die "Failed to find firefox den at $path"; my @entries; while(my $entry = $handle->read()) { next if ($entry eq File::Spec->updir()); next if ($entry eq File::Spec->curdir()); next if ($entry =~ /[.]tar[.]bz2$/smx); push @entries, $entry; } foreach my $entry (reverse sort { $a cmp $b } @entries) { my $entry_version; if ($entry =~ /^firefox\-([\d.]+)(?:esr|a\d+)?$/smx) { ($entry_version) = ($1); } elsif ($entry eq 'firefox-nightly') { } elsif ($entry eq 'firefox-developer') { } elsif ($entry eq 'firefox-upgrade') { } elsif ($entry =~ /^waterfox/smx) { } else { die "Unrecognised entry '$entry' in $path"; } if ($entry =~ /^waterfox/smx) { } elsif ($entry eq 'firefox-nightly') { } elsif ($entry eq 'firefox-developer') { } elsif ($entry eq 'firefox-upgrade') { } else { my $path_to_binary = File::Spec->catfile($path, $entry, 'firefox'); my $old_version; my $old_output = `$path_to_binary --version 2>/dev/null`; if ($old_output =~ /^Mozilla[ ]Firefox[ ]([\d.]+)/smx) { ($old_version) = ($1); } else { die "$path_to_binary old '$old_output' could not be parsed"; } if ($old_version ne $entry_version) { die "$old_version does not equal $entry_version for $path_to_binary"; } } } warn "Den is correct"; my %old_versions; my %paths_to_binary; ENTRY: foreach my $entry (reverse sort { $a cmp $b } @entries) { my $entry_version; if ($entry =~ /^waterfox/smx) { } elsif ($entry eq 'firefox-nightly') { } elsif ($entry eq 'firefox-developer') { } elsif ($entry eq 'firefox-upgrade') { } elsif ($entry =~ /^firefox\-([\d.]+)(?:esr|a\d+)?$/smx) { ($entry_version) = ($1); } else { die "Unrecognised entry '$entry' in $path"; } my $path_to_binary; if ($entry =~ /^waterfox/smx) { $path_to_binary = File::Spec->catfile($path, $entry, 'waterfox'); } else { $path_to_binary = File::Spec->catfile($path, $entry, 'firefox'); } $paths_to_binary{$entry} = $path_to_binary; my $old_version; my $old_output = `$path_to_binary --version 2>/dev/null`; if ($entry =~ /^waterfox/smx) { $old_version = $old_output; } elsif ($old_output =~ /^Mozilla[ ]Firefox[ ]([\d.]+)/smx) { ($old_version) = ($1); } else { die "$path_to_binary old '$old_output' could not be parsed"; } if ($entry =~ /^waterfox/smx) { } elsif ($entry eq 'firefox-nightly') { } elsif ($entry eq 'firefox-developer') { } elsif ($entry eq 'firefox-upgrade') { } elsif ($old_version ne $entry_version) { die "$old_version does not equal $entry_version for $path_to_binary"; } $old_versions{$entry} = $old_version; } system { $^X } $^X, '-MDevel::Cover=-silent,1', '-Ilib', 't/01-marionette.t' and die "Failed to 'make'"; { local $ENV{FIREFOX_HOST} = 'localhost'; warn "Remote Firefox for " . ($ENV{FIREFOX_BINARY} || 'firefox'); system { $^X } $^X, '-MDevel::Cover=-silent,1', '-Ilib', 't/01-marionette.t' and die "Failed to 'make'"; } if (my $entry = $old_versions{'firefox-upgrade'}) { setup_upgrade(); if (defined $paths_to_binary{$entry}) { local $ENV{FIREFOX_BINARY} = $paths_to_binary{$entry}; local $ENV{FIREFOX_HOST} = 'localhost'; warn "Remote Firefox for " . $ENV{FIREFOX_BINARY}; system { $^X } $^X, '-MDevel::Cover=-silent,1', '-Ilib', 't/01-marionette.t' and die "Failed to 'make'"; } } my $firefox_nightly_failed; ENTRY: foreach my $entry (reverse sort { $a cmp $b } @entries) { my $old_version = $old_versions{$entry}; my $count = 0; my $path_to_binary = $paths_to_binary{$entry}; $ENV{FIREFOX_BINARY} = $path_to_binary; my $reset_time = 600; # 10 minutes if ($entry =~ /^waterfox/smx) { WATERFOX: { local $ENV{WATERFOX} = 1; $count += 1; my $result = system { $^X } $^X, '-MDevel::Cover=-silent,1', '-Ilib', 't/01-marionette.t'; if ($result != 0) { if ($count < 3) { warn "Failed '$^X -MDevel::Cover=-silent,1 -Ilib t/01-marionette' " . localtime . ". Sleeping for $reset_time seconds for $path_to_binary"; sleep $reset_time; redo WATERFOX; } else { die "Failed to make $count times"; } } } WATERFOX_VIA_FIREFOX: { local $ENV{WATERFOX_VIA_FIREFOX} = 1; $count += 1; my $result = system { $^X } $^X, '-MDevel::Cover=-silent,1', '-Ilib', 't/01-marionette.t'; if ($result != 0) { if ($count < 3) { warn "Failed '$^X -MDevel::Cover=-silent,1 -Ilib t/01-marionette' " . localtime . ". Sleeping for $reset_time seconds for $path_to_binary"; sleep $reset_time; redo WATERFOX_VIA_FIREFOX; } else { die "Failed to make $count times"; } } } } if (-e $ENV{FIREFOX_BINARY}) { $count = 0; LOCAL: { $count += 1; my $result = system { $^X } $^X, '-MDevel::Cover=-silent,1', '-Ilib', 't/01-marionette.t'; if ($result != 0) { if ($count < 3) { warn "Failed '$^X -MDevel::Cover=-silent,1 -Ilib t/01-marionette' " . localtime . ". Sleeping for $reset_time seconds for $path_to_binary"; if ($entry eq 'firefox-nightly') { $firefox_nightly_failed = 1; next ENTRY; } sleep $reset_time; redo LOCAL; } else { die "Failed to make $count times"; } } } if ($entry eq 'firefox-upgrade') { setup_upgrade(); } my $bash_command = 'cd ' . Cwd::cwd() . '; RELEASE_TESTING=1 FIREFOX_BINARY="' . $ENV{FIREFOX_BINARY} . "\" $^X -MDevel::Cover=-silent,1 -Ilib t/01-marionette.t"; $count = 0; SSH: { $count += 1; warn "Remote Execution of '$bash_command'"; my $result = system { 'ssh' } 'ssh', 'localhost', $bash_command; if ($result != 0) { if ($count < 3) { warn "Failed '$bash_command' " . localtime . ". Sleeping for $reset_time seconds for $path_to_binary"; if ($entry eq 'firefox-nightly') { $firefox_nightly_failed = 1; next ENTRY; } sleep $reset_time; redo SSH; } else { die "Failed to remote cover for $ENV{FIREFOX_BINARY} $count times"; } } } if ($entry eq 'firefox-upgrade') { setup_upgrade(); } $bash_command = 'cd ' . Cwd::cwd() . '; RELEASE_TESTING=1 FIREFOX_VISIBLE=1 FIREFOX_BINARY="' . $ENV{FIREFOX_BINARY} . "\" $^X -MDevel::Cover=-silent,1 -Ilib t/01-marionette.t"; $count = 0; REMOTE_VISIBLE: { $count += 1; warn "Remote Execution of '$bash_command'"; my $result = system { 'ssh' } 'ssh', 'localhost', $bash_command; if ($result != 0) { if ($count < 3) { warn "Failed '$bash_command' " . localtime . ". Sleeping for $reset_time seconds for $path_to_binary"; if ($entry eq 'firefox-nightly') { $firefox_nightly_failed = 1; next ENTRY; } sleep $reset_time; redo REMOTE_VISIBLE; } else { die "Failed to remote cover for visible $ENV{FIREFOX_BINARY} $count times"; } } } } my $new_version; my $new_output = `$path_to_binary --version 2>/dev/null`; if ($entry =~ /^waterfox/smx) { $new_version = $new_output; } elsif ($new_output =~ /^Mozilla[ ]Firefox[ ]([\d.]+)/smx) { ($new_version) = ($1); } else { die "$path_to_binary new '$new_output' could not be parsed"; } if ($entry eq 'firefox-nightly') { } elsif ($entry eq 'firefox-developer') { } elsif ($entry eq 'firefox-upgrade') { } elsif ($entry eq 'waterfox') { } elsif ($old_version ne $new_version) { die "$old_version changed to $new_version for $path_to_binary"; } } while (kill 0, $pid) { kill 'TERM', $pid; waitpid $pid, POSIX::WNOHANG(); } undef $pid; chdir $cwd or die "Failed to chdir to '$cwd':$!"; system { 'cover' } 'cover' and die "Failed to 'cover' for $ENV{FIREFOX_BINARY}"; if ($firefox_nightly_failed) { warn "Firefox Nightly failed to complete successfully\n"; } else { warn "Firefox Nightly PASSED successfully\n"; } END { if (defined $pid) { while (kill 0, $pid) { kill 'TERM', $pid; waitpid $pid, POSIX::WNOHANG(); } } } Firefox-Marionette-1.22/t/pod.t0000755000175000017500000000021414175143706015007 0ustar davedave#!perl -T use Test::More; eval "use Test::Pod 1.41"; plan skip_all => "Test::Pod 1.41 required for testing POD" if $@; all_pod_files_ok(); Firefox-Marionette-1.22/t/00.load.t0000755000175000017500000000021514175143706015363 0ustar davedaveuse Test::More tests => 1; BEGIN { use_ok( 'Firefox::Marionette' ); } diag( "Testing Firefox::Marionette $Firefox::Marionette::VERSION" ); Firefox-Marionette-1.22/t/pod-coverage.t0000755000175000017500000000105414175143706016603 0ustar davedave#!perl -T use Test::More; eval "use Test::Pod::Coverage 1.04"; plan skip_all => "Test::Pod::Coverage 1.04 required for testing POD coverage" if $@; all_pod_coverage_ok({ trustme => [ qr/^BY_(ID|NAME|CLASS|TAG|SELECTOR|LINK|PARTIAL|XPATH)$/, qr/^(find_elements?|page_source|send_keys)$/, qr/^(active_frame|switch_to_shadow_root)$/, qr/^(chrome_window_handle|chrome_window_handles|current_chrome_window_handle)$/, qr/^(ftp)$/, qr/^(xvfb)$/, qr/^(TO_JSON)$/, qr/^(list.*)$/, qr/^(accept_dialog)$/, qr/^(find_by_.*)$/, ] }); Firefox-Marionette-1.22/t/addons/0000755000175000017500000000000014175144502015303 5ustar davedaveFirefox-Marionette-1.22/t/addons/test.xpi0000755000175000017500000000103414175143706017012 0ustar davedavePK8?7Løœ©8¦ manifest.jsonUT KPfZ”PfZux õõMÍ‚0„ï<Åf„õÈM_ÃRè‚5ý!mƒQ»[[‰^&Ùof'»k€š9’ÝBÎKk°Sõ1 ×¼X'ÈÉñ‰ ÿbxd‡ÌùÁÉ9|ùY èÓ2 \)xP?ó‰.. borderify.jsUT PfZPfZux õõdocument.body.style.border = "5px solid red"; PK8?7Løœ©8¦ €manifest.jsonUTKPfZux õõPK \?7L9C>.. €íborderify.jsUTPfZux õõPK¥aFirefox-Marionette-1.22/t/addons/discogs-search/0000755000175000017500000000000014175144502020201 5ustar davedaveFirefox-Marionette-1.22/t/addons/discogs-search/manifest.json0000644000175000017500000000101514175143706022704 0ustar davedave{ "manifest_version": 2, "name": "Discogs search engine", "description": "Adds a search engine that searches discogs.com", "version": "1.0", "browser_specific_settings": { "gecko": { "id": "test@example.com", "strict_min_version": "55" } }, "chrome_settings_overrides": { "search_provider": { "name": "Discogs", "search_url": "https://www.discogs.com/search/?q={searchTerms}", "keyword": "disc", "favicon_url": "https://www.discogs.com/favicon.ico" } } } Firefox-Marionette-1.22/t/addons/discogs-search/README.md0000644000175000017500000000076114175143706021471 0ustar davedave# discogs-search ## What it does This add-on adds a search engine to the browser, that sends the search term to the [discogs.com](https://discogs.com) website. It also adds a keyword "disc", so you can type "disc Model 500" and get the discogs search engine without having to select it. ## What it shows How to use the [`chrome_settings_overrides`](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/chrome_settings_overrides) manifest key to define a new search engine. Firefox-Marionette-1.22/t/01-marionette.t0000755000175000017500000056377414175143706016643 0ustar davedave#! /usr/bin/perl use strict; use warnings; use Digest::SHA(); use MIME::Base64(); use Test::More; use Cwd(); use Firefox::Marionette(); use Waterfox::Marionette(); use Compress::Zlib(); use Config; use HTTP::Daemon(); use HTTP::Status(); use HTTP::Response(); use IO::Socket::SSL(); my $segv_detected; my $at_least_one_success; my $terminated; my $class; if (defined $ENV{WATERFOX}) { $class = 'Waterfox::Marionette'; $class->import(qw(:all)); } else { $class = 'Firefox::Marionette'; $class->import(qw(:all)); } if ($ENV{FIREFOX_ALARM}) { alarm 900; # ten minutes is heaps for bulk testing } my $test_time_limit = 90; if (($^O eq 'MSWin32') || ($^O eq 'cygwin')) { } elsif ($> == 0) { # see RT#131304 my $current = $ENV{HOME}; my $correct = (getpwuid($>))[7]; if ($current eq $correct) { } else { $ENV{HOME} = $correct; diag("Running as root. Resetting HOME environment variable from $current to $ENV{HOME}"); diag("Could be running in an environment where sudo does not reset the HOME environment variable, such as ubuntu"); } if ( exists $ENV{XAUTHORITY} ) { # see GH#1 delete $ENV{XAUTHORITY}; warn "Running as root. Deleting the XAUTHORITY environment variable\n"; } } my @sig_nums = split q[ ], $Config{sig_num}; my @sig_names = split q[ ], $Config{sig_name}; my %signals_by_name; my $idx = 0; foreach my $sig_name (@sig_names) { $signals_by_name{$sig_name} = $sig_nums[$idx]; $idx += 1; } $SIG{INT} = sub { $terminated = 1; die "Caught an INT signal"; }; $SIG{TERM} = sub { $terminated = 1; die "Caught a TERM signal"; }; sub out_of_time { my ($package, $file, $line) = caller 1; if (!defined $line) { ($package, $file, $line) = caller; } diag("Testing has been running for " . (time - $^T) . " seconds at $file line $line"); if ($ENV{RELEASE_TESTING}) { return; } elsif (time - $^T > $test_time_limit) { return 1; } else { return; } } my $launches = 0; my $ca_cert_handle; my $metacpan_ca_cert_handle; my $guid_regex = qr/[a-f\d]{8}\-[a-f\d]{4}\-[a-f\d]{4}\-[a-f\d]{4}\-[a-f\d]{12}/smx; my @old_binary_keys = (qw(firefox_binary firefox marionette));; my ($major_version, $minor_version, $patch_version); sub start_firefox { my ($require_visible, %parameters) = @_; if ($terminated) { die "Caught a signal"; } if ($ENV{FIREFOX_BINARY}) { my $key = shift @old_binary_keys; $key ||= 'binary'; $parameters{$key} = $ENV{FIREFOX_BINARY}; diag("Overriding firefox binary to $parameters{$key}"); } if ($parameters{manual_certificate_add}) { delete $parameters{manual_certificate_add}; } elsif (defined $ca_cert_handle) { if ($launches % 2) { diag("Setting trust to list"); $parameters{trust} = [ '/dev/fd/' . fileno $ca_cert_handle ]; } else { diag("Setting trust to scalar"); $parameters{trust} = '/dev/fd/' . fileno $ca_cert_handle; } } if ((defined $major_version) && ($major_version >= 61)) { } elsif ($parameters{har}) { diag("HAR support is not available for Firefox versions less than 61"); delete $parameters{har}; } if ($parameters{console}) { $parameters{console} = 1; } if (defined $ENV{WATERFOX_VIA_FIREFOX}) { $parameters{waterfox} = 1; } if (defined $ENV{FIREFOX_NIGHTLY}) { $parameters{nightly} = 1; } if (defined $ENV{FIREFOX_DEVELOPER}) { $parameters{developer} = 1; } if (defined $ENV{FIREFOX_DEBUG}) { $parameters{debug} = $ENV{FIREFOX_DEBUG}; } if ($ENV{FIREFOX_HOST}) { $parameters{host} = $ENV{FIREFOX_HOST}; diag("Overriding host to '$parameters{host}'"); if ($ENV{FIREFOX_USER}) { $parameters{user} = $ENV{FIREFOX_USER}; } elsif (($ENV{FIREFOX_HOST} eq 'localhost') && (!$ENV{FIREFOX_PORT})) { if ($launches != 0) { diag("Overriding user to 'firefox'"); $parameters{user} = 'firefox'; } } if ((defined $parameters{capabilities}) && (!$parameters{capabilities}->moz_headless())) { my $old = $parameters{capabilities}; my %new = ( moz_headless => 1 ); if (defined $old->proxy()) { $new{proxy} = $old->proxy(); } if (defined $old->moz_use_non_spec_compliant_pointer_origin()) { $new{moz_use_non_spec_compliant_pointer_origin} = $old->moz_use_non_spec_compliant_pointer_origin(); } if (defined $old->accept_insecure_certs()) { $new{accept_insecure_certs} = $old->accept_insecure_certs(); } if (defined $old->strict_file_interactability()) { $new{strict_file_interactability} = $old->strict_file_interactability(); } if (defined $old->unhandled_prompt_behavior()) { $new{unhandled_prompt_behavior} = $old->unhandled_prompt_behavior(); } if (defined $old->set_window_rect()) { $new{set_window_rect} = $old->set_window_rect(); } if (defined $old->page_load_strategy()) { $new{page_load_strategy} = $old->page_load_strategy(); } if (defined $old->moz_webdriver_click()) { $new{moz_webdriver_click} = $old->moz_webdriver_click(); } if (defined $old->moz_accessibility_checks()) { $new{moz_accessibility_checks} = $old->moz_accessibility_checks(); } if (defined $old->timeouts()) { $new{timeouts} = $old->timeouts(); } $parameters{capabilities} = Firefox::Marionette::Capabilities->new(%new); } } if ($ENV{FIREFOX_PORT}) { $parameters{port} = $ENV{FIREFOX_PORT}; } if (defined $parameters{capabilities}) { if ((defined $major_version) && ($major_version >= 52)) { } else { delete $parameters{capabilities}->{page_load_strategy}; delete $parameters{capabilities}->{moz_webdriver_click}; delete $parameters{capabilities}->{moz_accessibility_checks}; delete $parameters{capabilities}->{accept_insecure_certs}; delete $parameters{capabilities}->{strict_file_interactability}; delete $parameters{capabilities}->{unhandled_prompt_behavior}; delete $parameters{capabilities}->{set_window_rect}; delete $parameters{capabilities}->{moz_use_non_spec_compliant_pointer_origin}; } } if ($ENV{FIREFOX_VISIBLE}) { $require_visible = 1; if (!$parameters{visible}) { $parameters{visible} = 1; } if ((defined $parameters{capabilities}) && ($parameters{capabilities}->moz_headless())) { my $old = $parameters{capabilities}; my %new = ( moz_headless => 0 ); if (defined $old->proxy()) { $new{proxy} = $old->proxy(); } if (defined $old->moz_use_non_spec_compliant_pointer_origin()) { $new{moz_use_non_spec_compliant_pointer_origin} = $old->moz_use_non_spec_compliant_pointer_origin(); } if (defined $old->accept_insecure_certs()) { $new{accept_insecure_certs} = $old->accept_insecure_certs(); } if (defined $old->strict_file_interactability()) { $new{strict_file_interactability} = $old->strict_file_interactability(); } if (defined $old->unhandled_prompt_behavior()) { $new{unhandled_prompt_behavior} = $old->unhandled_prompt_behavior(); } if (defined $old->set_window_rect()) { $new{set_window_rect} = $old->set_window_rect(); } if (defined $old->page_load_strategy()) { $new{page_load_strategy} = $old->page_load_strategy(); } if (defined $old->moz_webdriver_click()) { $new{moz_webdriver_click} = $old->moz_webdriver_click(); } if (defined $old->moz_accessibility_checks()) { $new{moz_accessibility_checks} = $old->moz_accessibility_checks(); } if (defined $old->timeouts()) { $new{timeouts} = $old->timeouts(); } $parameters{capabilities} = Firefox::Marionette::Capabilities->new(%new); } diag("Overriding firefox visibility"); } my $skip_message; if ($segv_detected) { $skip_message = "Previous SEGV detected. Trying to shutdown tests as fast as possible"; return ($skip_message, undef); } if (out_of_time()) { $skip_message = "Running out of time. Trying to shutdown tests as fast as possible"; return ($skip_message, undef); } my $firefox; eval { $firefox = $class->new(%parameters); }; my $exception = $@; chomp $exception; if ($exception) { my ($package, $file, $line) = caller; my $source = $package eq 'main' ? $file : $package; diag("Exception in $source at line $line during new:$exception"); $skip_message = "SEGV detected. No need to restart"; } elsif ((!defined $firefox) && ($major_version < 50)) { $skip_message = "Failed to start Firefox:$exception"; } if ($exception =~ /^(Firefox exited with a 11|Firefox killed by a SEGV signal \(11\))/) { diag("Caught a SEGV type exception"); if ($at_least_one_success) { $skip_message = "SEGV detected. No need to restart"; $segv_detected = 1; return ($skip_message, undef); } else { diag("Running any appliable memory checks"); if ($^O eq 'linux') { diag("grep -r Mem /proc/meminfo"); diag(`grep -r Mem /proc/meminfo`); diag("ulimit -a | grep -i mem"); diag(`ulimit -a | grep -i mem`); } elsif ($^O =~ /bsd/i) { diag("sysctl hw | egrep 'hw.(phys|user|real)'"); diag(`sysctl hw | egrep 'hw.(phys|user|real)'`); diag("ulimit -a | grep -i mem"); diag(`ulimit -a | grep -i mem`); } my $time_to_recover = 2; # magic number. No science behind it. Trying to give time to allow O/S to recover. diag("About to sleep for $time_to_recover seconds to allow O/S to recover"); sleep $time_to_recover; $firefox = undef; eval { $firefox = $class->new(%parameters); }; if ($firefox) { $segv_detected = 1; } else { diag("Caught a second exception:$@"); $skip_message = "Skip tests that depended on firefox starting successfully:$@"; } } } elsif ($exception =~ /^Alarm at time exceeded/) { die $exception; } elsif ($exception) { if (($^O eq 'MSWin32') || ($^O eq 'cygwin') || ($^O eq 'darwin')) { diag("Failed to start in $^O:$exception"); } else { `Xvfb -help 2>/dev/null | grep displayfd`; if ($? == 0) { if ($require_visible) { diag("Failed to start a visible firefox in $^O but Xvfb succeeded:$exception"); } } elsif ($? == 1) { my $dbus_output = `dbus-launch 2>/dev/null`; if ($? == 0) { if ($^O eq 'freebsd') { my $mount = `mount`; if ($mount =~ /fdescfs/) { diag("Failed to start with fdescfs mounted and a working Xvfb and D-Bus:$exception"); } else { $skip_message = "Unable to launch a visible firefox in $^O without fdescfs mounted:$exception"; } } else { diag("Failed to start with a working Xvfb and D-Bus:$exception"); } if ($dbus_output =~ /DBUS_SESSION_BUS_PID=(\d+)\b/smx) { my ($dbus_pid) = ($1); while(kill 0, $dbus_pid) { kill $signals_by_name{INT}, $dbus_pid; sleep 1; waitpid $dbus_pid, POSIX::WNOHANG(); } } } else { $skip_message = "Unable to launch a visible firefox in $^O with an incorrectly setup D-Bus:$exception"; } } elsif ($require_visible) { diag("Failed to start a visible firefox in $^O but Xvfb succeeded:$exception"); $skip_message = "Skip tests that depended on firefox starting successfully:$exception"; } elsif ($ENV{DISPLAY}) { diag("Failed to start a hidden firefox in $^O with X11 DISPLAY $ENV{DISPLAY} is available:$exception"); $skip_message = "Skip tests that depended on firefox starting successfully:$exception"; } else { diag("Failed to start a hidden firefox in $^O:$exception"); } } } if (($firefox) && (!$skip_message)) { $launches += 1; } return ($skip_message, $firefox); } umask 0; my $binary = 'firefox'; if ($ENV{FIREFOX_BINARY}) { $binary = $ENV{FIREFOX_BINARY}; } elsif ( $^O eq 'MSWin32' ) { my $program_files_key; foreach my $possible ( 'ProgramFiles(x86)', 'ProgramFiles' ) { if ( $ENV{$possible} ) { $program_files_key = $possible; last; } } $binary = File::Spec->catfile( $ENV{$program_files_key}, 'Mozilla Firefox', 'firefox.exe' ); } elsif ( $^O eq 'darwin' ) { $binary = '/Applications/Firefox.app/Contents/MacOS/firefox'; } elsif ($^O eq 'cygwin') { my $windows_x86_firefox_path = "$ENV{PROGRAMFILES} (x86)/Mozilla Firefox/firefox.exe"; my $windows_firefox_path = "$ENV{PROGRAMFILES}/Mozilla Firefox/firefox.exe"; if ( -e $windows_x86_firefox_path ) { $binary = $windows_x86_firefox_path; } elsif ( -e $windows_firefox_path ) { $binary = $windows_firefox_path; } } my $version_string = `"$binary" -version`; diag("Version is $version_string"); if ((exists $ENV{FIREFOX_HOST}) && (defined $ENV{FIREFOX_HOST})) { diag("FIREFOX_HOST is $ENV{FIREFOX_HOST}"); } if ((exists $ENV{FIREFOX_USER}) && (defined $ENV{FIREFOX_USER})) { diag("FIREFOX_USER is $ENV{FIREFOX_USER}"); } if ((exists $ENV{FIREFOX_PORT}) && (defined $ENV{FIREFOX_PORT})) { diag("FIREFOX_PORT is $ENV{FIREFOX_PORT}"); } if ((exists $ENV{FIREFOX_VISIBLE}) && (defined $ENV{FIREFOX_VISIBLE})) { diag("FIREFOX_VISIBLE is $ENV{FIREFOX_VISIBLE}"); } if ($^O eq 'MSWin32') { } elsif ($^O eq 'darwin') { } else { if (exists $ENV{XAUTHORITY}) { diag("XAUTHORITY is $ENV{XAUTHORITY}"); } if (exists $ENV{DISPLAY}) { diag("DISPLAY is $ENV{DISPLAY}"); } my $dbus_output = `dbus-launch`; if ($? == 0) { diag("D-Bus is working"); if ($dbus_output =~ /DBUS_SESSION_BUS_PID=(\d+)\b/smx) { my ($dbus_pid) = ($1); while(kill 0, $dbus_pid) { kill $signals_by_name{INT}, $dbus_pid; sleep 1; waitpid $dbus_pid, POSIX::WNOHANG(); } } } else { diag("D-Bus appears to be broken. 'dbus-launch' was unable to successfully complete:$?"); } if ($^O eq 'freebsd') { diag("xorg-vfbserver version is " . `pkg info xorg-vfbserver | perl -nle 'print "\$1" if (/Version\\s+:\\s+(\\S+)\\s*/);'`); diag("xauth version is " . `pkg info xauth | perl -nle 'print "\$1" if (/Version\\s+:\\s+(\\S+)\\s*/);'`); my $machine_id_path = '/etc/machine-id'; if (-e $machine_id_path) { diag("$machine_id_path is ok"); } else { diag("$machine_id_path has not been created. Please run 'sudo dbus-uuidgen --ensure=$machine_id_path'"); } print "mount | grep fdescfs\n"; my $result = `mount | grep fdescfs`; if ($result =~ /fdescfs/) { diag("fdescfs has been mounted. /dev/fd/ should work correctly for xvfb/xauth"); } else { diag("It looks like 'sudo mount -t fdescfs fdesc /dev/fd' needs to be executed") } } elsif ($^O eq 'dragonfly') { diag("xorg-vfbserver version is " . `pkg info xorg-vfbserver | perl -nle 'print "\$1" if (/Version\\s+:\\s+(\\S+)\\s*/);'`); diag("xauth version is " . `pkg info xauth | perl -nle 'print "\$1" if (/Version\\s+:\\s+(\\S+)\\s*/);'`); my $machine_id_path = '/etc/machine-id'; if (-e $machine_id_path) { diag("$machine_id_path is ok"); } else { diag("$machine_id_path has not been created. Please run 'sudo dbus-uuidgen --ensure=$machine_id_path'"); } } elsif ($^O eq 'linux') { if (-f '/etc/debian_version') { diag("Debian Version is " . `cat /etc/debian_version`); } elsif (-f '/etc/redhat-release') { diag("Redhat Version is " . `cat /etc/redhat-release`); } `dpkg --help >/dev/null 2>/dev/null`; if ($? == 0) { diag("Xvfb deb version is " . `dpkg -s Xvfb | perl -nle 'print if s/^Version:[ ]//smx'`); } else { `rpm --help >/dev/null 2>/dev/null`; if (($? == 0) && (-f '/usr/bin/Xvfb')) { diag("Xvfb rpm version is " . `rpm -qf /usr/bin/Xvfb`); } } } } if ($^O eq 'linux') { diag("grep -r Mem /proc/meminfo"); diag(`grep -r Mem /proc/meminfo`); diag("ulimit -a | grep -i mem"); diag(`ulimit -a | grep -i mem`); } elsif ($^O =~ /bsd/i) { diag("sysctl hw | egrep 'hw.(phys|user|real)'"); diag(`sysctl hw | egrep 'hw.(phys|user|real)'`); diag("ulimit -a | grep -i mem"); diag(`ulimit -a | grep -i mem`); } my $count = 0; foreach my $name (Firefox::Marionette::Profile->names()) { my $profile = Firefox::Marionette::Profile->existing($name); $count += 1; } foreach my $name (Waterfox::Marionette::Profile->names()) { my $profile = Waterfox::Marionette::Profile->existing($name); $count += 1; } ok(1, "Read $count existing profiles"); diag("This firefox installation has $count existing profiles"); if (Firefox::Marionette::Profile->default_name()) { ok(1, "Found default profile"); } else { ok(1, "No default profile"); } if (Waterfox::Marionette::Profile->default_name()) { ok(1, "Found default waterfox profile"); } else { ok(1, "No default waterfox profile"); } my $profile; eval { if ($ENV{WATERFOX}) { $profile = Waterfox::Marionette::Profile->existing(); } else { $profile = Firefox::Marionette::Profile->existing(); } }; ok(1, "Read existing profile if any"); my $firefox; eval { $firefox = $class->new(binary => '/firefox/is/not/here'); }; chomp $@; ok((($@) and (not($firefox))), "$class->new() threw an exception when launched with an incorrect path to a binary:$@"); eval { $firefox = $class->new(binary => $^X); }; chomp $@; ok((($@) and (not($firefox))), "$class->new() threw an exception when launched with a path to a non firefox binary:$@"); my $tls_tests_ok; if ( !IO::Socket::SSL->new( PeerAddr => 'missing.example.org:443', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), ) ) { if ( IO::Socket::SSL->new( PeerAddr => 'untrusted-root.badssl.com:443', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), ) ) { if ( !IO::Socket::SSL->new( PeerAddr => 'untrusted-root.badssl.com:443', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER(), ) ) { if ( IO::Socket::SSL->new( PeerAddr => 'metacpan.org:443', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER(), ) ) { diag("TLS/Network seem okay"); $tls_tests_ok = 1; } else { diag("TLS/Network are NOT okay:Failed to connect to metacpan.org:$IO::Socket::SSL::SSL_ERROR"); } } else { diag("TLS/Network are NOT okay:Successfully connected to untrusted-root.badssl.com"); } } else { diag("TLS/Network are NOT okay:Failed to connect to untrusted-root.badssl.com:$IO::Socket::SSL::SSL_ERROR"); } } else { diag("TLS/Network are NOT okay:Successfully connected to missing.example.org"); } my $skip_message; SKIP: { if ($ENV{FIREFOX_HOST}) { skip("No profile testing when the FIREFOX_HOST override is used", 6); } if ($ENV{FIREFOX_BINARY}) { skip("No profile testing when the FIREFOX_BINARY override is used", 6); } if (!$ENV{RELEASE_TESTING}) { skip("No profile testing except for RELEASE_TESTING", 6); } my @names = Firefox::Marionette::Profile->names(); foreach my $name (@names) { next unless ($name eq 'throw'); ($skip_message, $firefox) = start_firefox(0, debug => 1, profile_name => $name ); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 6); } ok($firefox, "Firefox loaded with the $name profile"); ok($firefox->go('http://example.com'), "firefox with the $name profile loaded example.com"); ok($firefox->quit() == 0, "firefox with the $name profile quit successfully"); my $profile; if ($ENV{WATERFOX}) { $profile = Waterfox::Marionette::Profile->existing($name); } else { $profile = Firefox::Marionette::Profile->existing($name); } ($skip_message, $firefox) = start_firefox(0, profile => $profile ); if (defined $ENV{FIREFOX_DEBUG}) { ok($firefox->debug() eq $ENV{FIREFOX_DEBUG}, "\$firefox->debug() returns \$ENV{FIREFOX_DEBUG}:$ENV{FIREFOX_DEBUG}"); } else { ok(!$firefox->debug(1), "\$firefox->debug(1) returns false but sets debug to true"); ok($firefox->debug(), "\$firefox->debug() returns true"); } ok($firefox, "Firefox loaded with a profile copied from $name"); ok($firefox->go('http://example.com'), "firefox with the copied profile from $name loaded example.com"); ok($firefox->quit() == 0, "firefox with the profile copied from $name quit successfully"); } } if ($ENV{WATERFOX}) { ok($profile = Waterfox::Marionette::Profile->new(), "Waterfox::Marionette::Profile->new() correctly returns a new profile"); } else { ok($profile = Firefox::Marionette::Profile->new(), "Firefox::Marionette::Profile->new() correctly returns a new profile"); } ok(((defined $profile->get_value('marionette.port')) && ($profile->get_value('marionette.port') == 0)), "\$profile->get_value('marionette.port') correctly returns 0"); ok($profile->set_value('browser.link.open_newwindow', 2), "\$profile->set_value('browser.link.open_newwindow', 2) to force new windows to appear"); ok($profile->set_value('browser.link.open_external', 2), "\$profile->set_value('browser.link.open_external', 2) to force new windows to appear"); ok($profile->set_value('browser.block.target_new_window', 'false'), "\$profile->set_value('browser.block.target_new_window', 'false') to force new windows to appear"); $profile->set_value('browser.link.open_newwindow', 2); # open in a new window $profile->set_value('browser.link.open_newwindow.restriction', 1); # don't restrict new windows $profile->set_value('dom.disable_open_during_load', 'false'); # don't block popups during page load $profile->set_value('privacy.popups.disable_from_plugin', 0); # no restrictions $profile->set_value('security.OCSP.GET.enabled', 'false'); $profile->clear_value('security.OCSP.enabled'); # just testing $profile->set_value('security.OCSP.enabled', 0); if ($ENV{FIREFOX_BINARY}) { $profile->set_value('security.sandbox.content.level', 0, 0); # https://wiki.mozilla.org/Security/Sandbox#Customization_Settings } my $correct_exit_status = 0; my $mozilla_pid_support; SKIP: { diag("Initial tests"); ($skip_message, $firefox) = start_firefox(0, debug => 1, profile => $profile, mime_types => [ 'application/pkcs10', 'application/pdf' ]); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 38); } if (defined $ENV{FIREFOX_DEBUG}) { ok($firefox->debug() eq $ENV{FIREFOX_DEBUG}, "\$firefox->debug() returns \$ENV{FIREFOX_DEBUG}:$ENV{FIREFOX_DEBUG}"); } else { ok($firefox->debug(), "\$firefox->debug() returns true"); } ok($firefox, "Firefox has started in Marionette mode"); ok((scalar grep { /^application\/pkcs10$/ } $firefox->mime_types()), "application/pkcs10 has been added to mime_types"); ok((scalar grep { /^application\/pdf$/ } $firefox->mime_types()), "application/pdf was already in mime_types"); ok((scalar grep { /^application\/x\-gzip$/ } $firefox->mime_types()), "application/x-gzip was already in mime_types"); ok((!scalar grep { /^text\/html$/ } $firefox->mime_types()), "text/html should not be in mime_types"); my $capabilities = $firefox->capabilities(); ok(1, "\$capabilities->proxy() " . defined $capabilities->proxy() ? "shows an existing proxy setup" : "is undefined"); diag("Browser version is " . $capabilities->browser_version()); if ($firefox->nightly()) { diag($capabilities->browser_version() . " is a nightly release"); } if ($firefox->developer()) { diag($capabilities->browser_version() . " is a developer release"); } ($major_version, $minor_version, $patch_version) = split /[.]/smx, $capabilities->browser_version(); if (!defined $minor_version) { $minor_version = ''; } if (!defined $patch_version) { $patch_version = ''; } diag("Operating System is " . ($capabilities->platform_name() || 'Unknown') . q[ ] . ($capabilities->platform_version() || 'Unknown')); diag("Profile Directory is " . $capabilities->moz_profile()); diag("Mozilla PID is " . ($capabilities->moz_process_id() || 'Unknown')); $mozilla_pid_support = defined $capabilities->moz_process_id() ? 1 : 0; diag("Firefox BuildID is " . ($capabilities->moz_build_id() || 'Unknown')); diag("Addons are " . ($firefox->addons() ? 'working' : 'disabled')); if (($ENV{RELEASE_TESTING}) && ($major_version >= 52)) { my $update = $firefox->update(); ok(ref $update eq 'Firefox::Marionette::UpdateStatus', "\$firefox->update() produces a Firefox::Marionette::UpdateStatus object"); diag("Update status code is " . $update->update_status_code()); if ($update->successful()) { while ($update->successful()) { ok(1, "Firefox was updated"); my $capabilities = $firefox->capabilities(); diag("Firefox BuildID is " . ($capabilities->moz_build_id() || 'Unknown') . " after an update"); foreach my $key (qw(app_version build_id channel details_url display_version elevation_failure error_code install_date is_complete_update name number_of_updates patch_count previous_app_version prompt_wait_time selected_patch service_url status_text type unsupported update_state update_status_code)) { if (defined $update->$key()) { if ($key =~ /^(elevation_failure|unsupported|is_complete_update)$/smx) { ok((($update->$key() == 1) || ($update->$key() == 0)), "\$update->$key() produces a boolean:" . $update->$key()); } elsif ($key eq 'type') { ok($update->$key() =~ /^(major|partial|minor|complete)$/smx, "\$update->$key() produces an allowed type:" . $update->$key()); } else { ok(1, "\$update->$key() produces a result:" . $update->$key()); } } else { ok(1, "\$update->$key() produces undef"); } } $update = $firefox->update(); if (defined $update->app_version()) { diag("New Browser version is " . $update->app_version()); ($major_version, $minor_version, $patch_version) = split /[.]/smx, $update->app_version(); } } } elsif (defined $update->number_of_updates()) { ok(1, "Firefox was NOT updated"); ok($update->number_of_updates() =~ /^\d+$/smx, "There were " . $update->number_of_updates() . " updates available"); } else { diag("Unable to determine the number of updates available"); ok(1, "Unable to determine the number of updates available"); } } if ($ENV{FIREFOX_HOST}) { ok(-d $firefox->ssh_local_directory(), "Firefox::Marionette->ssh_local_directory() returns the existing ssh local directory:" . $firefox->ssh_local_directory()); } else { ok(-d $firefox->root_directory(), "Firefox::Marionette->root_directory() returns the exising local directory:" . $firefox->root_directory()); } ok($firefox->application_type(), "\$firefox->application_type() returns " . $firefox->application_type()); ok($firefox->marionette_protocol() =~ /^\d+$/smx, "\$firefox->marionette_protocol() returns " . $firefox->marionette_protocol()); my $window_type = $firefox->window_type(); ok($window_type && $window_type eq 'navigator:browser', "\$firefox->window_type() returns 'navigator:browser':$window_type"); ok($firefox->sleep_time_in_ms() == 1, "\$firefox->sleep_time_in_ms() is 1 millisecond"); my $new_x = 3; my $new_y = 9; my $new_height = 452; my $new_width = 326; my $new = Firefox::Marionette::Window::Rect->new( pos_x => $new_x, pos_y => $new_y, height => $new_height, width => $new_width ); my $old; eval { $old = $firefox->window_rect($new); }; SKIP: { if (($major_version < 50) && (!defined $old)) { skip("Firefox $major_version does not appear to support the \$firefox->window_rect() method", 13); } TODO: { local $TODO = $major_version < 55 ? $capabilities->browser_version() . " probably does not have support for \$firefox->window_rect()->pos_x()" : q[]; ok(defined $old->pos_x() && $old->pos_x() =~ /^\-?\d+([.]\d+)?$/, "Window used to have a X position of " . (defined $old->pos_x() ? $old->pos_x() : q[])); ok(defined $old->pos_y() && $old->pos_y() =~ /^\-?\d+([.]\d+)?$/, "Window used to have a Y position of " . (defined $old->pos_y() ? $old->pos_y() : q[])); } ok($old->width() =~ /^\d+([.]\d+)?$/, "Window used to have a width of " . $old->width()); ok($old->height() =~ /^\d+([.]\d+)?$/, "Window used to have a height of " . $old->height()); my $new2 = $firefox->window_rect(); TODO: { local $TODO = $major_version < 55 ? $capabilities->browser_version() . " probably does not have support for \$firefox->window_rect()->pos_x()" : q[]; ok(defined $new2->pos_x() && $new2->pos_x() == $new->pos_x(), "Window has a X position of " . $new->pos_x()); ok(defined $new2->pos_y() && $new2->pos_y() == $new->pos_y(), "Window has a Y position of " . $new->pos_y()); } TODO: { local $TODO = $major_version >= 60 && $^O eq 'darwin' ? "darwin has dodgy support for \$firefox->window_rect()->width()" : $firefox->nightly() ? "Nightly returns incorrect values for \$firefox->window_rect()->width()" : q[]; ok($new2->width() >= $new->width(), "Window has a width of " . $new->width() . ":" . $new2->width()); } ok($new2->height() == $new->height(), "Window has a height of " . $new->height()); TODO: { local $TODO = $major_version < 57 ? $capabilities->browser_version() . " probably does not have support for \$firefox->window_rect()->wstate()" : $major_version >= 66 ? $capabilities->browser_version() . " probably does not have support for \$firefox->window_rect()->wstate()" : q[]; ok(defined $old->wstate() && $old->wstate() =~ /^\w+$/, "Window has a state of " . ($old->wstate() || q[])); } my $rect = $firefox->window_rect(); TODO: { local $TODO = $major_version < 55 ? $capabilities->browser_version() . " probably does not have support for \$firefox->window_rect()->pos_x()" : q[]; ok(defined $rect->pos_x() && $rect->pos_x() =~ /^[-]?\d+([.]\d+)?$/, "Window has a X position of " . ($rect->pos_x() || q[])); ok(defined $rect->pos_y() && $rect->pos_y() =~ /^[-]?\d+([.]\d+)?$/, "Window has a Y position of " . ($rect->pos_y() || q[])); } ok($rect->width() =~ /^\d+([.]\d+)?$/, "Window has a width of " . $rect->width()); ok($rect->height() =~ /^\d+([.]\d+)?$/, "Window has a height of " . $rect->height()); } my $page_timeout = 45_043; my $script_timeout = 48_021; my $implicit_timeout = 41_001; $new = Firefox::Marionette::Timeouts->new(page_load => $page_timeout, script => $script_timeout, implicit => $implicit_timeout); my $timeouts = $firefox->timeouts($new); ok((ref $timeouts) eq 'Firefox::Marionette::Timeouts', "\$firefox->timeouts(\$new) returns a Firefox::Marionette::Timeouts object"); if ($ENV{RELEASE_TESTING}) { $firefox->restart(); my $restart_timeouts = $firefox->timeouts(); ok($restart_timeouts->page_load() == $page_timeout, "\$timeouts->page_load() is $page_timeout"); ok($restart_timeouts->script() == $script_timeout, "\$timeouts->script() is $script_timeout"); ok($restart_timeouts->implicit() == $implicit_timeout, "\$timeouts->implicit() is $implicit_timeout"); } my $timeouts2 = $firefox->timeouts(); ok((ref $timeouts2) eq 'Firefox::Marionette::Timeouts', "\$firefox->timeouts() returns a Firefox::Marionette::Timeouts object"); ok($timeouts->page_load() == 300_000, "\$timeouts->page_load() is 5 minutes"); ok($timeouts->script() == 30_000, "\$timeouts->script() is 30 seconds"); ok(defined $timeouts->implicit() && $timeouts->implicit() == 0, "\$timeouts->implicit() is 0 milliseconds"); $timeouts = $firefox->timeouts($new); ok($timeouts->page_load() == $page_timeout, "\$timeouts->page_load() is $page_timeout"); ok($timeouts->script() == $script_timeout, "\$timeouts->script() is $script_timeout"); ok($timeouts->implicit() == $implicit_timeout, "\$timeouts->implicit() is $implicit_timeout"); ok(!defined $firefox->child_error(), "Firefox does not have a value for child_error"); ok($firefox->alive(), "Firefox is still alive"); ok(not($firefox->script('window.open("https://duckduckgo.com", "_blank");')), "Opening new window to duckduckgo.com via 'window.open' script"); ok($firefox->close_current_window_handle(), "Closed new tab/window"); SKIP: { if ($major_version < 55) { skip("Deleting and re-creating sessions can hang firefox for old versions", 1); } ok($firefox->delete_session()->new_session(), "\$firefox->delete_session()->new_session() has cleared the old session and created a new session"); } my $child_error = $firefox->quit(); if ($child_error != 0) { diag("Firefox exited with a \$? of $child_error"); } ok($child_error =~ /^\d+$/, "Firefox has closed with an integer exit status of " . $child_error); if ($major_version < 50) { $correct_exit_status = $child_error; } ok($firefox->child_error() == $child_error, "Firefox returns $child_error for the child error, matching the return value of quit():$child_error:" . $firefox->child_error()); ok(!$firefox->alive(), "Firefox is not still alive"); } if ((!defined $major_version) || ($major_version < 40)) { $profile->set_value('security.tls.version.max', 3); } $profile->set_value('browser.newtabpage.activity-stream.feeds.favicon', 'true'); $profile->set_value('browser.shell.shortcutFavicons', 'true'); $profile->set_value('browser.newtabpage.enabled', 'true'); $profile->set_value('browser.pagethumbnails.capturing_disabled', 'false', 0); $profile->set_value('startup.homepage_welcome_url', 'false', 0); SKIP: { if (($^O eq 'MSWin32') || ($^O eq 'cygwin')) { skip("$^O is not supported for reconnecting yet", 8); } elsif (!$mozilla_pid_support) { skip("No pid support for this version of firefox", 8); } elsif (!$ENV{RELEASE_TESTING}) { skip("No survive testing except for RELEASE_TESTING", 8); } diag("Starting new firefox for testing reconnecting"); ($skip_message, $firefox) = start_firefox(0, debug => 1, survive => 1); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 8); } ok($firefox, "Firefox has started in Marionette mode with as survivable"); my $capabilities = $firefox->capabilities(); ok((ref $capabilities) eq 'Firefox::Marionette::Capabilities', "\$firefox->capabilities() returns a Firefox::Marionette::Capabilities object"); my $firefox_pid = $capabilities->moz_process_id(); ok($firefox_pid, "Firefox process has a process id of $firefox_pid"); if (!$ENV{FIREFOX_HOST}) { ok((kill 0, $firefox_pid), "Can contact firefox process ($firefox_pid)"); } $firefox = undef; if (!$ENV{FIREFOX_HOST}) { ok((kill 0, $firefox_pid), "Can contact firefox process ($firefox_pid)"); } ($skip_message, $firefox) = start_firefox(0, debug => 1, reconnect => 1); ok($firefox, "Firefox has reconnected in Marionette mode"); $capabilities = $firefox->capabilities(); ok($firefox_pid == $capabilities->moz_process_id(), "Firefox has the same process id"); $firefox = undef; if (!$ENV{FIREFOX_HOST}) { ok((!kill 0, $firefox_pid), "Cannot contact firefox process ($firefox_pid)"); } if (!$ENV{FIREFOX_HOST}) { if ($ENV{FIREFOX_BINARY}) { skip("No profile testing when the FIREFOX_BINARY override is used", 6); } if (!$ENV{RELEASE_TESTING}) { skip("No profile testing except for RELEASE_TESTING", 6); } my $name = 'throw'; ($skip_message, $firefox) = start_firefox(0, debug => 1, har => 1, survive => 1, profile_name => $name ); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 8); } ok($firefox, "Firefox has started in Marionette mode with as survivable with a profile_name and har"); my $capabilities = $firefox->capabilities(); ok((ref $capabilities) eq 'Firefox::Marionette::Capabilities', "\$firefox->capabilities() returns a Firefox::Marionette::Capabilities object"); my $firefox_pid = $capabilities->moz_process_id(); ok($firefox_pid, "Firefox process has a process id of $firefox_pid"); ok((kill 0, $firefox_pid), "Can contact firefox process ($firefox_pid)"); $firefox = undef; ok((kill 0, $firefox_pid), "Can contact firefox process ($firefox_pid)"); ($skip_message, $firefox) = start_firefox(0, debug => 1, reconnect => 1, profile_name => $name); ok($firefox, "Firefox has reconnected in Marionette mode"); ok($firefox_pid == $capabilities->moz_process_id(), "Firefox has the same process id"); $firefox = undef; ok(!(kill 0, $firefox_pid), "Cannot contact firefox process ($firefox_pid)"); } } if (($^O eq 'MSWin32') || ($^O eq 'cygwin')) { } elsif ($ENV{RELEASE_TESTING}) { eval { $ca_cert_handle = File::Temp->new( TEMPLATE => File::Spec->catfile( File::Spec->tmpdir(), 'firefox_test_ca_cert_XXXXXXXXXXX')) or Firefox::Marionette::Exception->throw( "Failed to open temporary file for writing:$!"); fcntl $ca_cert_handle, Fcntl::F_SETFD(), 0 or Carp::croak("Can't clear close-on-exec flag on temporary file:$!"); my $ca_private_key_handle = File::Temp->new( TEMPLATE => File::Spec->catfile( File::Spec->tmpdir(), 'firefox_test_ca_private_XXXXXXXXXXX')) or Firefox::Marionette::Exception->throw( "Failed to open temporary file for writing:$!"); system {'openssl'} 'openssl', 'genrsa', '-out' => $ca_private_key_handle->filename(), 4096 and Carp::croak("Failed to generate a private key:$!"); my $ca_config_handle = File::Temp->new( TEMPLATE => File::Spec->catfile( File::Spec->tmpdir(), 'firefox_test_ca_config_XXXXXXXXXXX')) or Firefox::Marionette::Exception->throw( "Failed to open temporary file for writing:$!"); $ca_config_handle->print(<<"_CONFIG_"); [ req ] distinguished_name = req_distinguished_name attributes = req_attributes prompt = no [ req_distinguished_name ] C = AU ST = Victoria L = Melbourne O = David Dick OU = CPAN CN = Firefox::Marionette Root CA emailAddress = ddick\@cpan.org [ req_attributes ] _CONFIG_ seek $ca_config_handle, 0, 0 or Carp::croak("Failed to seek to start of temporary file:$!"); fcntl $ca_config_handle, Fcntl::F_SETFD(), 0 or Carp::croak("Can't clear close-on-exec flag on temporary file:$!"); system {'openssl'} 'openssl', 'req', '-x509', '-set_serial' => '1', '-config' => $ca_config_handle->filename(), '-days' => 10, '-key' => $ca_private_key_handle->filename(), '-out' => $ca_cert_handle->filename() and Carp::croak("Failed to generate a CA root certificate:$!"); 1; } or do { chomp $@; diag("Did not generate a CA root certificate:$@"); }; } SKIP: { diag("Starting new firefox for testing capabilities and accessing proxies"); my $daemon = HTTP::Daemon->new(LocalAddr => 'localhost') || die "Failed to create HTTP::Daemon"; my $localPort = URI->new($daemon->url())->port(); my %proxy_parameters = (http => 'localhost:' . $localPort, https => 'proxy.example.org:4343', none => [ 'local.example.org' ], socks => 'socks.example.org:1081'); if ((defined $major_version) && ($major_version < 90)) { $proxy_parameters{ftp} = 'ftp.example.org:2121'; } my $proxy = Firefox::Marionette::Proxy->new(%proxy_parameters); ($skip_message, $firefox) = start_firefox(0, kiosk => 1, sleep_time_in_ms => 5, profile => $profile, capabilities => Firefox::Marionette::Capabilities->new(proxy => $proxy, moz_headless => 1, strict_file_interactability => 1, accept_insecure_certs => 1, page_load_strategy => 'eager', unhandled_prompt_behavior => 'accept and notify', moz_webdriver_click => 1, moz_accessibility_checks => 1, moz_use_non_spec_compliant_pointer_origin => 1, timeouts => Firefox::Marionette::Timeouts->new(page_load => 54_321, script => 4567, implicit => 6543))); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 26); } ok($firefox, "Firefox has started in Marionette mode with definable capabilities set to known values"); ok($firefox->sleep_time_in_ms() == 5, "\$firefox->sleep_time_in_ms() is 5 milliseconds"); my $capabilities = $firefox->capabilities(); ok((ref $capabilities) eq 'Firefox::Marionette::Capabilities', "\$firefox->capabilities() returns a Firefox::Marionette::Capabilities object"); SKIP: { if (!grep /^set_window_rect$/, $capabilities->enumerate()) { diag("\$capabilities->set_window_rect is not supported for " . $capabilities->browser_version()); skip("\$capabilities->set_window_rect is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->set_window_rect() =~ /^[10]$/smx, "\$capabilities->set_window_rect() is a 0 or 1"); } SKIP: { if (!grep /^unhandled_prompt_behavior$/, $capabilities->enumerate()) { diag("\$capabilities->unhandled_prompt_behavior is not supported for " . $capabilities->browser_version()); skip("\$capabilities->unhandled_prompt_behavior is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->unhandled_prompt_behavior() eq 'accept and notify', "\$capabilities->unhandled_prompt_behavior() is 'accept and notify'"); } SKIP: { if (!grep /^moz_shutdown_timeout$/, $capabilities->enumerate()) { diag("\$capabilities->moz_shutdown_timeout is not supported for " . $capabilities->browser_version()); skip("\$capabilities->moz_shutdown_timeout is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->moz_shutdown_timeout() =~ /^\d+$/smx, "\$capabilities->moz_shutdown_timeout() is an integer"); } SKIP: { if (!grep /^strict_file_interactability$/, $capabilities->enumerate()) { diag("\$capabilities->strict_file_interactability is not supported for " . $capabilities->browser_version()); skip("\$capabilities->strict_file_interactability is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->strict_file_interactability() == 1, "\$capabilities->strict_file_interactability() is set to true"); } SKIP: { if (!grep /^page_load_strategy$/, $capabilities->enumerate()) { diag("\$capabilities->page_load_strategy is not supported for " . $capabilities->browser_version()); skip("\$capabilities->page_load_strategy is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->page_load_strategy() eq 'eager', "\$capabilities->page_load_strategy() is 'eager'"); } SKIP: { if (!grep /^accept_insecure_certs$/, $capabilities->enumerate()) { diag("\$capabilities->accept_insecure_certs is not supported for " . $capabilities->browser_version()); skip("\$capabilities->accept_insecure_certs is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->accept_insecure_certs() == 1, "\$capabilities->accept_insecure_certs() is set to true"); } SKIP: { if (!grep /^moz_webdriver_click$/, $capabilities->enumerate()) { diag("\$capabilities->moz_webdriver_click is not supported for " . $capabilities->browser_version()); skip("\$capabilities->moz_webdriver_click is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->moz_webdriver_click() == 1, "\$capabilities->moz_webdriver_click() is set to true"); } SKIP: { if (!grep /^moz_use_non_spec_compliant_pointer_origin$/, $capabilities->enumerate()) { diag("\$capabilities->moz_use_non_spec_compliant_pointer_origin is not supported for " . $capabilities->browser_version()); skip("\$capabilities->moz_use_non_spec_compliant_pointer_origin is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->moz_use_non_spec_compliant_pointer_origin() == 1, "\$capabilities->moz_use_non_spec_compliant_pointer_origin() is set to true"); } SKIP: { if (!grep /^moz_accessibility_checks$/, $capabilities->enumerate()) { diag("\$capabilities->moz_accessibility_checks is not supported for " . $capabilities->browser_version()); skip("\$capabilities->moz_accessibility_checks is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->moz_accessibility_checks() == 1, "\$capabilities->moz_accessibility_checks() is set to true"); } TODO: { local $TODO = $major_version < 56 ? $capabilities->browser_version() . " does not have support for -headless argument" : q[]; ok($capabilities->moz_headless() == 1 || $ENV{FIREFOX_VISIBLE} || 0, "\$capabilities->moz_headless() is set to " . ($ENV{FIREFOX_VISIBLE} ? 'true' : 'false')); } if (out_of_time()) { skip("Running out of time. Trying to shutdown tests as fast as possible", 13); } $capabilities = $firefox->capabilities(); ok((ref $capabilities) eq 'Firefox::Marionette::Capabilities', "\$firefox->capabilities() returns a Firefox::Marionette::Capabilities object"); SKIP: { if (!$capabilities->proxy()) { diag("\$capabilities->proxy is not supported for " . $capabilities->browser_version()); skip("\$capabilities->proxy is not supported for " . $capabilities->browser_version(), 10); } ok($capabilities->proxy()->type() eq 'manual', "\$capabilities->proxy()->type() is 'manual'"); ok($capabilities->proxy()->http() eq 'localhost:' . $localPort, "\$capabilities->proxy()->http() is 'localhost:" . $localPort . "':" . $capabilities->proxy()->http()); ok($capabilities->proxy()->https() eq 'proxy.example.org:4343', "\$capabilities->proxy()->https() is 'proxy.example.org:4343'"); if ($major_version < 90) { ok($capabilities->proxy()->ftp() eq 'ftp.example.org:2121', "\$capabilities->proxy()->ftp() is 'ftp.example.org:2121'"); } ok($capabilities->timeouts()->page_load() == 54_321, "\$capabilities->timeouts()->page_load() is '54,321'"); ok($capabilities->timeouts()->script() == 4567, "\$capabilities->timeouts()->script() is '4,567'"); ok($capabilities->timeouts()->implicit() == 6543, "\$capabilities->timeouts()->implicit() is '6,543'"); my $none = 0; foreach my $host ($capabilities->proxy()->none()) { $none += 1; } ok($capabilities->proxy()->socks() eq 'socks.example.org:1081', "\$capabilities->proxy()->socks() is 'socks.example.org:1081':" . $capabilities->proxy()->socks() ); ok($capabilities->proxy()->socks_version() == 5, "\$capabilities->proxy()->socks_version() is 5"); TODO: { local $TODO = $major_version < 58 ? $capabilities->browser_version() . " does not have support for \$firefox->capabilities()->none()" : q[]; ok($none == 1, "\$capabilities->proxy()->none() is a reference to a list with 1 element"); } } if (out_of_time()) { skip("Running out of time. Trying to shutdown tests as fast as possible", 2); } SKIP: { if (($ENV{FIREFOX_HOST}) && ($ENV{FIREFOX_HOST} ne 'localhost')) { diag("\$capabilities->proxy is not supported for remote hosts"); skip("\$capabilities->proxy is not supported for remote hosts", 1); } elsif (($ENV{FIREFOX_HOST}) && ($ENV{FIREFOX_HOST} eq 'localhost') && ($ENV{FIREFOX_PORT})) { diag("\$capabilities->proxy is not supported for remote hosts"); skip("\$capabilities->proxy is not supported for remote hosts", 3); } elsif (!$capabilities->proxy()) { skip("\$capabilities->proxy is not supported for " . $capabilities->browser_version(), 1); } elsif ((exists $Config::Config{'d_fork'}) && (defined $Config::Config{'d_fork'}) && ($Config::Config{'d_fork'} eq 'define')) { if ($ENV{RELEASE_TESTING}) { my $handle = File::Temp->new( TEMPLATE => File::Spec->catfile( File::Spec->tmpdir(), 'firefox_test_proxy_XXXXXXXXXXX')) or Firefox::Marionette::Exception->throw( "Failed to open temporary file for writing:$!"); fcntl $handle, Fcntl::F_SETFD(), 0 or Carp::croak("Can't clear close-on-exec flag on temporary file:$!"); if (my $pid = fork) { my $url = 'http://wtf.example.org'; my $favicon_url = 'http://wtf.example.org/favicon.ico'; $firefox->go($url); ok($firefox->html() =~ /success/smx, "Correctly accessed the Proxy"); diag($firefox->html()); while(kill $signals_by_name{TERM}, $pid) { waitpid $pid, POSIX::WNOHANG(); sleep 1; } $handle->seek(0,0) or die "Failed to seek to start of temporary file for proxy check:$!"; my $quoted_url = quotemeta $url; my $quoted_favicon_url = quotemeta $favicon_url; while(my $line = <$handle>) { chomp $line; if ($line =~ /^$favicon_url$/smx) { } elsif ($line !~ /^$quoted_url\/?$/smx) { die "Firefox is requesting this $line without any reason"; } } } elsif (defined $pid) { eval { local $SIG{ALRM} = sub { die "alarm during proxy server\n" }; alarm 5; $0 = "[Test HTTP Proxy for " . getppid . "]"; while (my $connection = $daemon->accept()) { diag("Accepted connection"); if (my $child = fork) { } elsif (defined $child) { eval { local $SIG{ALRM} = sub { die "alarm during proxy server accept\n" }; alarm 5; while (my $request = $connection->get_request()) { diag("Got request for " . $request->uri()); $handle->print($request->uri() . "\n"); my $response = HTTP::Response->new(200, "OK", undef, "success"); $connection->send_response($response); } $connection->close; $connection = undef; exit 0; } or do { chomp $@; diag("Caught exception in proxy server accept:$@"); }; exit 1; } else { diag("Failed to fork connection:$!"); die "Failed to fork:$!"; } } } or do { chomp $@; diag("Caught exception in proxy server:$@"); }; exit 1; } else { diag("Failed to fork http proxy:$!"); die "Failed to fork:$!"; } } else { skip("Skipping proxy forks except for RELEASE_TESTING=1", 1); diag("Skipping proxy forks except for RELEASE_TESTING=1"); } } else { skip("No forking available for $^O", 1); diag("No forking available for $^O"); } } ok($firefox->quit() == $correct_exit_status, "Firefox has closed with an exit status of $correct_exit_status:" . $firefox->child_error()); } SKIP: { diag("Starting new firefox for testing proxies"); ($skip_message, $firefox) = start_firefox(0, chatty => 1, devtools => 1, debug => 1, page_load => 65432, capabilities => Firefox::Marionette::Capabilities->new(proxy => Firefox::Marionette::Proxy->new( pac => URI->new('https://proxy.example.org')), moz_headless => 1)); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 6); } ok($firefox, "Firefox has started in Marionette mode with definable capabilities set to known values"); my $capabilities = $firefox->capabilities(); ok((ref $capabilities) eq 'Firefox::Marionette::Capabilities', "\$firefox->capabilities() returns a Firefox::Marionette::Capabilities object"); SKIP: { if (!$capabilities->proxy()) { diag("\$capabilities->proxy is not supported for " . $capabilities->browser_version()); skip("\$capabilities->proxy is not supported for " . $capabilities->browser_version(), 2); } ok($capabilities->proxy()->type() eq 'pac', "\$capabilities->proxy()->type() is 'pac'"); ok($capabilities->proxy()->pac()->host() eq 'proxy.example.org', "\$capabilities->proxy()->pac()->host() is 'proxy.example.org'"); } ok($capabilities->timeouts()->page_load() == 65432, "\$firefox->capabilities()->timeouts()->page_load() correctly reflects the page_load shortcut timeout"); ok($firefox->quit() == $correct_exit_status, "Firefox has closed with an exit status of $correct_exit_status:" . $firefox->child_error()); } SKIP: { diag("Starting new firefox for testing proxies again"); ($skip_message, $firefox) = start_firefox(1, seer => 1, chatty => 1, debug => 1, capabilities => Firefox::Marionette::Capabilities->new(proxy => Firefox::Marionette::Proxy->new( host => 'proxy.example.org:3128'))); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 7); } ok($firefox, "Firefox has started in Marionette mode with definable capabilities set to known values"); my $capabilities = $firefox->capabilities(); ok((ref $capabilities) eq 'Firefox::Marionette::Capabilities', "\$firefox->capabilities() returns a Firefox::Marionette::Capabilities object"); SKIP: { if (!$capabilities->proxy()) { diag("\$capabilities->proxy is not supported for " . $capabilities->browser_version()); skip("\$capabilities->proxy is not supported for " . $capabilities->browser_version(), 4); } ok($capabilities->proxy()->type() eq 'manual', "\$capabilities->proxy()->type() is 'manual'"); ok($capabilities->proxy()->https() eq 'proxy.example.org:3128', "\$capabilities->proxy()->https() is 'proxy.example.org:3128'"); ok($capabilities->proxy()->http() eq 'proxy.example.org:3128', "\$capabilities->proxy()->http() is 'proxy.example.org:3128'"); } ok($firefox->quit() == $correct_exit_status, "Firefox has closed with an exit status of $correct_exit_status:" . $firefox->child_error()); } SKIP: { diag("Starting new firefox for testing PDFs and script elements"); ($skip_message, $firefox) = start_firefox(0, capabilities => Firefox::Marionette::Capabilities->new(accept_insecure_certs => 1, moz_headless => 1)); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 6); } if (!$tls_tests_ok) { skip("TLS test infrastructure seems compromised", 6); } ok($firefox, "Firefox has started in Marionette mode with definable capabilities set to known values"); my $shadow_root; my $path = File::Spec->catfile(Cwd::cwd(), qw(t data elements.html)); if ($^O eq 'cygwin') { $path = $firefox->execute( 'cygpath', '-s', '-m', $path ); } $firefox->go("file://$path"); $firefox->find_class('add')->click(); my $span = $firefox->has_tag('span'); { my $count = 0; my $element = $firefox->script('return arguments[0].children[0]', args => [ $span ]); ok(ref $element eq 'Firefox::Marionette::Element' && $element->tag_name() eq 'button', "\$firefox->has_tag('span') has children and the first child is an Firefox::Marionette::Element with a tag_name of 'button'"); } my $custom_square; TODO: { local $TODO = $major_version < 63 ? "Firefox cannot create elements from a shadow root for versions less than 63" : undef; $custom_square = $firefox->has_tag('custom-square'); ok(ref $custom_square eq 'Firefox::Marionette::Element', "\$firefox->has_tag('custom-square') returns a Firefox::Marionette::Element"); if (ref $custom_square eq 'Firefox::Marionette::Element') { my $element = $firefox->script('return arguments[0].shadowRoot.children[0]', args => [ $custom_square ]); ok(!$span->shadowy(), "\$span->shadowy() returns false"); ok($custom_square->shadowy(), "\$custom_square->shadowy() returns true"); ok($element->tag_name() eq 'style', "First element from scripted shadowRoot is a style tag"); } } if ($major_version >= 96) { $shadow_root = $custom_square->shadow_root(); ok(ref $shadow_root eq 'Firefox::Marionette::ShadowRoot', "\$firefox->has_tag('custom-square')->shadow_root() returns a Firefox::Marionette::ShadowRoot"); my $count = 0; foreach my $element (@{$firefox->script('return arguments[0].children', args => [ $shadow_root ])}) { if ($count == 0) { ok($element->tag_name() eq 'style', "First element from ShadowRoot via script is a style tag"); } else { ok($element->tag_name() eq 'div', "Second element from ShadowRoot via script is a div tag"); } $count += 1; } ok($count == 2, "\$firefox->has_tag('custom-square')->shadow_root() has 2 children"); ok(ref $shadow_root eq 'Firefox::Marionette::ShadowRoot', "\$firefox->has_tag('custom-square')->shadow_root() returns a Firefox::Marionette::ShadowRoot"); { my $element = $firefox->script('return arguments[0].children[0]', args => [ $shadow_root ]); ok($element->tag_name() eq 'style', "Element returned from ShadowRoot via script is a style tag"); } $count = 0; foreach my $element (@{$firefox->script('return [ 2, arguments[0].children[0] ]', args => [ $shadow_root ])}) { if ($count == 0) { ok($element == 2, "First element is the numeric 2"); } else { ok($element->tag_name() eq 'style', "Second element from ShadowRoot via script is a style tag"); } $count += 1; } ok($count == 2, "\$firefox->script() correctly returns an array with 2 elements"); } { my $value = $firefox->script('return [2,1]', args => [ $span ]); ok($value->[0] == 2, "Value returned from script is the numeric 2 in an array"); } { my $value = $firefox->script('return [2,arguments[0]]', args => [ $span ]); ok(ref $value->[1] eq 'Firefox::Marionette::Element' && $value->[1]->tag_name() eq 'span', "Value returned from script is a Firefox::Mariontte::Element for a 'span' in an array"); } { my $value = $firefox->script('return arguments[0]', args => { elem => $span }); ok(ref $value->{elem} eq 'Firefox::Marionette::Element' && $value->{elem}->tag_name() eq 'span', "Value returned from script is a Firefox::Mariontte::Element for a 'span' in a hash"); } { my $value = $firefox->script('return 2', args => [ $span ]); ok($value == 2, "Value returned from script is the numeric 2"); } { my $hash = $firefox->script('return { value: 2 }', args => [ $span ]); ok($hash->{value} == 2, "Value returned from script is the numeric 2 in a hash"); } my $capabilities = $firefox->capabilities(); ok((ref $capabilities) eq 'Firefox::Marionette::Capabilities', "\$firefox->capabilities() returns a Firefox::Marionette::Capabilities object"); if (!grep /^accept_insecure_certs$/, $capabilities->enumerate()) { diag("\$capabilities->accept_insecure_certs is not supported for " . $capabilities->browser_version()); skip("\$capabilities->accept_insecure_certs is not supported for " . $capabilities->browser_version(), 4); } ok($capabilities->accept_insecure_certs(), "\$capabilities->accept_insecure_certs() is true"); ok($firefox->go(URI->new("https://untrusted-root.badssl.com/")), "https://untrusted-root.badssl.com/ has been loaded"); if (out_of_time()) { skip("Running out of time. Trying to shutdown tests as fast as possible", 2); } my $raw_pdf; eval { my $handle = $firefox->pdf(); ok(ref $handle eq 'File::Temp', "\$firefox->pdf() returns a File::Temp object:" . ref $handle); my $result; while($result = $handle->read(my $buffer, 4096)) { $raw_pdf .= $buffer; } defined $result or die "Failed to read from File::Temp handle:$!"; close $handle or die "Failed to close File::Temp handle:$!"; diag("WebDriver:Print command is supported for " . $capabilities->browser_version()); 1; } or do { chomp $@; diag("WebDriver:Print command is not supported for " . $capabilities->browser_version() . ":$@"); skip("WebDriver:Print command is not supported for " . $capabilities->browser_version() . ":$@", 2); }; ok($raw_pdf =~ /^%PDF\-\d+[.]\d+/smx, "PDF is produced in file handle for pdf method"); eval { require PDF::API2; } or do { diag("PDF::API2 is not available"); skip("PDF::API2 is not available", 2); }; diag("PDF::API2 tests are being run"); my $pdf = PDF::API2->open_scalar($raw_pdf); my $pages = $pdf->pages(); my $page = $pdf->openpage(0); my ($llx, $lly, $urx, $ury) = $page->mediabox(); ok($urx == 612 && $ury == 792, "Correct page height ($ury) and width ($urx)"); if ($ENV{RELEASE_TESTING}) { $raw_pdf = $firefox->pdf(raw => 1, printBackground => 1, landscape => 0, page => { width => 7, height => 12 }); $pdf = PDF::API2->open_scalar($raw_pdf); $page = $pdf->openpage(0); ($llx, $lly, $urx, $ury) = $page->mediabox(); $urx = int $urx; # for darwin $ury = int $ury; # for darwin ok(((centimetres_to_points(7) == $urx) || (centimetres_to_points(7) == $urx - 1)) && ((centimetres_to_points(12) == $ury) || (centimetres_to_points(12) == $ury - 1)), "Correct page height of " . centimetres_to_points(12) . " (was actually $ury) and width " . centimetres_to_points(7) . " (was actually $urx)"); $raw_pdf = $firefox->pdf(raw => 1, shrinkToFit => 1, pageRanges => [0], landscape => 1, page => { width => 7, height => 12 }); $pdf = PDF::API2->open_scalar($raw_pdf); $page = $pdf->openpage(0); ($llx, $lly, $urx, $ury) = $page->mediabox(); $urx = int $urx; # for darwin $ury = int $ury; # for darwin ok(((centimetres_to_points(12) == $urx) || (centimetres_to_points(12) == $urx - 1)) && ((centimetres_to_points(7) == $ury) || (centimetres_to_points(7) == $ury - 1)), "Correct page height of " . centimetres_to_points(7) . " (was actually $ury) and width " . centimetres_to_points(12) . " (was actually $urx)"); foreach my $paper_size ($firefox->paper_sizes()) { $raw_pdf = $firefox->pdf(raw => 1, size => $paper_size, page_ranges => [], print_background => 1, shrink_to_fit => 1); $pdf = PDF::API2->open_scalar($raw_pdf); $page = $pdf->openpage(0); ($llx, $lly, $urx, $ury) = $page->mediabox(); ok($raw_pdf =~ /^%PDF\-\d+[.]\d+/smx, "Raw PDF is produced for pdf method with size of $paper_size (width $urx points, height $ury points)"); } my %paper_sizes = ( 'A4' => { width => 21, height => 29.7 }, 'leTter' => { width => 21.6, height => 27.9 }, ); foreach my $paper_size (sort { $a cmp $b } keys %paper_sizes) { $raw_pdf = $firefox->pdf(raw => 1, size => $paper_size, margin => { top => 2, left => 2, right => 2, bottom => 2 }); ok($raw_pdf =~ /^%PDF\-\d+[.]\d+/smx, "Raw PDF is produced for pdf method"); $pdf = PDF::API2->open_scalar($raw_pdf); $pages = $pdf->pages(); $page = $pdf->openpage(0); ($llx, $lly, $urx, $ury) = $page->mediabox(); $urx = int $urx; # for darwin $ury = int $ury; # for darwin ok(((centimetres_to_points($paper_sizes{$paper_size}->{height}) == $ury) || (centimetres_to_points($paper_sizes{$paper_size}->{height}) + 1) == $ury) && ((centimetres_to_points($paper_sizes{$paper_size}->{width}) == $urx) || (centimetres_to_points($paper_sizes{$paper_size}->{width}) + 1) == $urx), "Correct page height ($ury) and width ($urx) for " . uc $paper_size); } my $result; eval { $firefox->pdf(size => 'UM'); $result = 1; } or do { $result = 0; chomp $@; }; ok($result == 0, "Correctly throws exception for unknown PDF page size:$@"); $result = undef; eval { $firefox->pdf(margin => { foo => 21 }); $result = 1; } or do { $result = 0; chomp $@; }; ok($result == 0, "Correctly throws exception for unknown margin key:$@"); $result = undef; eval { $firefox->pdf(page => { bar => 21 }); $result = 1; } or do { $result = 0; chomp $@; }; ok($result == 0, "Correctly throws exception for unknown page key:$@"); $result = undef; eval { $firefox->pdf(foo => 'bar'); $result = 1; } or do { $result = 0; chomp $@; }; ok($result == 0, "Correctly throws exception for unknown pdf key:$@"); } } sub centimetres_to_points { my ($centimetres) = @_; my $inches = $centimetres / 2.54; my $points = int $inches * 72; return $points; } SKIP: { diag("Starting new firefox for testing logins"); ($skip_message, $firefox) = start_firefox(0, capabilities => Firefox::Marionette::Capabilities->new(moz_headless => 1)); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 4); } if (!$tls_tests_ok) { skip("TLS test infrastructure seems compromised", 4); } ok($firefox, "Firefox has started in Marionette mode with definable capabilities set to known values"); my $capabilities = $firefox->capabilities(); ok((ref $capabilities) eq 'Firefox::Marionette::Capabilities', "\$firefox->capabilities() returns a Firefox::Marionette::Capabilities object"); if (out_of_time()) { skip("Running out of time. Trying to shutdown tests as fast as possible", 2); } if (grep /^accept_insecure_certs$/, $capabilities->enumerate()) { ok(!$capabilities->accept_insecure_certs(), "\$capabilities->accept_insecure_certs() is false"); eval { $firefox->go(URI->new("https://untrusted-root.badssl.com/")) }; my $exception = "$@"; chomp $exception; ok(ref $@ eq 'Firefox::Marionette::Exception::InsecureCertificate', "https://untrusted-root.badssl.com/ threw an exception:$exception"); } else { diag("\$capabilities->accept_insecure_certs is not supported for " . $capabilities->browser_version()); } if (out_of_time()) { skip("Running out of time. Trying to shutdown tests as fast as possible", 2); } my $profile_directory = $firefox->profile_directory(); ok($profile_directory, "\$firefox->profile_directory() returns $profile_directory"); my $possible_logins_path = File::Spec->catfile($profile_directory, 'logins.json'); ok(!-e $possible_logins_path, "There is no logins.json file yet"); eval { $firefox->fill_login() }; ok(ref $@ eq 'Firefox::Marionette::Exception', "Unable to fill in form when no form is present:$@"); my $cant_load_github; my $result; eval { $result = $firefox->go('https://github.com/login'); }; if ($@) { $cant_load_github = 1; diag("\$firefox->go('https://github.com/login') threw an exception:$@"); } else { ok($result, "\$firefox loads https://github.com/login"); } if (out_of_time()) { skip("Running out of time. Trying to shutdown tests as fast as possible", 2); } ok(scalar $firefox->logins() == 0, "\$firefox->logins() shows the correct number (0) of records"); my $now = time; my $current_year = (localtime($now))[6]; my $pause_login = Firefox::Marionette::Login->new(host => 'https://pause.perl.org', user => 'DDICK', password => 'qwerty', realm => 'PAUSE', user_fieldname => undef); ok($firefox->add_login($pause_login), "\$firefox->add_login() copes with a http auth login");; foreach my $login ($firefox->logins()) { ok($login->host() eq 'https://pause.perl.org', "\$login->host() eq 'https://pause.perl.org'"); ok($login->user() eq 'DDICK', "\$login->user() eq 'DDICK'"); ok($login->password() eq 'qwerty', "\$login->password() eq 'qwerty'"); ok($login->realm() eq 'PAUSE', "\$login->realm() eq 'PAUSE'"); ok(!defined $login->user_field(), "\$login->user_field() is undefined"); ok(!defined $login->password_field(), "\$login->password_field() is undefined"); ok(!defined $login->origin(), "\$login->origin() is undefined"); if ((defined $login->guid()) || ($major_version >= 59)) { ok($login->guid() =~ /^[{]$guid_regex[}]$/smx, "\$login->guid() is a UUID"); } if ((defined $login->creation_time()) || ($major_version >= 59)) { my $creation_year = (localtime($login->creation_time()))[6]; ok((($creation_year == $current_year) || ($creation_year == $current_year + 1)), "\$login->creation_time() returns a time with the correct year"); } if ((defined $login->last_used_time()) || ($major_version >= 59)) { my $last_used_year = (localtime($login->last_used_time()))[6]; ok((($last_used_year == $current_year) || ($last_used_year == $current_year + 1)), "\$login->last_used_time() returns a time with the correct year"); } if ((defined $login->password_changed_time()) || ($major_version >= 59)) { my $password_changed_year = (localtime($login->password_changed_time()))[6]; ok((($password_changed_year == $current_year) || ($password_changed_year == $current_year + 1)), "\$login->password_changed_time() returns a time with the correct year"); } if ((defined $login->times_used()) || ($major_version >= 59)) { ok($login->times_used() =~ /^\d+$/smx, "\$login->times_used() is a number"); } } ok(scalar $firefox->logins() == 1, "\$firefox->logins() shows the correct number (1) of records"); my $github_login = Firefox::Marionette::Login->new(host => 'https://github.com', user => 'ddick@cpan.org', password => 'qwerty', user_field => 'login', password_field => 'password'); ok($firefox->add_login($github_login), "\$firefox->add_login() copes with a form based login"); ok($firefox->delete_login($pause_login), "\$firefox->delete_login() removes the http auth login"); foreach my $login ($firefox->logins()) { ok($login->host() eq 'https://github.com', "\$login->host() eq 'https://github.com':" . $login->host()); ok($login->user() eq 'ddick@cpan.org', "\$login->user() eq 'ddick\@cpan.org':" . $login->user()); ok($login->password() eq 'qwerty', "\$login->password() eq 'qwerty':" . $login->password()); ok(!defined $login->realm(), "\$login->realm() is undefined"); ok($login->user_field() eq 'login', "\$login->user_field() eq 'login':" . $login->user_field()); ok($login->password_field() eq 'password', "\$login->password_field() eq 'password':" . $login->password_field()); ok(!defined $login->origin(), "\$login->origin() is not defined"); if ((defined $login->guid()) || ($major_version >= 59)) { ok($login->guid() =~ /^[{]$guid_regex[}]$/smx, "\$login->guid() is a UUID"); } if ((defined $login->creation_time()) || ($major_version >= 59)) { my $creation_year = (localtime($login->creation_time()))[6]; ok((($creation_year == $current_year) || ($creation_year == $current_year + 1)), "\$login->creation_time() returns a time with the correct year"); } if ((defined $login->last_used_time()) || ($major_version >= 59)) { my $last_used_year = (localtime($login->last_used_time()))[6]; ok((($last_used_year == $current_year) || ($last_used_year == $current_year + 1)), "\$login->last_used_time() returns a time with the correct year"); } if ((defined $login->password_changed_time()) || ($major_version >= 59)) { my $password_changed_year = (localtime($login->password_changed_time()))[6]; ok((($password_changed_year == $current_year) || ($password_changed_year == $current_year + 1)), "\$login->password_changed_time() returns a time with the correct year"); } if ((defined $login->times_used()) || ($major_version >= 59)) { ok($login->times_used() =~ /^\d+$/smx, "\$login->times_used() is a number"); } } my $perlmonks_login = Firefox::Marionette::Login->new(host => 'https://www.perlmonks.org', origin => 'https://www.perlmonks.org', user => 'ddick', password => 'qwerty', user_field => 'user', password_field => 'passwd', creation_time => $now - 20, last_used_time => $now - 10, password_changed_time => $now, password_changed_in_ms => $now * 1000 - 15, times_used => 50); ok($firefox->add_login($perlmonks_login), "\$firefox->add_login() copes with another form based login"); ok($firefox->delete_login($github_login), "\$firefox->delete_login() removes the original form based login"); foreach my $login ($firefox->logins()) { ok($login->host() eq 'https://www.perlmonks.org', "\$login->host() eq 'https://www.perlmonks.org':" . $login->host()); ok($login->user() eq 'ddick', "\$login->user() eq 'ddick':" . $login->user()); ok($login->password() eq 'qwerty', "\$login->password() eq 'qwerty':" . $login->password()); ok(!defined $login->realm(), "\$login->realm() is undefined"); ok($login->user_field() eq 'user', "\$login->user_field() eq 'user':" . $login->user_field()); ok($login->password_field() eq 'passwd', "\$login->password_field() eq 'passwd':" . $login->password_field()); ok($login->origin() eq 'https://www.perlmonks.org', "\$login->origin() eq 'https://www.perlmonks.org':" . $login->host()); if ((defined $login->guid()) || ($major_version >= 59)) { ok($login->guid() =~ /^[{]$guid_regex[}]$/smx, "\$login->guid() is a UUID"); } if ((defined $login->creation_time()) || ($major_version >= 59)) { ok($login->creation_time() == $now - 20, "\$login->last_used_time() returns the assigned time:" . localtime $login->creation_time()); } if ((defined $login->last_used_time()) || ($major_version >= 59)) { ok($login->last_used_time() == $now - 10, "\$login->last_used_time() returns the assigned time:" . localtime $login->last_used_time()); } if ((defined $login->password_changed_in_ms()) || ($major_version >= 59)) { my $password_changed_year = (localtime($login->password_changed_time()))[6]; ok($password_changed_year == $current_year, "\$login->password_changed_time() returns a time with the correct year"); ok($login->password_changed_in_ms() == $now * 1000 - 15, "\$login->password_changed_time_in_ms() returns the correct number of milliseconds"); } if ((defined $login->times_used()) || ($major_version >= 59)) { ok($login->times_used() == 50, "\$login->times_used() is the assigned number"); } } ok($firefox->add_login($github_login), "\$firefox->add_login() copes re-adding the original form based login"); ok(!$firefox->pwd_mgr_needs_login(), "\$firefox->pwd_mgr_needs_login() returns false"); my @charset = ( 'A' .. 'Z', 'a' .. 'z', 0..9 ); my $lock_password; for(1 .. 50) { $lock_password .= $charset[rand scalar @charset]; } eval { $firefox->pwd_mgr_lock(); }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->pwd_mgr_lock() throws an exception when no password is supplied:" . ref $@); ok($firefox->pwd_mgr_lock($lock_password), "\$firefox->pwd_mgr_lock() sets the primary password"); ok($firefox->pwd_mgr_logout(), "\$firefox->pwd_mgr_logout() logs out"); ok($firefox->pwd_mgr_needs_login(), "\$firefox->pwd_mgr_needs_login() returns true"); my $wrong_password = substr $lock_password, 0, 10; eval { $firefox->pwd_mgr_login($wrong_password); }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->pwd_mgr_login() throws an exception when the wrong password is supplied:" . ref $@); eval { $firefox->pwd_mgr_login(); }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->pwd_mgr_login() throws an exception when no password is supplied:" . ref $@); ok($firefox->pwd_mgr_login($lock_password), "\$firefox->pwd_mgr_login() logs in"); ok(!$firefox->pwd_mgr_needs_login(), "\$firefox->pwd_mgr_needs_login() returns false"); ok($firefox->add_login($pause_login), "\$firefox->add_login() copes with a http auth login");; if (!$cant_load_github) { ok($firefox->fill_login(), "\$firefox->fill_login() works correctly"); } ok($firefox->delete_login($github_login), "\$firefox->delete_login() removes the original form based login"); ok($firefox->add_login(host => 'https://github.com', user => 'ddick@cpan.org', password => 'qwerty', user_field => 'login', password_field => 'password', origin => 'https://github.com'), "\$firefox->add_login() copes with a driectly specified form based login"); if (!$cant_load_github) { ok($firefox->fill_login(), "\$firefox->fill_login() works correctly"); } ok(scalar $firefox->logins() == 3, "\$firefox->logins() shows the correct number (3) of records"); ok($firefox->delete_logins(), "\$firefox->delete_logins() works"); ok(scalar $firefox->logins() == 0, "\$firefox->logins() shows the correct number (0) of records"); ok($firefox->add_login(host => 'https://github.com', user => 'ddick@cpan.org', password => 'qwerty', user_field => 'login', password_field => 'password', origin => 'https://example.com'), "\$firefox->add_login() copes with a driectly specified form based login with an incorrect origin"); eval { $firefox->fill_login(); }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->fill_logins() throws an exception when it fails to fill the form b/c of the wrong origin:" . ref $@); ok($firefox->delete_logins(), "\$firefox->delete_logins() works"); my $github_login_with_wrong_user_field = Firefox::Marionette::Login->new(host => 'https://github.com', user => 'ddick@cpan.org', password => 'qwerty', user_field => 'nopewrong', password_field => 'password'); ok($firefox->add_login($github_login_with_wrong_user_field), "\$firefox->add_login() copes with a form based login with the incorrect user_field"); eval { $firefox->fill_login(); }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->fill_logins() throws an exception when it fails to fill the form b/c of the wrong user_field:" . ref $@); ok($firefox->delete_login($github_login_with_wrong_user_field), "\$firefox->delete_login() removes the form based login with the incorrect user_field"); my $github_login_with_wrong_password_field = Firefox::Marionette::Login->new(host => 'https://github.com', user => 'ddick@cpan.org', password => 'qwerty', user_field => 'login', password_field => 'defintelyincorrect'); ok($firefox->add_login($github_login_with_wrong_password_field), "\$firefox->add_login() copes with a form based login with the incorrect password_field"); eval { $firefox->fill_login(); }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->fill_logins() throws an exception when it fails to fill the form b/c of the wrong password_field:" . ref $@); ok($firefox->delete_login($github_login_with_wrong_password_field), "\$firefox->delete_login() removes the form based login with the incorrect user_field"); ok(scalar $firefox->logins() == 0, "\$firefox->logins() shows the correct number (0) of records"); ok($firefox->add_login(host => 'https://www.perlmonks.org', origin => 'https://www.perlmonks.org', user => 'ddick', password => 'qwerty', user_field => 'user', password_field => 'passwd', creation_time => $now - 20, last_used_time => $now - 10, password_changed_time => $now, password_changed_in_ms => $now * 1000 - 15, times_used => 50), "\$firefox->add_login() copes with a form based login passed directly to it"); foreach my $login ($firefox->logins()) { ok($login->host() eq 'https://www.perlmonks.org', "\$login->host() eq 'https://www.perlmonks.org':" . $login->host()); ok($login->user() eq 'ddick', "\$login->user() eq 'ddick':" . $login->user()); ok($login->password() eq 'qwerty', "\$login->password() eq 'qwerty':" . $login->password()); ok(!defined $login->realm(), "\$login->realm() is undefined"); ok($login->user_field() eq 'user', "\$login->user_field() eq 'user':" . $login->user_field()); ok($login->password_field() eq 'passwd', "\$login->password_field() eq 'passwd':" . $login->password_field()); ok($login->origin() eq 'https://www.perlmonks.org', "\$login->origin() eq 'https://www.perlmonks.org':" . $login->host()); if ((defined $login->guid()) || ($major_version >= 59)) { ok($login->guid() =~ /^[{]$guid_regex[}]$/smx, "\$login->guid() is a UUID"); } if ((defined $login->creation_time()) || ($major_version >= 59)) { ok($login->creation_time() == $now - 20, "\$login->last_used_time() returns the assigned time:" . localtime $login->creation_time()); } if ((defined $login->last_used_time()) || ($major_version >= 59)) { ok($login->last_used_time() == $now - 10, "\$login->last_used_time() returns the assigned time:" . localtime $login->last_used_time()); } if ((defined $login->password_changed_in_ms()) || ($major_version >= 59)) { my $password_changed_year = (localtime($login->password_changed_time()))[6]; ok($password_changed_year == $current_year, "\$login->password_changed_time() returns a time with the correct year"); ok($login->password_changed_in_ms() == $now * 1000 - 15, "\$login->password_changed_time_in_ms() returns the correct number of milliseconds"); } if ((defined $login->times_used()) || ($major_version >= 59)) { ok($login->times_used() == 50, "\$login->times_used() is the assigned number"); } ok($firefox->delete_login($login), "\$firefox->delete_login() removes the form based login passed directly"); } ok(scalar $firefox->logins() == 0, "\$firefox->logins() shows the correct number (0) of records"); foreach my $path (qw(t/data/1Passwordv7.csv t/data/bitwarden_export_org.csv t/data/keepass.csv t/data/last_pass_example.csv)) { my $handle = FileHandle->new($path, Fcntl::O_RDONLY()) or die "Failed to open $path:$!"; my @logins; foreach my $login (Firefox::Marionette->logins_from_csv($handle)) { ok($login->host() =~ /^https?:\/\/(?:[a-z]+[.])?[a-z]+[.](?:com|net|org)$/smx, "Firefox::Marionette::Login->host() from Firefox::Marionette->logins_from_csv('$path') looks correct:" . Encode::encode('UTF-8', $login->host(), 1)); ok($login->user(), "Firefox::Marionette::Login->user() from Firefox::Marionette->logins_from_csv('$path') looks correct:" . Encode::encode('UTF-8', $login->user(), 1)); ok($firefox->add_login($login), "\$firefox->add_login() copes with a login from Firefox::Marionette->logins_from_csv('$path') passed directly to it"); push @logins, $login; } ok(scalar @logins, "$path produces Firefox::Marionette::Login records:" . scalar @logins); my %existing; foreach my $login ($firefox->logins()) { $existing{$login->host()}{$login->user()} = $login; } $handle = FileHandle->new($path, Fcntl::O_RDONLY()) or die "Failed to open $path:$!"; foreach my $login (Firefox::Marionette->logins_from_csv($handle)) { ok(exists $existing{$login->host()}{$login->user()} && $existing{$login->host()}{$login->user()}->password() eq $login->password(), "\$firefox->logins() produces a matching login after adding record from Firefox::Marionette->logins_from_csv('$path')"); ok($firefox->delete_login($login), "\$firefox->delete_login() copes with a login from Firefox::Marionette->logins_from_csv('$path') passed directly to it"); } } ok(scalar $firefox->logins() == 0, "\$firefox->logins() shows the correct number (0) of records"); foreach my $path (qw(t/data/1Passwordv8.1pux)) { my $handle = FileHandle->new($path, Fcntl::O_RDONLY()) or die "Failed to open $path:$!"; my @logins; foreach my $login (Firefox::Marionette->logins_from_zip($handle)) { ok($login->host() =~ /^https?:\/\/(?:[a-z]+[.])?[a-z]+[.](?:com|net|org)$/smx, "Firefox::Marionette::Login->host() from Firefox::Marionette->logins_from_zip('$path') looks correct:" . Encode::encode('UTF-8', $login->host(), 1)); ok($login->user(), "Firefox::Marionette::Login->user() from Firefox::Marionette->logins_from_zip('$path') looks correct:" . Encode::encode('UTF-8', $login->user(), 1)); ok($firefox->add_login($login), "\$firefox->add_login() copes with a login from Firefox::Marionette->logins_from_zip('$path') passed directly to it"); push @logins, $login; } ok(scalar @logins, "$path produces Firefox::Marionette::Login records:" . scalar @logins); my %existing; foreach my $login ($firefox->logins()) { $existing{$login->host()}{$login->user()} = $login; } $handle = FileHandle->new($path, Fcntl::O_RDONLY()) or die "Failed to open $path:$!"; foreach my $login (Firefox::Marionette->logins_from_zip($handle)) { ok(exists $existing{$login->host()}{$login->user()} && $existing{$login->host()}{$login->user()}->password() eq $login->password(), "\$firefox->logins() produces a matching login after adding record from Firefox::Marionette->logins_from_zip('$path')"); ok($firefox->delete_login($login), "\$firefox->delete_login() copes with a login from Firefox::Marionette->logins_from_zip('$path') passed directly to it"); } } ok(scalar $firefox->logins() == 0, "\$firefox->logins() shows the correct number (0) of records"); ok($firefox->quit() == $correct_exit_status, "Firefox has closed with an exit status of $correct_exit_status:" . $firefox->child_error()); } SKIP: { diag("Starting new firefox for testing custom headers"); ($skip_message, $firefox) = start_firefox(0, har => 1, debug => 0, capabilities => Firefox::Marionette::Capabilities->new(moz_headless => 1)); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 6); } if (!$tls_tests_ok) { skip("TLS test infrastructure seems compromised", 6); } ok($firefox, "Firefox has started in Marionette mode with definable capabilities set to known values"); ok(scalar $firefox->logins() == 0, "\$firefox->logins() has no entries:" . scalar $firefox->logins()); my $testing_header_name = 'X-CPAN-Testing'; my $testing_header_value = (ref $firefox) . q[ All ] . $Firefox::Marionette::VERSION; $firefox->add_header($testing_header_name => $testing_header_value); my $testing_header_2_name = 'X-CPAN-Testing2'; my $testing_header_2_value = (ref $firefox) . q[ All2 ] . $Firefox::Marionette::VERSION; $firefox->delete_header($testing_header_2_name)->add_header($testing_header_2_name => $testing_header_2_value); my $testing_site_header_name = 'X-CPAN-Site-Testing'; my $testing_site_header_value = (ref $firefox) . q[ Site ] . $Firefox::Marionette::VERSION; my $site_hostname = 'fastapi.metacpan.org'; $firefox->add_site_header($site_hostname, $testing_site_header_name => $testing_site_header_value); my $testing_site_header_2_name = 'X-CPAN-Site-Testing2'; my $testing_site_header_2_value = (ref $firefox) . q[ Site2 ] . $Firefox::Marionette::VERSION; $firefox->delete_site_header($site_hostname, $testing_site_header_2_name)->add_site_header($site_hostname, $testing_site_header_2_name => $testing_site_header_2_value); my $testing_no_site_header_name = 'X-CPAN-No-Site-Testing'; my $testing_no_site_header_value = (ref $firefox) . q[ None ] . $Firefox::Marionette::VERSION; my $no_site_hostname = 'missing.metacpan.org'; $firefox->add_site_header($no_site_hostname, $testing_no_site_header_name => $testing_no_site_header_value); $firefox->delete_header('Accept-Language'); $firefox->delete_site_header('fastapi.metacpan.org', 'Cache-Control'); my $capabilities = $firefox->capabilities(); ok((ref $capabilities) eq 'Firefox::Marionette::Capabilities', "\$firefox->capabilities() returns a Firefox::Marionette::Capabilities object"); if (!grep /^accept_insecure_certs$/, $capabilities->enumerate()) { diag("\$capabilities->accept_insecure_certs is not supported for " . $capabilities->browser_version()); skip("\$capabilities->accept_insecure_certs is not supported for " . $capabilities->browser_version(), 3); } ok(!$capabilities->accept_insecure_certs(), "\$capabilities->accept_insecure_certs() is false"); if ($ENV{RELEASE_TESTING}) { # har sometimes hangs and sometimes metacpan.org fails certificate checks. for example. http://www.cpantesters.org/cpan/report/e71bfb3b-7413-1014-98e6-045206f7812f ok($firefox->go(URI->new("https://fastapi.metacpan.org/author/DDICK")), "https://fastapi.metacpan.org/author/DDICK has been loaded"); ok($firefox->interactive() && $firefox->loaded(), "\$firefox->interactive() and \$firefox->loaded() are ok"); if ($major_version < 61) { skip("HAR support not available in Firefox before version 61", 1); } my $correct = 0; my $number_of_entries = 0; my $count = 0; GET_HAR: while($number_of_entries == 0) { my $har = $firefox->har(); ok($har->{log}->{creator}->{name} eq ucfirst $firefox->capabilities()->browser_name(), "\$firefox->har() gives a data structure with the correct creator name"); $number_of_entries = 0; $correct = 0; foreach my $entry (@{$har->{log}->{entries}}) { $number_of_entries += 1; } if ($number_of_entries > 0) { foreach my $header (@{$har->{log}->{entries}->[0]->{request}->{headers}} ) { if (lc $header->{name} eq $testing_no_site_header_name) { diag("Should not have found an '$header->{name}' header"); $correct = -1; } elsif (lc $header->{name} eq 'accept-language') { diag("Should not have found an '$header->{name}' header"); $correct = -1; } elsif (lc $header->{name} eq 'cache-control') { diag("Should not have found an '$header->{name}' header"); $correct = -1; } elsif ((lc $header->{name} eq lc $testing_header_name) && ($header->{value} eq $testing_header_value)) { diag("Found an '$header->{name}' header"); if ($correct >= 0) { $correct += 1; } } elsif ((lc $header->{name} eq lc $testing_header_2_name) && ($header->{value} eq $testing_header_2_value)) { diag("Found an '$header->{name}' header"); if ($correct >= 0) { $correct += 1; } } elsif ((lc $header->{name} eq lc $testing_site_header_name) && ($header->{value} eq $testing_site_header_value)) { diag("Found an '$header->{name}' header"); if ($correct >= 0) { $correct += 1; } } elsif ((lc $header->{name} eq lc $testing_site_header_2_name) && ($header->{value} eq $testing_site_header_2_value)) { diag("Found an '$header->{name}' header"); if ($correct >= 0) { $correct += 1; } } } } sleep 1; $count += 1; if ($count > 20) { diag("Unable to find any HAR entries for 20 seconds"); last GET_HAR; } } if ($^O eq 'cygwin') { TODO: { local $TODO = "cygwin can fail this test"; ok($correct == 4, "Correct headers have been set"); } } else { ok($correct == 4, "Correct headers have been set"); } } } my $bad_network_behaviour; SKIP: { diag("Starting new firefox for testing metacpan and iframe, with find, downloads, extensions and actions"); ($skip_message, $firefox) = start_firefox(0, debug => 0, page_load => 600000, script => 5432, profile => $profile, capabilities => Firefox::Marionette::Capabilities->new(accept_insecure_certs => 1, page_load_strategy => 'eager')); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 247); } ok($firefox, "Firefox has started in Marionette mode without defined capabilities, but with a defined profile and debug turned off"); my $path = File::Spec->catfile(Cwd::cwd(), qw(t data elements.html)); if ($^O eq 'cygwin') { $path = $firefox->execute( 'cygpath', '-s', '-m', $path ); } my $frame_url = "file://$path"; my $frame_element = '//iframe[@name="iframe"]'; ok($firefox->go($frame_url), "$frame_url has been loaded"); if (out_of_time()) { skip("Running out of time. Trying to shutdown tests as fast as possible", 246); } my $first_window_handle = $firefox->window_handle(); if ($major_version < 90) { ok($first_window_handle =~ /^\d+$/, "\$firefox->window_handle() is an integer:" . $first_window_handle); } else { ok($first_window_handle =~ /^$guid_regex$/smx, "\$firefox->window_handle() is a GUID:" . $first_window_handle); } my $chrome_window_handle_supported; eval { $chrome_window_handle_supported = $firefox->chrome_window_handle(); } or do { diag("\$firefox->chrome_window_handle is not supported for $major_version.$minor_version.$patch_version:$@"); }; SKIP: { if (!$chrome_window_handle_supported) { diag("\$firefox->chrome_window_handle is not supported for $major_version.$minor_version.$patch_version"); skip("\$firefox->chrome_window_handle is not supported for $major_version.$minor_version.$patch_version", 1); } if ($major_version < 90) { ok($chrome_window_handle_supported =~ /^\d+$/, "\$firefox->chrome_window_handle() is an integer:" . $chrome_window_handle_supported); } else { ok($chrome_window_handle_supported =~ /^$guid_regex$/smx, "\$firefox->chrome_window_handle() is a GUID:" . $chrome_window_handle_supported); } } ok($firefox->capabilities()->timeouts()->script() == 5432, "\$firefox->capabilities()->timeouts()->script() correctly reflects the scripts shortcut timeout:" . $firefox->capabilities()->timeouts()->script()); SKIP: { if (!$chrome_window_handle_supported) { diag("\$firefox->chrome_window_handle is not supported for $major_version.$minor_version.$patch_version"); skip("\$firefox->chrome_window_handle is not supported for $major_version.$minor_version.$patch_version", 2); } if ($major_version < 90) { ok($firefox->chrome_window_handle() == $firefox->current_chrome_window_handle(), "\$firefox->chrome_window_handle() is equal to \$firefox->current_chrome_window_handle()"); } else { ok($firefox->chrome_window_handle() eq $firefox->current_chrome_window_handle(), "\$firefox->chrome_window_handle() is equal to \$firefox->current_chrome_window_handle()"); } ok(scalar $firefox->chrome_window_handles() == 1, "There is one window/tab open at the moment"); } ok(scalar $firefox->window_handles() == 1, "There is one actual window open at the moment"); my $original_chrome_window_handle; SKIP: { if (!$chrome_window_handle_supported) { diag("\$firefox->chrome_window_handle is not supported for $major_version.$minor_version.$patch_version"); skip("\$firefox->chrome_window_handle is not supported for $major_version.$minor_version.$patch_version", 1); } ($original_chrome_window_handle) = $firefox->chrome_window_handles(); foreach my $handle ($firefox->chrome_window_handles()) { if ($major_version < 90) { ok($handle =~ /^\d+$/, "\$firefox->chrome_window_handles() returns a list of integers:" . $handle); } else { ok($handle =~ /^$guid_regex$/, "\$firefox->chrome_window_handles() returns a list of GUIDs:" . $handle); } } } my ($original_window_handle) = $firefox->window_handles(); foreach my $handle ($firefox->window_handles()) { if ($major_version < 90) { ok($handle =~ /^\d+$/, "\$firefox->window_handles() returns a list of integers:" . $handle); } else { ok($handle =~ /^$guid_regex$/, "\$firefox->window_handles() returns a list of integers:" . $handle); } } ok(not($firefox->script('window.open("https://duckduckgo.com", "_blank");')), "Opening new window to duckduckgo.com via 'window.open' script"); ok(scalar $firefox->window_handles() == 2, "There are two actual windows open at the moment"); my $new_chrome_window_handle; SKIP: { if (!$chrome_window_handle_supported) { diag("\$firefox->chrome_window_handle is not supported for $major_version.$minor_version.$patch_version"); skip("\$firefox->chrome_window_handle is not supported for $major_version.$minor_version.$patch_version", 4); } ok(scalar $firefox->chrome_window_handles() == 2, "There are two windows/tabs open at the moment"); foreach my $handle ($firefox->chrome_window_handles()) { if ($major_version < 90) { ok($handle =~ /^\d+$/, "\$firefox->chrome_window_handles() returns a list of integers:" . $handle); } else { ok($handle =~ /^$guid_regex$/, "\$firefox->chrome_window_handles() returns a list of integers:" . $handle); } if ($handle ne $original_chrome_window_handle) { $new_chrome_window_handle = $handle; } } ok($new_chrome_window_handle, "New chrome window handle $new_chrome_window_handle detected"); } my $new_window_handle; foreach my $handle ($firefox->window_handles()) { if ($major_version < 90) { ok($handle =~ /^\d+$/, "\$firefox->window_handles() returns a list of integers:" . $handle); } else { ok($handle =~ /^$guid_regex$/, "\$firefox->window_handles() returns a list of integers:" . $handle); } if ($handle ne $original_window_handle) { $new_window_handle = $handle; } } ok($new_window_handle, "New window handle $new_window_handle detected"); TODO: { my $screen_orientation = q[]; eval { $screen_orientation = $firefox->screen_orientation(); ok($screen_orientation, "\$firefox->screen_orientation() is " . $screen_orientation); } or do { if (($@->isa('Firefox::Marionette::Exception')) && ($@ =~ /(?:Only supported in Fennec|unsupported operation: Only supported on Android)/)) { local $TODO = "Only supported in Fennec"; ok($screen_orientation, "\$firefox->screen_orientation() is " . $screen_orientation); } elsif ($major_version < 60) { my $exception = "$@"; chomp $exception; diag("\$firefox->screen_orientation() is unavailable in " . $firefox->browser_version() . ":$exception"); local $TODO = "\$firefox->screen_orientation() is unavailable in " . $firefox->browser_version() . ":$exception"; ok($screen_orientation, "\$firefox->screen_orientation() is " . $screen_orientation); } else { ok($screen_orientation, "\$firefox->screen_orientation() is " . $screen_orientation); } }; } ok($firefox->switch_to_window($original_window_handle), "\$firefox->switch_to_window() used to move back to the original window:$@"); TODO: { my $element; eval { $element = $firefox->find($frame_element)->switch_to_shadow_root(); }; if ($@) { chomp $@; diag("Switch to shadow root is broken:$@"); } local $TODO = "Switch to shadow root can be broken"; ok($element, "Switched to $frame_element shadow root"); } SKIP: { my $switch_to_frame; eval { $switch_to_frame = $firefox->list($frame_element)->switch_to_frame() }; if ((!$switch_to_frame) && (($major_version < 50) || ($major_version > 80))) { chomp $@; diag("switch_to_frame is not supported for $major_version.$minor_version.$patch_version:$@"); skip("switch_to_frame is not supported for $major_version.$minor_version.$patch_version", 1); } ok($switch_to_frame, "Switched to $frame_element frame"); } SKIP: { my $active_frame; eval { $active_frame = $firefox->active_frame() }; if ((!$active_frame) && (($major_version < 50) || ($major_version > 80))) { chomp $@; diag("\$firefox->active_frame is not supported for $major_version.$minor_version.$patch_version:$@"); skip("\$firefox->active_frame is not supported for $major_version.$minor_version.$patch_version:$@", 1); } ok($active_frame->isa('Firefox::Marionette::Element'), "\$firefox->active_frame() returns a Firefox::Marionette::Element object"); } SKIP: { my $switch_to_parent_frame; eval { $switch_to_parent_frame = $firefox->switch_to_parent_frame(); }; if ((!$switch_to_parent_frame) && ($major_version < 50)) { chomp $@; diag("\$firefox->switch_to_parent_frame is not supported for $major_version.$minor_version.$patch_version:$@"); skip("\$firefox->switch_to_parent_frame is not supported for $major_version.$minor_version.$patch_version", 1); } ok($switch_to_parent_frame, "Switched to parent frame"); } SKIP: { if (!$chrome_window_handle_supported) { diag("\$firefox->chrome_window_handle is not supported for $major_version.$minor_version.$patch_version"); skip("\$firefox->chrome_window_handle is not supported for $major_version.$minor_version.$patch_version", 1); } foreach my $handle ($firefox->close_current_chrome_window_handle()) { local $TODO = $major_version < 52 ? "\$firefox->close_current_chrome_window_handle() can return a undef value for versions less than 52" : undef; if ($major_version < 90) { ok(defined $handle && $handle == $new_chrome_window_handle, "Closed original window, which means the remaining chrome window handle should be $new_chrome_window_handle:" . ($handle || '')); } else { ok(defined $handle && $handle eq $new_chrome_window_handle, "Closed original window, which means the remaining chrome window handle should be $new_chrome_window_handle:" . ($handle || '')); } } } ok($firefox->switch_to_window($new_window_handle), "\$firefox->switch_to_window() used to move back to the original window"); my $metacpan_uri = 'https://metacpan.org/'; ok($firefox->go($metacpan_uri), "$metacpan_uri has been loaded in the new window"); if (out_of_time()) { skip("Running out of time. Trying to shutdown tests as fast as possible", 224); } my $uri = $firefox->uri(); ok($uri =~ /metacpan/smx, "\$firefox->uri() contains /metacpan/:$uri"); if ($uri ne $metacpan_uri) { if (my $proxy = $firefox->capabilities()->proxy()) { diag("Proxy type is " . $firefox->capabilities()->proxy()->type()); if ($firefox->capabilities()->proxy()->pac()) { diag("Proxy pac is " . $firefox->capabilities()->proxy()->pac()); } if ($firefox->capabilities()->proxy()->https()) { diag("Proxy for https is " . $firefox->capabilities()->proxy()->https()); } if ($firefox->capabilities()->proxy()->socks()) { diag("Proxy for socks is " . $firefox->capabilities()->proxy()->socks()); } } else { diag("\$firefox->capabilities()->proxy() is not supported for " . $firefox->capabilities()->browser_version()); } $bad_network_behaviour = 1; diag("Skipping metacpan tests as loading $metacpan_uri sent firefox to $uri"); skip("Skipping metacpan tests as loading $metacpan_uri sent firefox to $uri", 223); } ok($firefox->title() =~ /Search/, "metacpan.org has a title containing Search"); my $context; eval { $context = $firefox->context(); }; SKIP: { if ((!$context) && ($major_version < 50)) { chomp $@; diag("\$firefox->context is not supported for $major_version.$minor_version.$patch_version:$@"); skip("\$firefox->context is not supported for $major_version.$minor_version.$patch_version", 2); } ok($firefox->context('chrome') eq 'content', "Initial context of the browser is 'content'"); ok($firefox->context('content') eq 'chrome', "Changed context of the browser is 'chrome'"); } ok($firefox->page_source() =~ /lucky/smx, "metacpan.org contains the phrase 'lucky' in page source"); ok($firefox->html() =~ /lucky/smx, "metacpan.org contains the phrase 'lucky' in html"); ok($firefox->refresh(), "\$firefox->refresh()"); my $element = $firefox->active_element(); ok($element, "\$firefox->active_element() returns an element"); TODO: { local $TODO = $major_version < 50 ? "\$firefox->active_frame() is not working for $major_version.$minor_version.$patch_version" : undef; my $active_frame; eval { $active_frame = $firefox->active_frame() }; if (($@) && ($major_version < 50)) { diag("\$firefox->active_frame is not supported for $major_version.$minor_version.$patch_version:$@"); } ok(not(defined $active_frame), "\$firefox->active_frame() is undefined for " . $firefox->uri()); } my @links = $firefox->links(); ok(scalar @links, "Found " . (scalar @links) . " links in metacpan.org"); foreach my $link (@links) { if (defined $link->url()) { ok($link->url(), "Link from metacpan.org has a url of " . $link->url()); } if (my $text = $link->text()) { ok($link->text(), "Link from metacpan.org has text of " . $text); } if ($link->name()) { ok($link->name(), "Link from metacpan.org has name of " . $link->name()); } if (defined $link->tag()) { ok($link->tag(), "Link from metacpan.org has a tag of " . $link->tag()); } if (defined $link->base()) { ok($link->base(), "Link from metacpan.org has a base of " . $link->base()); } if ($link->URI()) { ok($link->URI() && $link->URI()->isa('URI::URL'), "Link from metacpan.org has a URI of " . $link->URI()); } if ($link->url_abs()) { ok($link->url_abs(), "Link from metacpan.org has a url_abs of " . $link->url_abs()); } my %attributes = $link->attrs(); my $count = 0; foreach my $key (sort { $a cmp $b } keys %attributes) { ok($key, "Link from metacpan.org has a attribute called '" . $key . "' with a value of '" . $attributes{$key} . "'"); $count += 1; } ok($count, "Link from metacpan.org has $count attributes"); } my @images = $firefox->images(); foreach my $image (@images) { ok($image->url(), "Image from metacpan.org has a url of " . $image->url()); ok($image->height(), "Image from metacpan.org has height of " . $image->height()); ok($image->width(), "Image from metacpan.org has width of " . $image->width()); if ($image->alt()) { ok($image->alt(), "Image from metacpan.org has alt of " . $image->alt()); } if ($image->name()) { ok($image->name(), "Image from metacpan.org has name of " . $image->name()); } if (defined $image->tag()) { ok($image->tag() =~ /^(image|input)$/smx, "Image from metacpan.org has a tag of " . $image->tag()); } if (defined $image->base()) { ok($image->base(), "Image from metacpan.org has a base of " . $image->base()); } if ($image->URI()) { ok($image->URI() && $image->URI()->isa('URI::URL'), "Image from metacpan.org has a URI of " . $image->URI()); } if ($image->url_abs()) { ok($image->url_abs(), "Image from metacpan.org has a url_abs of " . $image->url_abs()); } my %attributes = $image->attrs(); my $count = 0; foreach my $key (sort { $a cmp $b } keys %attributes) { ok($key, "Image from metacpan.org has a attribute called '" . $key . "' with a value of '" . $attributes{$key} . "'"); $count += 1; } ok($count, "Image from metacpan.org has $count attributes"); } my $search_box_id; foreach my $element ($firefox->has_tag('input')) { if ((lc $element->attribute('type')) eq 'text') { $search_box_id = $element->attribute('id'); } } ok($firefox->find('//input[@id="' . $search_box_id . '"]', BY_XPATH())->type('Test::More'), "Sent 'Test::More' to the '$search_box_id' field directly to the element"); my $autofocus; ok($autofocus = $firefox->find_element('//input[@id="' . $search_box_id . '"]')->attribute('autofocus'), "The value of the autofocus attribute is '$autofocus'"); $autofocus = undef; eval { $autofocus = $firefox->find('//input[@id="' . $search_box_id . '"]')->property('autofocus'); }; SKIP: { if ((!$autofocus) && ($major_version < 50)) { chomp $@; diag("The property method is not supported for $major_version.$minor_version.$patch_version:$@"); skip("The property method is not supported for $major_version.$minor_version.$patch_version", 4); } ok($autofocus, "The value of the autofocus property is '$autofocus'"); ok($firefox->find_by_class('main-content')->find('//input[@id="' . $search_box_id . '"]')->property('id') eq $search_box_id, "Correctly found nested element with find"); ok($firefox->title() eq $firefox->find_tag('title')->property('innerHTML'), "\$firefox->title() is the same as \$firefox->find_tag('title')->property('innerHTML')"); } my $count = 0; foreach my $element ($firefox->find_by_class('main-content')->list('//input[@id="' . $search_box_id . '"]')) { ok($element->attribute('id') eq $search_box_id, "Correctly found nested element with list"); $count += 1; } ok($count == 1, "Found elements with nested list:$count"); $count = 0; foreach my $element ($firefox->find_by_class('main-content')->find('//input[@id="' . $search_box_id . '"]')) { ok($element->attribute('id') eq $search_box_id, "Correctly found nested element with find"); $count += 1; } ok($count == 1, "Found elements with nested find:$count"); $count = 0; foreach my $element ($firefox->has_class('main-content')->has('//input[@id="' . $search_box_id . '"]')) { ok($element->attribute('id') eq $search_box_id, "Correctly found nested element with has"); $count += 1; } $count = 0; foreach my $element ($firefox->has_class('main-content')->has('//input[@id="not-an-element-at-all-or-ever"]')) { $count += 1; } ok($count == 0, "Found no elements with nested has:$count"); $count = 0; foreach my $element ($firefox->find('//input[@id="' . $search_box_id . '"]')) { ok($element->attribute('id') eq $search_box_id, "Correctly found element with wantarray find"); $count += 1; } ok($count == 1, "Found elements with wantarray find:$count"); ok($firefox->find($search_box_id, 'id')->attribute('id') eq $search_box_id, "Correctly found element when searching by id"); ok($firefox->find($search_box_id, BY_ID())->attribute('id') eq $search_box_id, "Correctly found element when searching by id"); ok($firefox->has($search_box_id, BY_ID())->attribute('id') eq $search_box_id, "Correctly found element for default has"); ok($firefox->list_by_id($search_box_id)->attribute('id') eq $search_box_id, "Correctly found element with list_by_id"); ok($firefox->find_by_id($search_box_id)->attribute('id') eq $search_box_id, "Correctly found element with find_by_id"); ok($firefox->find_by_class('main-content')->find_by_id($search_box_id)->attribute('id') eq $search_box_id, "Correctly found nested element with find_by_id"); ok($firefox->find_id($search_box_id)->attribute('id') eq $search_box_id, "Correctly found element with find_id"); ok($firefox->has_id($search_box_id)->attribute('id') eq $search_box_id, "Correctly found element with has_id"); ok(!defined $firefox->has_id('search-input-totally-not-there-EVER'), "Correctly returned undef with has_id for a non existant element"); ok($firefox->find_class('main-content')->find_id($search_box_id)->attribute('id') eq $search_box_id, "Correctly found nested element with find_id"); ok($firefox->has_class('main-content')->has_id($search_box_id)->attribute('id') eq $search_box_id, "Correctly found nested element with has_id"); $count = 0; foreach my $element ($firefox->find_by_class('main-content')->list_by_id($search_box_id)) { ok($element->attribute('id') eq $search_box_id, "Correctly found nested element with list_by_id"); $count += 1; } ok($count == 1, "Found elements with nested list_by_id:$count"); $count = 0; foreach my $element ($firefox->find_by_class('main-content')->find_by_id($search_box_id)) { ok($element->attribute('id') eq $search_box_id, "Correctly found nested element with find_by_id"); $count += 1; } ok($count == 1, "Found elements with nested find_by_id:$count"); $count = 0; foreach my $element ($firefox->find_class('main-content')->find_id($search_box_id)) { ok($element->attribute('id') eq $search_box_id, "Correctly found nested element with find_id"); $count += 1; } ok($count == 1, "Found elements with nested find_id:$count"); $count = 0; foreach my $element ($firefox->find_by_id($search_box_id)) { ok($element->attribute('id') eq $search_box_id, "Correctly found element with wantarray find_by_id"); $count += 1; } ok($count == 1, "Found elements with wantarray find_by_id:$count"); ok($firefox->find('q', 'name')->attribute('id') eq $search_box_id, "Correctly found element when searching by id"); ok($firefox->find('q', BY_NAME())->attribute('id') eq $search_box_id, "Correctly found element when searching by id"); ok($firefox->list_by_name('q')->attribute('id') eq $search_box_id, "Correctly found element with list_by_name"); ok($firefox->find_by_name('q')->attribute('id') eq $search_box_id, "Correctly found element with find_by_name"); ok($firefox->find_by_class('main-content')->find_by_name('q')->attribute('id') eq $search_box_id, "Correctly found nested element with find_by_name"); ok($firefox->find_name('q')->attribute('id') eq $search_box_id, "Correctly found element with find_name"); ok($firefox->has_name('q')->attribute('id') eq $search_box_id, "Correctly found element with has_name"); ok(!defined $firefox->has_name('q-definitely-not-exists'), "Correctly returned undef for has_name and a missing element"); ok($firefox->find_class('main-content')->find_name('q')->attribute('id') eq $search_box_id, "Correctly found nested element with find_name"); ok($firefox->has_class('main-content')->has_name('q')->attribute('id') eq $search_box_id, "Correctly found nested element with has_name"); $count = 0; foreach my $element ($firefox->find_by_class('main-content')->list_by_name('q')) { ok($element->attribute('id') eq $search_box_id, "Correctly found nested element with list_by_name"); $count += 1; } ok($count == 1, "Found elements with nested list_by_name:$count"); $count = 0; foreach my $element ($firefox->find_by_class('main-content')->find_by_name('q')) { ok($element->attribute('id') eq $search_box_id, "Correctly found nested element with find_by_name"); $count += 1; } ok($count == 1, "Found elements with nested find_by_name:$count"); $count = 0; foreach my $element ($firefox->find_by_name('q')) { ok($element->attribute('id') eq $search_box_id, "Correctly found element with wantarray find_by_name"); $count += 1; } ok($count == 1, "Found elements with wantarray find_by_name:$count"); $count = 0; foreach my $element ($firefox->find_name('q')) { ok($element->attribute('id') eq $search_box_id, "Correctly found element with wantarray find_name"); $count += 1; } ok($count == 1, "Found elements with wantarray find_name:$count"); ok($firefox->find('input', 'tag name')->attribute('id'), "Correctly found element when searching by tag name"); ok($firefox->find('input', BY_TAG())->attribute('id'), "Correctly found element when searching by tag name"); ok($firefox->list_by_tag('input')->attribute('id'), "Correctly found element with list_by_tag"); ok($firefox->find_by_tag('input')->attribute('id'), "Correctly found element with find_by_tag"); ok($firefox->find_by_class('main-content')->find_by_tag('input')->attribute('id'), "Correctly found nested element with find_by_tag"); ok($firefox->find_tag('input')->attribute('id'), "Correctly found element with find_tag"); ok($firefox->has_tag('input')->attribute('id'), "Correctly found element with has_tag"); ok($firefox->find_class('main-content')->find_tag('input')->attribute('id'), "Correctly found nested element with find_tag"); ok($firefox->has_class('main-content')->has_tag('input')->attribute('id'), "Correctly found nested element with has_tag"); $count = 0; foreach my $element ($firefox->find_by_class('main-content')->list_by_tag('input')) { ok($element->attribute('id'), "Correctly found nested element with list_by_tag"); $count += 1; } ok($count == 2, "Found elements with nested list_by_tag:$count"); $count = 0; foreach my $element ($firefox->find_by_class('main-content')->find_by_tag('input')) { ok($element->attribute('id'), "Correctly found nested element with find_by_tag"); $count += 1; } ok($count == 2, "Found elements with nested find_by_tag:$count"); $count = 0; foreach my $element ($firefox->find_by_tag('input')) { ok($element->attribute('id'), "Correctly found element with wantarray find_by_tag"); $count += 1; } ok($count == 2, "Found elements with wantarray find_by_tag:$count"); $count = 0; foreach my $element ($firefox->find_tag('input')) { ok($element->attribute('id'), "Correctly found element with wantarray find_tag"); $count += 1; } ok($count == 2, "Found elements with wantarray find_by_tag:$count"); ok($firefox->find('form-control home-search-input', 'class name')->attribute('id'), "Correctly found element when searching by class name"); ok($firefox->find('form-control home-search-input', BY_CLASS())->attribute('id'), "Correctly found element when searching by class name"); ok($firefox->list_by_class('form-control home-search-input')->attribute('id'), "Correctly found element with list_by_class"); ok($firefox->find_by_class('form-control home-search-input')->attribute('id'), "Correctly found element with find_by_class"); ok($firefox->find_by_class('main-content')->find_by_class('form-control home-search-input')->attribute('id'), "Correctly found nested element with find_by_class"); ok($firefox->find_class('form-control home-search-input')->attribute('id'), "Correctly found element with find_class"); ok($firefox->find_class('main-content')->find_class('form-control home-search-input')->attribute('id'), "Correctly found nested element with find_class"); ok($firefox->has_class('main-content')->has_class('form-control home-search-input')->attribute('id'), "Correctly found nested element with has_class"); ok(!defined $firefox->has_class('main-content')->has_class('absolutely-can-never-exist-in-any-universe-seriously-10'), "Correctly returned undef for nested element with has_class for a missing class"); $count = 0; foreach my $element ($firefox->find_by_class('main-content')->list_by_class('form-control home-search-input')) { ok($element->attribute('id'), "Correctly found nested element with list_by_class"); $count += 1; } ok($count == 1, "Found elements with nested find_by_class:$count"); $count = 0; foreach my $element ($firefox->find_by_class('main-content')->find_by_class('form-control home-search-input')) { ok($element->attribute('id'), "Correctly found element with wantarray find_by_class"); $count += 1; } ok($count == 1, "Found elements with wantarray find_by_class:$count"); $count = 0; foreach my $element ($firefox->find_class('main-content')->find_class('form-control home-search-input')) { ok($element->attribute('id'), "Correctly found element with wantarray find_class"); $count += 1; } ok($count == 1, "Found elements with wantarray find_by_class:$count"); ok($firefox->find('input.home-search-input', 'css selector')->attribute('id'), "Correctly found element when searching by css selector"); ok($firefox->find('input.home-search-input', BY_SELECTOR())->attribute('id'), "Correctly found element when searching by css selector"); ok($firefox->list_by_selector('input.home-search-input')->attribute('id'), "Correctly found element with list_by_selector"); ok($firefox->find_by_selector('input.home-search-input')->attribute('id'), "Correctly found element with find_by_selector"); ok($firefox->find_by_class('main-content')->find_by_selector('input.home-search-input')->attribute('id'), "Correctly found nested element with find_by_selector"); ok($firefox->find_selector('input.home-search-input')->attribute('id'), "Correctly found element with find_selector"); ok($firefox->find_class('main-content')->find_selector('input.home-search-input')->attribute('id'), "Correctly found nested element with find_selector"); ok($firefox->has_class('main-content')->has_selector('input.home-search-input')->attribute('id'), "Correctly found nested element with has_selector"); $count = 0; foreach my $element ($firefox->find_by_class('main-content')->list_by_selector('input.home-search-input')) { ok($element->attribute('id'), "Correctly found nested element with list_by_selector"); $count += 1; } ok($count == 1, "Found elements with nested list_by_selector:$count"); $count = 0; foreach my $element ($firefox->find_by_class('main-content')->find_by_selector('input.home-search-input')) { ok($element->attribute('id'), "Correctly found nested element with find_by_selector"); $count += 1; } ok($count == 1, "Found elements with nested find_by_selector:$count"); $count = 0; foreach my $element ($firefox->has_selector('input.home-search-input')) { ok($element->attribute('id'), "Correctly found wantarray element with has_selector"); $count += 1; } ok($count == 1, "Found elements with wantarray has_selector:$count"); $count = 0; foreach my $element ($firefox->find_by_selector('input.home-search-input')) { ok($element->attribute('id'), "Correctly found wantarray element with find_by_selector"); $count += 1; } ok($count == 1, "Found elements with wantarray find_by_selector:$count"); $count = 0; foreach my $element ($firefox->find_selector('input.home-search-input')) { ok($element->attribute('id'), "Correctly found wantarray element with find_selector"); $count += 1; } ok($count == 1, "Found elements with wantarray find_by_selector:$count"); ok($firefox->find('API', 'link text')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found element when searching by link text"); ok($firefox->find('API', BY_LINK())->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found element when searching by link text"); ok($firefox->list_by_link('API')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found element with list_by_link"); ok($firefox->find_by_link('API')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found element with find_by_link"); TODO: { local $TODO = $major_version == 45 ? "Nested find_link can break for $major_version.$minor_version.$patch_version" : undef; my $result; eval { $result = $firefox->find_by_class('container-fluid')->find_by_link('API')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx; }; ok($result, "Correctly found nested element with find_by_link"); } ok($firefox->find_link('API')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found element with find_link"); ok($firefox->has_link('API')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found element with has_link"); TODO: { local $TODO = $major_version == 45 ? "Nested find_link can break for $major_version.$minor_version.$patch_version" : undef; my $result; eval { $result = $firefox->find_class('container-fluid')->find_link('API')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx; }; ok($result, "Correctly found nested element with find_link"); eval { $result = $firefox->has_class('container-fluid')->has_link('API')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx; }; ok($result, "Correctly found nested element with has_link"); } $count = 0; foreach my $element ($firefox->find_by_class('navbar navbar-default')->list_by_link('API')) { ok($element->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found nested element with list_by_link"); $count += 1; } SKIP: { if (($count == 0) && ($major_version < 50)) { chomp $@; diag("Nested list_by_link can break for $major_version.$minor_version.$patch_version:$@"); skip("Nested list_by_link can break for $major_version.$minor_version.$patch_version", 2); } ok($count == 1, "Found elements with nested list_by_link:$count"); } $count = 0; foreach my $element ($firefox->find_by_class('container-fluid')->find_by_link('API')) { ok($element->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found nested element with find_by_link"); $count += 1; } SKIP: { if (($count == 0) && ($major_version < 50)) { chomp $@; diag("Nested find_by_link can break for $major_version.$minor_version.$patch_version:$@"); skip("Nested find_by_link can break for $major_version.$minor_version.$patch_version", 2); } ok($count == 1, "Found elements with nested find_by_link:$count"); } $count = 0; foreach my $element ($firefox->find_by_link('API')) { ok($element->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found wantarray element with find_by_link"); $count += 1; } if (($count == 1) && ($major_version < 50)) { SKIP: { skip("Firefox $major_version.$minor_version.$patch_version does not correctly implement returning multiple elements for find_by_link", 2); } } else { ok($count == 2, "Found elements with wantarray find_by_link:$count"); } $count = 0; foreach my $element ($firefox->find_link('API')) { ok($element->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found wantarray element with find_link"); $count += 1; } if (($count == 1) && ($major_version < 50)) { SKIP: { skip("Firefox $major_version.$minor_version.$patch_version does not correctly implement returning multiple elements for find_link", 2); } } else { ok($count == 2, "Found elements with wantarray find_link:$count"); } ok($firefox->find('AP', 'partial link text')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found element when searching by partial link text"); ok($firefox->find('AP', BY_PARTIAL())->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found element when searching by partial link text"); ok($firefox->list_by_partial('AP')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found element with list_by_partial"); ok($firefox->find_by_partial('AP')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found element with find_by_partial"); ok($firefox->find_by_class('container-fluid')->find_by_partial('AP')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found nested element with find_by_partial"); ok($firefox->find_partial('AP')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found element with find_partial"); ok($firefox->has_partial('AP')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found element with has_partial"); ok($firefox->find_class('container-fluid')->find_partial('AP')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found nested element with find_partial"); ok($firefox->has_class('container-fluid')->has_partial('AP')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found nested element with has_partial"); $count = 0; foreach my $element ($firefox->find_by_class('container-fluid')->list_by_partial('AP')) { if ($count == 0) { ok($element->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found nested element with list_by_partial"); } $count +=1; } if (($count == 2) && ($major_version < 50)) { SKIP: { skip("Firefox $major_version.$minor_version.$patch_version does not correctly implement returning multiple elements for list_by_partial", 1); } } else { ok($count == 1, "Found elements with nested list_by_partial:$count"); } $count = 0; foreach my $element ($firefox->find_by_class('container-fluid')->find_by_partial('AP')) { if ($count == 0) { ok($element->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found nested element with find_by_partial"); } $count +=1; } if (($count == 2) && ($major_version < 50)) { SKIP: { skip("Firefox $major_version.$minor_version.$patch_version does not correctly implement returning multiple elements for find_by_partial", 1); } } else { ok($count == 1, "Found elements with nested find_by_partial:$count"); } $count = 0; foreach my $element ($firefox->find_by_partial('AP')) { ok($element->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found wantarray element with find_by_partial"); $count +=1; } ok($count == 2, "Found elements with wantarray find_by_partial:$count"); $count = 0; foreach my $element ($firefox->find_partial('AP')) { ok($element->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found wantarray element with find_partial"); $count +=1; } ok($count == 2, "Found elements with wantarray find_partial:$count"); my $css_rule; ok($css_rule = $firefox->find('//input[@id="' . $search_box_id . '"]')->css('display'), "The value of the css rule 'display' is '$css_rule'"); my $result = $firefox->find('//input[@id="' . $search_box_id . '"]')->is_enabled(); ok($result =~ /^[01]$/, "is_enabled returns 0 or 1 for //input[\@id=\"$search_box_id\"]:$result"); $result = $firefox->find('//input[@id="' . $search_box_id . '"]')->is_displayed(); ok($result =~ /^[01]$/, "is_displayed returns 0 or 1 for //input[\@id=\"$search_box_id\"]:$result"); $result = $firefox->find('//input[@id="' . $search_box_id . '"]')->is_selected(); ok($result =~ /^[01]$/, "is_selected returns 0 or 1 for //input[\@id=\"$search_box_id\"]:$result"); ok($firefox->find('//input[@id="' . $search_box_id . '"]')->clear(), "Clearing the element directly"); TODO: { local $TODO = $major_version < 50 ? "property and attribute methods can have different values for empty" : undef; ok((!defined $firefox->find_id($search_box_id)->attribute('value')) && ($firefox->find_id($search_box_id)->property('value') eq ''), "Initial property and attribute values are empty for $search_box_id"); } ok($firefox->find('//input[@id="' . $search_box_id . '"]')->send_keys('Test::More'), "Sent 'Test::More' to the '$search_box_id' field directly to the element"); TODO: { local $TODO = $major_version < 50 ? "attribute method can have different values for empty" : undef; ok(!defined $firefox->find_id($search_box_id)->attribute('value'), "attribute for '$search_box_id' is still not defined "); } my $property; eval { $property = $firefox->find_id($search_box_id)->property('value'); }; SKIP: { if ((!$property) && ($major_version < 50)) { chomp $@; diag("The property method is not supported for $major_version.$minor_version.$patch_version:$@"); skip("The property method is not supported for $major_version.$minor_version.$patch_version", 1); } ok($property eq 'Test::More', "property for '$search_box_id' is now 'Test::More'"); } ok($firefox->find('//input[@id="' . $search_box_id . '"]')->clear(), "Clearing the element directly"); foreach my $element ($firefox->find_elements('//input[@id="' . $search_box_id . '"]')) { ok($firefox->send_keys($element, 'Test::More'), "Sent 'Test::More' to the '$search_box_id' field via the browser"); ok($firefox->clear($element), "Clearing the element via the browser"); ok($firefox->type($element, 'Test::More'), "Sent 'Test::More' to the '$search_box_id' field via the browser"); last; } my $text = $firefox->find('//button[@name="lucky"]')->text(); ok($text, "Read '$text' directly from 'Lucky' button"); my $tag_name = $firefox->find('//button[@name="lucky"]')->tag_name(); ok($tag_name, "'Lucky' button has a tag name of '$tag_name'"); my $rect; eval { $rect = $firefox->find('//button[@name="lucky"]')->rect(); }; SKIP: { if (($major_version < 50) && (!defined $rect)) { skip("Firefox $major_version does not appear to support the \$firefox->window_rect() method", 4); } ok($rect->pos_x() =~ /^\d+([.]\d+)?$/, "'Lucky' button has a X position of " . $rect->pos_x()); ok($rect->pos_y() =~ /^\d+([.]\d+)?$/, "'Lucky' button has a Y position of " . $rect->pos_y()); ok($rect->width() =~ /^\d+([.]\d+)?$/, "'Lucky' button has a width of " . $rect->width()); ok($rect->height() =~ /^\d+([.]\d+)?$/, "'Lucky' button has a height of " . $rect->height()); } ok(((scalar $firefox->cookies()) >= 0), "\$firefox->cookies() shows cookies on " . $firefox->uri()); ok($firefox->delete_cookies() && ((scalar $firefox->cookies()) == 0), "\$firefox->delete_cookies() clears all cookies"); my $capabilities = $firefox->capabilities(); my $buffer = undef; ok($firefox->selfie(raw => 1) =~ /^\x89\x50\x4E\x47\x0D\x0A\x1A\x0A/smx, "\$firefox->selfie(raw => 1) returns a PNG image"); my $handle = $firefox->selfie(); $handle->read($buffer, 20); ok($buffer =~ /^\x89\x50\x4E\x47\x0D\x0A\x1A\x0A/smx, "\$firefox->selfie() returns a PNG file"); $buffer = undef; $handle = $firefox->find('//button[@name="lucky"]')->selfie(); ok(ref $handle eq 'File::Temp', "\$firefox->selfie() returns a File::Temp object"); $handle->read($buffer, 20); ok($buffer =~ /^\x89\x50\x4E\x47\x0D\x0A\x1A\x0A/smx, "\$firefox->find('//button[\@name=\"lucky\"]')->selfie() returns a PNG file"); if ($major_version < 31) { SKIP: { skip("Firefox before 31 can hang when processing the hash parameter", 3); } } else { my $actual_digest = $firefox->selfie(hash => 1, highlights => [ $firefox->find('//button[@name="lucky"]') ]); SKIP: { if (($major_version < 50) && ($actual_digest !~ /^[a-f0-9]+$/smx)) { skip("Firefox $major_version does not appear to support the hash parameter for the \$firefox->selfie method", 1); } ok($actual_digest =~ /^[a-f0-9]+$/smx, "\$firefox->selfie(hash => 1, highlights => [ \$firefox->find('//button[\@name=\"lucky\"]') ]) returns a hex encoded SHA256 digest"); } $handle = $firefox->selfie(highlights => [ $firefox->find('//button[@name="lucky"]') ]); $buffer = undef; $handle->read($buffer, 20); ok($buffer =~ /^\x89\x50\x4E\x47\x0D\x0A\x1A\x0A/smx, "\$firefox->selfie(highlights => [ \$firefox->find('//button[\@name=\"lucky\"]') ]) returns a PNG file"); $handle->seek(0,0) or die "Failed to seek:$!"; $handle->read($buffer, 1_000_000) or die "Failed to read:$!"; my $correct_digest = Digest::SHA::sha256_hex(MIME::Base64::encode_base64($buffer, q[])); TODO: { local $TODO = "Digests can sometimes change for all platforms"; ok($actual_digest eq $correct_digest, "\$firefox->selfie(hash => 1, highlights => [ \$firefox->find('//button[\@name=\"lucky\"]') ]) returns the correct hex encoded SHA256 hash of the base64 encoded image"); } } my $clicked; my @elements = $firefox->find('//a[@href="https://fastapi.metacpan.org"]'); if (out_of_time()) { skip("Running out of time. Trying to shutdown tests as fast as possible", 61); } ELEMENTS: { foreach my $element (@elements) { if ($major_version < 31) { eval { if (($element->is_displayed()) && ($element->is_enabled())) { $element->click(); $clicked = 1; } }; } else { if (($element->is_displayed()) && ($element->is_enabled())) { $element->click(); $clicked = 1; } } if ($clicked) { if ($major_version < 31) { if ($firefox->uri()->host() eq 'github.com') { last ELEMENTS; } else { sleep 2; redo ELEMENTS; } } else { last ELEMENTS; } } } } ok($clicked, "Clicked the API link"); $firefox->sleep_time_in_ms(1_000); ok($firefox->await(sub { $firefox->uri()->host() eq 'github.com' }), "\$firefox->uri()->host() is equal to github.com:" . $firefox->uri()); while(!$firefox->loaded()) { diag("Waiting for firefox to load after clicking on API link"); sleep 1; } my @cookies = $firefox->cookies(); ok($cookies[0]->name() =~ /\w/, "The first cookie name is '" . $cookies[0]->name() . "'"); ok($cookies[0]->value() =~ /\w/, "The first cookie value is '" . $cookies[0]->value() . "'"); TODO: { local $TODO = ($major_version < 56) ? "\$cookies[0]->expiry() does not function for Firefox versions less than 56" : undef; if (defined $cookies[0]->expiry()) { ok($cookies[0]->expiry() =~ /^\d+$/, "The first cookie name has an integer expiry date of '" . ($cookies[0]->expiry() || q[]) . "'"); } else { ok(1, "The first cookie is a session cookie"); } } ok($cookies[0]->http_only() =~ /^[01]$/, "The first cookie httpOnly flag is a boolean set to '" . $cookies[0]->http_only() . "'"); ok($cookies[0]->secure() =~ /^[01]$/, "The first cookie secure flag is a boolean set to '" . $cookies[0]->secure() . "'"); ok($cookies[0]->path() =~ /\S/, "The first cookie path is a string set to '" . $cookies[0]->path() . "'"); ok($cookies[0]->domain() =~ /^[\w\-.]+$/, "The first cookie domain is a domain set to '" . $cookies[0]->domain() . "'"); if (defined $cookies[0]->same_site()) { ok($cookies[0]->same_site() =~ /^(Lax|Strict|None)$/, "The first cookie same-site value is legal '" . $cookies[0]->same_site() . "'"); } else { diag("Possible no same-site support for $major_version.$minor_version.$patch_version"); ok(1, "The first cookie same-site value is not present"); } my $original_number_of_cookies = scalar @cookies; ok(($original_number_of_cookies > 1) && ((ref $cookies[0]) eq 'Firefox::Marionette::Cookie'), "\$firefox->cookies() returns more than 1 cookie on " . $firefox->uri()); ok($firefox->delete_cookie($cookies[0]->name()), "\$firefox->delete_cookie('" . $cookies[0]->name() . "') deletes the specified cookie name"); ok(not(grep { $_->name() eq $cookies[0]->name() } $firefox->cookies()), "List of cookies no longer includes " . $cookies[0]->name()); ok($firefox->back(), "\$firefox->back() goes back one page"); while(!$firefox->loaded()) { diag("Waiting for firefox to load after clicking back button"); sleep 1; } while($firefox->uri()->host() ne 'metacpan.org') { diag("Waiting to load previous page:" . $firefox->uri()->host()); sleep 1; } ok($firefox->uri()->host() eq 'metacpan.org', "\$firefox->uri()->host() is equal to metacpan.org:" . $firefox->uri()); ok($firefox->forward(), "\$firefox->forward() goes forward one page"); while(!$firefox->loaded()) { diag("Waiting for firefox to load after clicking forward button"); sleep 1; } while($firefox->uri()->host() ne 'github.com') { diag("Waiting to load next page:" . $firefox->uri()->host()); sleep 1; } ok($firefox->uri()->host() eq 'github.com', "\$firefox->uri()->host() is equal to github.com:" . $firefox->uri()); ok($firefox->back(), "\$firefox->back() goes back one page"); while(!$firefox->loaded()) { diag("Waiting for firefox to load after clicking back button (2)"); sleep 1; } while($firefox->uri()->host() ne 'metacpan.org') { diag("Waiting to load previous page (2):" . $firefox->uri()->host()); sleep 1; } ok($firefox->uri()->host() eq 'metacpan.org', "\$firefox->uri()->host() is equal to metacpan.org:" . $firefox->uri()); my %additional; if ($major_version >= 64) { $additional{sandbox} = 'system'; } ok($firefox->script('return true;', %additional), "javascript command 'return true' executes successfully"); ok($firefox->script('return true', timeout => 10_000, new => 1, %additional), "javascript command 'return true' (using timeout and new (true) as parameters)"); ok($firefox->script('return true', scriptTimeout => 20_000, newSandbox => 0, %additional), "javascript command 'return true' (using scriptTimeout and newSandbox (false) as parameters)"); my $cookie = Firefox::Marionette::Cookie->new(name => 'BonusCookie', value => 'who really cares about privacy', expiry => time + 500000); ok($firefox->add_cookie($cookie), "\$firefox->add_cookie() adds a Firefox::Marionette::Cookie without a domain"); $cookie = Firefox::Marionette::Cookie->new(name => 'BonusSessionCookie', value => 'will go away anyway', sameSite => 0, httpOnly => 0, secure => 0); ok($firefox->add_cookie($cookie), "\$firefox->add_cookie() adds a Firefox::Marionette::Cookie without expiry"); $cookie = Firefox::Marionette::Cookie->new(name => 'StartingCookie', value => 'not sure aböut this', httpOnly => 1, secure => 1, sameSite => 1); ok($firefox->add_cookie($cookie), "\$firefox->add_cookie() adds a Firefox::Marionette::Cookie with a domain"); ok($firefox->find_id($search_box_id)->clear()->find_id($search_box_id)->type('Test::More'), "Sent 'Test::More' to the '$search_box_id' field directly to the element"); if (out_of_time()) { skip("Running out of time. Trying to shutdown tests as fast as possible", 36); } my $dummy_object = bless {}, 'What::is::this::object'; foreach my $name ('click', 'clear', 'is_selected', 'is_enabled', 'is_displayed', 'type', 'tag_name', 'rect', 'text') { eval { $firefox->$name({}); }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->$name() with a hash parameter produces a Firefox::Marionette::Exception exception"); eval { $firefox->$name(q[]); }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->$name() with a non ref parameter produces a Firefox::Marionette::Exception exception"); eval { $firefox->$name($dummy_object); }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->$name() with a non Element blessed parameter produces a Firefox::Marionette::Exception exception"); } ok($firefox->find_name('lucky')->click($element), "Clicked the \"I'm Feeling Lucky\" button"); diag("Going to Test::More page with a page load strategy of " . ($capabilities->page_load_strategy() || '')); SKIP: { if ($major_version < 45) { skip("Firefox below 45 (at least 24) does not support the getContext method", 5); } if (($major_version <= 63) && ($ENV{FIREFOX_VISIBLE})) { skip("Firefox below 63 are having problems with Xvfb", 5); } ok($firefox->bye(sub { $firefox->find_id('not-there-at-all') })->await(sub { $firefox->interactive() && $firefox->find_partial('Download'); })->click(), "Clicked on the download link"); diag("Clicked download link"); while(!$firefox->downloads()) { sleep 1; } while($firefox->downloading()) { sleep 1; } $count = 0; my $download_path; foreach my $path ($firefox->downloads()) { diag("Downloaded $path"); if ($path =~ /Test\-Simple/) { # dodging possible Devel::Cover messages $download_path = $path; $count += 1; } elsif ($INC{'Devel/Cover.pm'}) { } else { $count += 1; } } ok($count == 1, "Downloaded 1 files:$count"); my $handle = $firefox->download($download_path); ok($handle->isa('GLOB'), "Obtained GLOB from \$firefox->download(\$path)"); if ($INC{'Devel/Cover.pm'}) { } else { my $gz = Compress::Zlib::gzopen($handle, 'rb') or die "Failed to open gzip stream"; my $bytes_read = 0; while($gz->gzread(my $buffer, 4096)) { $bytes_read += length $buffer } ok($bytes_read > 1_000, "Downloaded file is gzipped"); } } foreach my $element ($firefox->find_tag('option')) { my $inner_html; eval { $inner_html = $element->property('innerHTML'); }; if ((defined $inner_html) && ($inner_html eq 'Jump to version')) { $firefox->script('arguments[0].selected = true', args => $element); ok($element->is_selected(), "\$firefox->is_selected() returns true for a selected item"); $firefox->script('arguments[0].disabled = true', args => $element); ok(!$element->is_enabled(), "After script disabled element, \$firefox->is_enabled() correctly reflects disabling"); } } $firefox->go('https://metacpan.org'); ok(!exists $INC{'Keys.pm'}, "Firefox::Marionette::Keys is not loaded"); eval { require Firefox::Marionette::Keys; }; ok($@ eq '', "Successfully loaded Firefox::Marionette::Keys"); Firefox::Marionette::Keys->import(qw(:all)); ok(CANCEL() eq chr 0xE001, "CANCEL() is correct as 0xE001"); ok(HELP() eq chr 0xE002, "HELP() is correct as OxE002"); ok(BACKSPACE() eq chr 0xE003, "BACKSPACE() is correct as OxE003"); ok(TAB() eq chr 0xE004, "TAB() is correct as OxE004"); ok(CLEAR() eq chr 0xE005, "CLEAR() is correct as OxE005"); ok(ENTER() eq chr 0xE006, "ENTER() is correct as OxE006"); ok(SHIFT() eq chr 0xE008, "SHIFT() is correct as OxE008 (Same as SHIFT_LEFT())"); ok(SHIFT_LEFT() eq chr 0xE008, "SHIFT_LEFT() is correct as OxE008"); ok(CONTROL() eq chr 0xE009, "CONTROL() is correct as OxE009 (Same as CONTROL_LEFT())"); ok(CONTROL_LEFT() eq chr 0xE009, "CONTROL_LEFT() is correct as OxE009"); ok(ALT() eq chr 0xE00A, "ALT() is correct as OxE00A (Same as ALT_LEFT())"); ok(ALT_LEFT() eq chr 0xE00A, "ALT_LEFT() is correct as OxE00A"); ok(PAUSE() eq chr 0xE00B, "PAUSE() is correct as OxE00B"); ok(ESCAPE() eq chr 0xE00C, "ESCAPE() is correct as OxE00C"); ok(SPACE() eq chr 0xE00D, "SPACE() is correct as OxE00D"); ok(PAGE_UP() eq chr 0xE00E, "PAGE_UP() is correct as OxE00E"); ok(PAGE_DOWN() eq chr 0xE00F, "PAGE_DOWN() is correct as OxE00F"); ok(END_KEY() eq chr 0xE010, "END_KEY() is correct as OxE010"); ok(HOME() eq chr 0xE011, "HOME() is correct as OxE011"); ok(ARROW_LEFT() eq chr 0xE012, "ARROW_LEFT() is correct as OxE012"); ok(ARROW_UP() eq chr 0xE013, "ARROW_UP() is correct as OxE013"); ok(ARROW_RIGHT() eq chr 0xE014, "ARROW_UP() is correct as OxE014"); ok(ARROW_DOWN() eq chr 0xE015, "ARROW_DOWN() is correct as OxE015"); ok(INSERT() eq chr 0xE016, "INSERT() is correct as OxE016"); ok(DELETE() eq chr 0xE017, "DELETE() is correct as OxE017"); ok(F1() eq chr 0xE031, "F1() is correct as OxE031"); ok(F2() eq chr 0xE032, "F2() is correct as OxE032"); ok(F3() eq chr 0xE033, "F3() is correct as OxE033"); ok(F4() eq chr 0xE034, "F4() is correct as OxE034"); ok(F5() eq chr 0xE035, "F5() is correct as OxE035"); ok(F6() eq chr 0xE036, "F6() is correct as OxE036"); ok(F7() eq chr 0xE037, "F7() is correct as OxE037"); ok(F8() eq chr 0xE038, "F8() is correct as OxE038"); ok(F9() eq chr 0xE039, "F9() is correct as OxE039"); ok(F10() eq chr 0xE03A, "F10() is correct as OxE03A"); ok(F11() eq chr 0xE03B, "F11() is correct as OxE03B"); ok(F12() eq chr 0xE03C, "F12() is correct as OxE03C"); ok(META() eq chr 0xE03D, "META() is correct as OxE03D (Same as META_LEFT())"); ok(META_LEFT() eq chr 0xE03D, "META_LEFT() is correct as OxE03D"); ok(ZENKAKU_HANKAKU() eq chr 0xE040, "ZENKAKU_HANKAKU() is correct as OxE040"); ok(SHIFT_RIGHT() eq chr 0xE050, "SHIFT_RIGHT() is correct as OxE050"); ok(CONTROL_RIGHT() eq chr 0xE051, "CONTROL_RIGHT() is correct as OxE051"); ok(ALT_RIGHT() eq chr 0xE052, "ALT_RIGHT() is correct as OxE052"); ok(META_RIGHT() eq chr 0xE053, "META_RIGHT() is correct as OxE053"); ok(!exists $INC{'Buttons.pm'}, "Firefox::Marionette::Buttons is not loaded"); eval { require Firefox::Marionette::Buttons; }; ok($@ eq '', "Successfully loaded Firefox::Marionette::Buttons"); Firefox::Marionette::Buttons->import(qw(:all)); ok(LEFT_BUTTON() == 0, "LEFT_BUTTON() is correct as O"); ok(MIDDLE_BUTTON() == 1, "MIDDLE_BUTTON() is correct as 1"); ok(RIGHT_BUTTON() == 2, "RIGHT_BUTTON() is correct as 2"); my $help_button = $firefox->find_class('btn search-btn help-btn'); ok($help_button, "Found help button on metacpan.org"); SKIP: { my $perform_ok; eval { $perform_ok = $firefox->perform( $firefox->key_down('h'), $firefox->pause(2), $firefox->key_up('h'), $firefox->mouse_move($help_button), $firefox->mouse_down(LEFT_BUTTON()), $firefox->pause(1), $firefox->mouse_up(LEFT_BUTTON()), $firefox->key_down(ESCAPE()), $firefox->pause(2), $firefox->key_up(ESCAPE()), ); }; if ((!$perform_ok) && ($major_version < 60)) { chomp $@; diag("The perform method is not supported for $major_version.$minor_version.$patch_version:$@"); skip("The perform method is not supported for $major_version.$minor_version.$patch_version", 5); } ok(ref $perform_ok eq $class, "\$firefox->perform() with a combination of mouse, pause and key actions"); my $value = $firefox->find('//input[@id="' . $search_box_id . '"]')->property('value'); ok($value eq 'h', "\$firefox->find('//input[\@id=\"$search_box_id\"]')->property('value') is equal to 'h' from perform method above:$value"); ok($firefox->perform($firefox->pause(2)), "\$firefox->perform() with a single pause action"); ok($firefox->perform($firefox->mouse_move(x => 0, y => 0),$firefox->mouse_down(), $firefox->mouse_up()), "\$firefox->perform() with a default mouse button and manual x,y co-ordinates"); eval { $firefox->perform({ type => 'unknown' }); }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->perform() throws an exception when passed an unknown action:$@"); ok($firefox->release(), "\$firefox->release()"); } SKIP: { if ((!$context) && ($major_version < 50)) { chomp $@; diag("\$firefox->context is not supported for $major_version.$minor_version.$patch_version:$@"); skip("\$firefox->context is not supported for $major_version.$minor_version.$patch_version", 2); } ok($firefox->chrome()->context() eq 'chrome', "Setting and reading context of the browser as 'chrome'"); ok($firefox->content()->context() eq 'content', "Setting and reading context of the browser as 'content'"); } my $body = $firefox->find("//body"); my $outer_html = $firefox->script(q{ return arguments[0].outerHTML;}, args => [$body]); ok($outer_html =~ /script(q{ return arguments[0].outerHTML;}, args => $body); ok($outer_html =~ /find('//a'); $firefox->script(q{arguments[0].parentNode.removeChild(arguments[0]);}, args => [$link]); eval { $link->attribute('href'); }; ok($@->isa('Firefox::Marionette::Exception::StaleElement') && $@ =~ /stale/smxi, "Correctly throws useful stale element exception"); ok($@->status() || 1, "Firefox::Marionette::Exception::Response->status() is callable:" . ($@->status() || q[])); ok($@->message(), "Firefox::Marionette::Exception::Response->message() is callable:" . $@->message()); ok($@->error() || 1, "Firefox::Marionette::Exception::Response->error() is callable:" . ($@->error() || q[])); ok($@->trace() || 1, "Firefox::Marionette::Exception::Response->trace() is callable"); SKIP: { if ((!$chrome_window_handle_supported) && ($major_version < 50)) { diag("\$firefox->current_chrome_window_handle is not supported for $major_version.$minor_version.$patch_version"); skip("\$firefox->current_chrome_window_handle is not supported for $major_version.$minor_version.$patch_version", 1); } my $current_chrome_window_handle = $firefox->current_chrome_window_handle(); if ($major_version < 90) { ok($current_chrome_window_handle =~ /^\d+$/, "Returned the current chrome window handle as an integer"); } else { ok($current_chrome_window_handle =~ /^$guid_regex$/smx, "Returned the current chrome window handle as a GUID"); } } $capabilities = $firefox->capabilities(); ok((ref $capabilities) eq 'Firefox::Marionette::Capabilities', "\$firefox->capabilities() returns a Firefox::Marionette::Capabilities object"); SKIP: { if (!grep /^page_load_strategy$/, $capabilities->enumerate()) { diag("\$capabilities->page_load_strategy is not supported for " . $capabilities->browser_version()); skip("\$capabilities->page_load_strategy is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->page_load_strategy() =~ /^\w+$/, "\$capabilities->page_load_strategy() is a string:" . $capabilities->page_load_strategy()); } ok($capabilities->moz_headless() =~ /^(1|0)$/, "\$capabilities->moz_headless() is a boolean:" . $capabilities->moz_headless()); SKIP: { if (!grep /^accept_insecure_certs$/, $capabilities->enumerate()) { diag("\$capabilities->accept_insecure_certs is not supported for " . $capabilities->browser_version()); skip("\$capabilities->accept_insecure_certs is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->accept_insecure_certs() =~ /^(1|0)$/, "\$capabilities->accept_insecure_certs() is a boolean:" . $capabilities->accept_insecure_certs()); } SKIP: { if (!grep /^moz_process_id$/, $capabilities->enumerate()) { diag("\$capabilities->moz_process_id is not supported for " . $capabilities->browser_version()); skip("\$capabilities->moz_process_id is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->moz_process_id() =~ /^\d+$/, "\$capabilities->moz_process_id() is an integer:" . $capabilities->moz_process_id()); } SKIP: { if (!grep /^moz_build_id$/, $capabilities->enumerate()) { diag("\$capabilities->moz_build_id is not supported for " . $capabilities->browser_version()); skip("\$capabilities->moz_build_id is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->moz_build_id() =~ /^\d{14}$/, "\$capabilities->moz_build_id() is an date/timestamp:" . $capabilities->moz_build_id()); } ok($capabilities->browser_name() =~ /^\w+$/, "\$capabilities->browser_name() is a string:" . $capabilities->browser_name()); ok($capabilities->rotatable() =~ /^(1|0)$/, "\$capabilities->rotatable() is a boolean:" . $capabilities->rotatable()); SKIP: { if (!grep /^moz_use_non_spec_compliant_pointer_origin$/, $capabilities->enumerate()) { diag("\$capabilities->moz_use_non_spec_compliant_pointer_origin is not supported for " . $capabilities->browser_version()); skip("\$capabilities->moz_use_non_spec_compliant_pointer_origin is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->moz_use_non_spec_compliant_pointer_origin() =~ /^(1|0)$/, "\$capabilities->moz_use_non_spec_compliant_pointer_origin() is a boolean:" . $capabilities->moz_use_non_spec_compliant_pointer_origin()); } SKIP: { if (!grep /^moz_accessibility_checks$/, $capabilities->enumerate()) { diag("\$capabilities->moz_accessibility_checks is not supported for " . $capabilities->browser_version()); skip("\$capabilities->moz_accessibility_checks is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->moz_accessibility_checks() =~ /^(1|0)$/, "\$capabilities->moz_accessibility_checks() is a boolean:" . $capabilities->moz_accessibility_checks()); } ok((ref $capabilities->timeouts()) eq 'Firefox::Marionette::Timeouts', "\$capabilities->timeouts() returns a Firefox::Marionette::Timeouts object"); ok($capabilities->timeouts()->page_load() =~ /^\d+$/, "\$capabilities->timeouts->page_load() is an integer:" . $capabilities->timeouts()->page_load()); ok($capabilities->timeouts()->script() =~ /^\d+$/, "\$capabilities->timeouts->script() is an integer:" . $capabilities->timeouts()->script()); ok($capabilities->timeouts()->implicit() =~ /^\d+$/, "\$capabilities->timeouts->implicit() is an integer:" . $capabilities->timeouts()->implicit()); if ($capabilities->browser_name() eq 'firefox') { ok($capabilities->browser_version() =~ /^\d+[.]\d+(?:[a]\d+)?([.]\d+)?$/, "\$capabilities->browser_version() is a major.minor.patch version number:" . $capabilities->browser_version()); } else { ok($capabilities->browser_version() =~ /^\d+[.]\d+(?:[a]\d+)?([.]\d+)?([.]\d+)?$/, "\$capabilities->browser_version() (non-firefox) is a major.minor.patch.whatever version number:" . $capabilities->browser_version()); } TODO: { local $TODO = ($major_version < 31) ? "\$capabilities->platform_version() may not exist for Firefox versions less than 31" : undef; ok(defined $capabilities->platform_version() && $capabilities->platform_version() =~ /\d+/, "\$capabilities->platform_version() contains a number:" . ($capabilities->platform_version() || '')); } ok($capabilities->moz_profile() =~ /firefox_marionette/, "\$capabilities->moz_profile() contains 'firefox_marionette':" . $capabilities->moz_profile()); SKIP: { if (!grep /^moz_webdriver_click$/, $capabilities->enumerate()) { diag("\$capabilities->moz_webdriver_click is not supported for " . $capabilities->browser_version()); skip("\$capabilities->moz_webdriver_click is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->moz_webdriver_click() =~ /^(1|0)$/, "\$capabilities->moz_webdriver_click() is a boolean:" . $capabilities->moz_webdriver_click()); } ok($capabilities->platform_name() =~ /\w+/, "\$capabilities->platform_version() contains alpha characters:" . $capabilities->platform_name()); eval { $firefox->dismiss_alert(); }; my $exception = "$@"; chomp $exception; ok($@, "Dismiss non-existant alert caused an exception to be thrown:$exception"); $count = 0; $result = undef; foreach my $path (qw(t/addons/test.xpi t/addons/discogs-search t/addons/discogs-search/manifest.json t/addons/discogs-search/)) { $count += 1; if ($major_version < 56) { if ($path =~ /discogs/) { next; } } my $install_id; my $install_path = Cwd::abs_path($path); diag("Original install path is $install_path"); if ($^O eq 'MSWin32') { $install_path =~ s/\//\\/smxg; } diag("Installing extension from $install_path"); my $temporary = 1; if ($firefox->nightly()) { $temporary = $count % 2 ? 1 : 0; } eval { $install_id = $firefox->install($install_path, $temporary); }; SKIP: { my $exception = "$@"; chomp $exception; if ((!$install_id) && ($major_version < 52)) { skip("addon:install may not be supported in firefox versions less than 52:$exception", 2); } ok($install_id, "Successfully installed an extension:$install_id"); ok($firefox->uninstall($install_id), "Successfully uninstalled an extension"); } $result = undef; $install_id = undef; $install_path = $path; diag("Original install path is $install_path"); if ($^O eq 'MSWin32') { $install_path =~ s/\//\\/smxg; } diag("Installing extension from $install_path"); eval { $install_id = $firefox->install($install_path, $temporary); }; SKIP: { my $exception = "$@"; chomp $exception; if ((!$install_id) && ($major_version < 52)) { skip("addon:install may not be supported in firefox versions less than 52:$exception", 2); } ok($install_id, "Successfully installed an extension:$install_id"); ok($firefox->uninstall($install_id), "Successfully uninstalled an extension"); } $result = undef; } eval { $result = $firefox->accept_connections(1); }; SKIP: { my $exception = "$@"; chomp $exception; if ((!$result) && ($major_version < 52)) { skip("Refusing future connections may not be supported in firefox versions less than 52:$exception", 1); } ok($result, "Accepting future connections"); $result = $firefox->accept_connections(0); ok($result, "Refusing future connections"); } ok($firefox->quit() == $correct_exit_status, "Firefox has closed with an exit status of $correct_exit_status:" . $firefox->child_error()); } SKIP: { diag("Starting new firefox for testing JSON from localhost and alerts"); ($skip_message, $firefox) = start_firefox(0, visible => 0, debug => 1, implicit => 987654); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 8); } ok($firefox, "Firefox has started in Marionette mode with visible set to 0"); my $capabilities = $firefox->capabilities(); ok((ref $capabilities) eq 'Firefox::Marionette::Capabilities', "\$firefox->capabilities() returns a Firefox::Marionette::Capabilities object"); TODO: { local $TODO = $major_version < 60 ? "\$capabilities->moz_headless() may not be available for Firefox versions less than 60" : undef; ok($capabilities->moz_headless() || $ENV{FIREFOX_VISIBLE} || 0, "\$capabilities->moz_headless() is set to " . ($ENV{FIREFOX_VISIBLE} ? 'false' : 'true')); } ok($capabilities->timeouts()->implicit() == 987654, "\$firefox->capabilities()->timeouts()->implicit() correctly reflects the implicit shortcut timeout"); my $daemon = HTTP::Daemon->new(LocalAddr => 'localhost') || die "Failed to create HTTP::Daemon"; SKIP: { if (($ENV{FIREFOX_HOST}) && ($ENV{FIREFOX_HOST} ne 'localhost')) { diag("\$capabilities->proxy is not supported for remote hosts"); skip("\$capabilities->proxy is not supported for remote hosts", 3); } elsif (($ENV{FIREFOX_HOST}) && ($ENV{FIREFOX_HOST} eq 'localhost') && ($ENV{FIREFOX_PORT})) { diag("\$capabilities->proxy is not supported for remote hosts"); skip("\$capabilities->proxy is not supported for remote hosts", 3); } elsif ((exists $Config::Config{'d_fork'}) && (defined $Config::Config{'d_fork'}) && ($Config::Config{'d_fork'} eq 'define')) { my $json_document = Encode::decode('UTF-8', '{ "id": "5", "value": "sömething"}'); my $txt_document = 'This is ordinary text'; if (my $pid = fork) { $firefox->go($daemon->url() . '?format=JSON'); ok($firefox->strip() eq $json_document, "Correctly retrieved JSON document"); diag(Encode::encode('UTF-8', $firefox->strip(), 1)); ok($firefox->json()->{id} == 5, "Correctly parsed JSON document"); ok(Encode::encode('UTF-8', $firefox->json()->{value}, 1) eq "sömething", "Correctly parsed UTF-8 JSON field"); $firefox->go($daemon->url() . '?format=txt'); ok($firefox->strip() eq $txt_document, "Correctly retrieved TXT document"); diag($firefox->strip()); while(kill 0, $pid) { kill $signals_by_name{TERM}, $pid; sleep 1; waitpid $pid, POSIX::WNOHANG(); } } elsif (defined $pid) { eval { local $SIG{ALRM} = sub { die "alarm during content server\n" }; alarm 40; $0 = "[Test HTTP Content Server for " . getppid . "]"; while (my $connection = $daemon->accept()) { diag("Accepted connection"); if (my $child = fork) { } elsif (defined $child) { eval { local $SIG{ALRM} = sub { die "alarm during content server accept\n" }; alarm 40; while (my $request = $connection->get_request()) { diag("Got request for " . $request->uri()); my ($headers, $response); if ($request->uri() =~ /format=JSON/) { $headers = HTTP::Headers->new('Content-Type', 'application/json; charset=utf-8'); $response = HTTP::Response->new(200, "OK", $headers, Encode::encode('UTF-8', $json_document, 1)); } elsif ($request->uri() =~ /format=txt/) { $headers = HTTP::Headers->new('Content-Type', 'text/plain'); $response = HTTP::Response->new(200, "OK", $headers, $txt_document); } else { $response = HTTP::Response->new(200, "OK", undef, 'hello world'); } $connection->send_response($response); if ($request->uri() =~ /format=JSON/) { last; } elsif ($request->uri() =~ /format=txt/) { last; } } $connection->close; $connection = undef; exit 0; } or do { chomp $@; diag("Caught exception in content server accept:$@"); }; exit 1; } else { diag("Failed to fork connection:$!"); die "Failed to fork:$!"; } } } or do { chomp $@; diag("Caught exception in content server:$@"); }; exit 1; } else { diag("Failed to fork http proxy:$!"); die "Failed to fork:$!"; } } else { skip("No forking available for $^O", 3); diag("No forking available for $^O"); } } my $alert_text = 'testing alert'; SKIP: { if ($major_version < 50) { skip("Firefox $major_version may hang when executing \$firefox->script(qq[alert(...)])", 2); } $firefox->script(qq[alert('$alert_text')]); ok($firefox->alert_text() eq $alert_text, "\$firefox->alert_text() correctly detects alert text"); ok($firefox->dismiss_alert(), "\$firefox->dismiss_alert() dismisses alert box"); } my $version = $capabilities->browser_version(); my ($major_version, $minor_version, $patch_version) = split /[.]/, $version; ok($firefox->async_script(qq[prompt("Please enter your name", "John Cole");]), "Started async script containing a prompt"); my $send_alert_text; eval { $send_alert_text = $firefox->await(sub { $firefox->send_alert_text("Roland Grelewicz"); }); }; SKIP: { if (($major_version < 50) && (!defined $send_alert_text)) { skip("Firefox $major_version does not appear to support the \$firefox->send_alert_text() method", 1); } ok($send_alert_text, "\$firefox->send_alert_text() sends alert text:$@"); } my $accept_dialog; eval { $accept_dialog = $firefox->accept_dialog(); }; SKIP: { if (($major_version < 50) && (!defined $accept_dialog)) { skip("Firefox $major_version does not appear to support the \$firefox->accept_dialog() method", 1); } elsif (($major_version == 78) && ($@) && ($@->isa('Firefox::Marionette::Exception::NoSuchAlert'))) { diag("Firefox $major_version has already closed the prompt:$@"); skip("Firefox $major_version has already closed the prompt", 1); } ok($accept_dialog, "\$firefox->accept_dialog() accepts the dialog box:$@"); } local $TODO = $major_version == 60 ? "Not entirely stable in firefox 60" : q[]; ok($firefox->quit() == $correct_exit_status, "Firefox has closed with an exit status of $correct_exit_status:" . $firefox->child_error()); } SKIP: { if ($ENV{RELEASE_TESTING}) { diag("Starting new firefox for testing images and links"); ($skip_message, $firefox) = start_firefox(0, visible => 0, debug => 1); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 8); } ok($firefox, "Firefox has started in Marionette mode with visible set to 0"); my $daemon = HTTP::Daemon->new(LocalAddr => 'localhost') || die "Failed to create HTTP::Daemon"; SKIP: { if (($ENV{FIREFOX_HOST}) && ($ENV{FIREFOX_HOST} ne 'localhost')) { diag("\$capabilities->proxy is not supported for remote hosts"); skip("\$capabilities->proxy is not supported for remote hosts", 3); } elsif (($ENV{FIREFOX_HOST}) && ($ENV{FIREFOX_HOST} eq 'localhost') && ($ENV{FIREFOX_PORT})) { diag("\$capabilities->proxy is not supported for remote hosts"); skip("\$capabilities->proxy is not supported for remote hosts", 3); } elsif ((exists $Config::Config{'d_fork'}) && (defined $Config::Config{'d_fork'}) && ($Config::Config{'d_fork'} eq 'define')) { if (my $pid = fork) { $firefox->go($daemon->url() . '?links_and_images'); foreach my $image ($firefox->images()) { ok($image->tag(), "Image tag is defined as " . $image->tag()); } foreach my $link ($firefox->links()) { if (defined $link->text()) { ok(defined $link->text(), "Link text is defined as " . $link->text()); } else { ok(1, "Link text is not defined"); } } while(kill 0, $pid) { kill $signals_by_name{TERM}, $pid; sleep 1; waitpid $pid, POSIX::WNOHANG(); } } elsif (defined $pid) { eval { local $SIG{ALRM} = sub { die "alarm during links and images server\n" }; alarm 40; $0 = "[Test HTTP Links and Images Server for " . getppid . "]"; while (my $connection = $daemon->accept()) { diag("Accepted connection"); if (my $child = fork) { waitpid $child, 0; } elsif (defined $child) { eval { local $SIG{ALRM} = sub { die "alarm during links and images server accept\n" }; alarm 40; if (my $request = $connection->get_request()) { diag("Got request (pid: $$) for " . $request->uri()); my ($headers, $response); if ($request->uri() =~ /image[.]png/) { $headers = HTTP::Headers->new('Content-Type', 'image/png'); $response = HTTP::Response->new(200, "OK", $headers, MIME::Base64::decode_base64("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWP48Gr6fwAIsANxwk14sgAAAABJRU5ErkJggg==")); } else { $headers = HTTP::Headers->new('Content-Type', 'text/html'); $response = HTTP::Response->new(200, "OK", $headers, 'Test
'); } $connection->send_response($response); } $connection->close; diag("Connection closed (pid: $$)"); $connection = undef; exit 0; } or do { chomp $@; diag("Caught exception in links and images server accept:$@"); }; diag("Connection error"); exit 1; } else { diag("Failed to fork connection:$!"); die "Failed to fork:$!"; } } } or do { chomp $@; diag("Caught exception in links and images server:$@"); }; exit 1; } else { diag("Failed to fork http proxy:$!"); die "Failed to fork:$!"; } } else { skip("No forking available for $^O", 3); diag("No forking available for $^O"); } } local $TODO = $major_version == 60 ? "Not entirely stable in firefox 60" : q[]; ok($firefox->quit() == $correct_exit_status, "Firefox has closed with an exit status of $correct_exit_status:" . $firefox->child_error()); } } sub display_name { my ($certificate) = @_; return $certificate->display_name() || $certificate->nickname(); } SKIP: { if ($bad_network_behaviour) { diag("Skipping proxy by argument, capabilities, window switching and certificates tests because these tests fail when metacpan connections are re-routed above"); skip("Skipping proxy by argument, capabilities, window switching and certificates tests because these tests fail when metacpan connections are re-routed above", 32); } diag("Starting new firefox for testing proxy by argument, capabilities, window switching and certificates"); my $proxy_host = 'all.example.org'; ($skip_message, $firefox) = start_firefox(1, import_profile_paths => [ 't/data/logins.json', 't/data/key4.db' ], manual_certificate_add => 1, console => 1, debug => 0, capabilities => Firefox::Marionette::Capabilities->new(moz_headless => 0, accept_insecure_certs => 0, page_load_strategy => 'none', moz_webdriver_click => 0, moz_accessibility_checks => 0, proxy => Firefox::Marionette::Proxy->new(host => $proxy_host)), timeouts => Firefox::Marionette::Timeouts->new(page_load => 78_901, script => 76_543, implicit => 34_567)); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 32); } ok($firefox, "Firefox has started in Marionette mode with definable capabilities set to different values"); my $profile_directory = $firefox->profile_directory(); ok($profile_directory, "\$firefox->profile_directory() returns $profile_directory"); my $possible_logins_path = File::Spec->catfile($profile_directory, 'logins.json'); unless ($ENV{FIREFOX_HOST}) { ok(-e $possible_logins_path, "There is a (imported) logins.json file in the profile directory"); } if ($major_version > 56) { ok(scalar $firefox->logins() == 1, "\$firefox->logins() shows the correct number (1) of records (including recent import):" . scalar $firefox->logins()); } my $capabilities = $firefox->capabilities(); ok((ref $capabilities) eq 'Firefox::Marionette::Capabilities', "\$firefox->capabilities() returns a Firefox::Marionette::Capabilities object"); ok($capabilities->timeouts()->page_load() == 78_901, "\$firefox->capabilities()->timeouts()->page_load() correctly reflects the timeouts shortcut timeout"); ok($capabilities->timeouts()->script() == 76_543, "\$firefox->capabilities()->timeouts()->script() correctly reflects the timeouts shortcut timeout"); ok($capabilities->timeouts()->implicit() == 34_567, "\$firefox->capabilities()->timeouts()->implicit() correctly reflects the timeouts shortcut timeout"); SKIP: { if (!$capabilities->proxy()) { diag("\$capabilities->proxy is not supported for " . $capabilities->browser_version()); skip("\$capabilities->proxy is not supported for " . $capabilities->browser_version(), 4); } ok($capabilities->proxy()->type() eq 'manual', "\$capabilities->proxy()->type() is 'manual'"); ok($capabilities->proxy()->http() eq "$proxy_host:80", "\$capabilities->proxy()->http() is '$proxy_host:80'"); ok($capabilities->proxy()->https() eq "$proxy_host:80", "\$capabilities->proxy()->https() is '$proxy_host:80'"); } SKIP: { if (!grep /^page_load_strategy$/, $capabilities->enumerate()) { diag("\$capabilities->page_load_strategy is not supported for " . $capabilities->browser_version()); skip("\$capabilities->page_load_strategy is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->page_load_strategy() eq 'none', "\$capabilities->page_load_strategy() is 'none'"); } SKIP: { if (!grep /^accept_insecure_certs$/, $capabilities->enumerate()) { diag("\$capabilities->accept_insecure_certs is not supported for " . $capabilities->browser_version()); skip("\$capabilities->accept_insecure_certs is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->accept_insecure_certs() == 0, "\$capabilities->accept_insecure_certs() is set to false"); } SKIP: { if (!grep /^moz_use_non_spec_compliant_pointer_origin$/, $capabilities->enumerate()) { diag("\$capabilities->moz_use_non_spec_compliant_pointer_origin is not supported for " . $capabilities->browser_version()); skip("\$capabilities->moz_use_non_spec_compliant_pointer_origin is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->moz_use_non_spec_compliant_pointer_origin() == 0, "\$capabilities->moz_use_non_spec_compliant_pointer_origin() is set to false"); } SKIP: { if (!grep /^moz_webdriver_click$/, $capabilities->enumerate()) { diag("\$capabilities->moz_webdriver_click is not supported for " . $capabilities->browser_version()); skip("\$capabilities->moz_webdriver_click is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->moz_webdriver_click() == 0, "\$capabilities->moz_webdriver_click() is set to false"); } SKIP: { if (!grep /^moz_accessibility_checks$/, $capabilities->enumerate()) { diag("\$capabilities->moz_accessibility_checks is not supported for " . $capabilities->browser_version()); skip("\$capabilities->moz_accessibility_checks is not supported for " . $capabilities->browser_version(), 1); } ok($capabilities->moz_accessibility_checks() == 0, "\$capabilities->moz_accessibility_checks() is set to false"); } SKIP: { if ($ENV{FIREFOX_HOST}) { diag("\$capabilities->headless is forced on for FIREFOX_HOST testing"); skip("\$capabilities->headless is forced on for FIREFOX_HOST testing", 1); } ok(not($capabilities->moz_headless()), "\$capabilities->moz_headless() is set to false"); } SKIP: { if ($major_version < 66) { skip("Firefox $major_version does not support \$firefox->new_window()", 15); } if ($firefox->capabilities()->browser_name() eq 'waterfox') { skip("Waterfox does not support \$firefox->new_window()", 15); } ok(scalar $firefox->window_handles() == 1, "The number of window handles is currently 1"); my ($old_window) = $firefox->window_handles(); my $new_window = $firefox->new_window(); ok(check_for_window($firefox, $new_window), "\$firefox->new_window() has created a new tab"); ok($firefox->switch_to_window($new_window), "\$firefox->switch_to_window(\$new_window) has switched focus to new tab"); ok($firefox->close_current_window_handle(), "Closed new tab"); ok(!check_for_window($firefox, $new_window), "\$firefox->new_window() has closed "); ok($firefox->switch_to_window($old_window), "\$firefox->switch_to_window(\$old_window) has switched focus to original window"); $new_window = $firefox->new_window(focus => 1, type => 'window', private => 1); ok(check_for_window($firefox, $new_window), "\$firefox->new_window() has created a new in focus, private window"); $firefox->switch_to_window($new_window); ok($firefox->close_current_window_handle(), "Closed new window"); ok(!check_for_window($firefox, $new_window), "\$firefox->new_window() has been closed"); ok($firefox->switch_to_window($old_window), "\$firefox->switch_to_window(\$old_window) has switched focus to original window"); $new_window = $firefox->new_window(focus => 0, type => 'tab'); ok(check_for_window($firefox, $new_window), "\$firefox->new_window() has created a new tab"); ok($firefox->switch_to_window($new_window), "\$firefox->switch_to_window(\$new_window) has switched focus to new tab"); ok($firefox->close_current_window_handle(), "Closed new tab"); ok(!check_for_window($firefox, $new_window), "\$firefox->new_window() has been closed"); ok(scalar $firefox->window_handles() == 1, "The number of window handles is currently 1"); $firefox->switch_to_window($old_window); } my $alert_text = 'testing alert'; SKIP: { if ($major_version < 50) { skip("Firefox $major_version may hang when executing \$firefox->script(qq[alert(...)])", 1); } $firefox->script(qq[alert('$alert_text')]); ok($firefox->accept_alert(), "\$firefox->accept_alert() accepts alert box"); } my @certificates; eval { @certificates = $firefox->certificates(); }; SKIP: { if ((scalar @certificates == 0) && ($major_version < 50)) { chomp $@; diag("\$firefox->certificates is not supported for $major_version.$minor_version.$patch_version:$@"); skip("\$firefox->certificates is not supported for $major_version.$minor_version.$patch_version", 57); } eval { $firefox->add_certificate( ) }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->add_certificate(path => \$value) throws an exception if nothing is added"); eval { $firefox->add_certificate( path => '/this/does/not/exist' ) }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->add_certificate(path => \$value) throws an exception if a non existent file is added"); eval { $firefox->add_certificate( string => 'this is nonsense' ); }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->add_certificate(string => \$value) throws an exception if nonsense is added"); my $handle = File::Temp->new( TEMPLATE => File::Spec->catfile( File::Spec->tmpdir(), 'firefox_test_part_cert_XXXXXXXXXXX')) or Firefox::Marionette::Exception->throw( "Failed to open temporary file for writing:$!"); $handle->print(<<'_CERT_') or die "Failed to write to temporary file:$!"; -----BEGIN CERTIFICATE----- MIIFsDC _CERT_ seek $handle, 0, 0 or Carp::croak("Failed to seek to start of temporary file:$!"); eval { $firefox->add_certificate( path => $handle->filename() ); }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->add_certificate(string => \$value) throws an exception if partial certificate is added"); if (defined $ca_cert_handle) { ok($firefox->add_certificate(path => $ca_cert_handle->filename(), trust => ',,,'), "Adding a certificate with no permissions"); } my $count = 0; foreach my $certificate (sort { display_name($a) cmp display_name($b) } $firefox->certificates()) { ok($certificate, "Found the " . Encode::encode('UTF-8', display_name($certificate)) . " from the certificate database"); ok($firefox->certificate_as_pem($certificate) =~ /BEGIN[ ]CERTIFICATE.*MII.*END[ ]CERTIFICATE\-+\s$/smx, Encode::encode('UTF-8', display_name($certificate)) . " looks like a PEM encoded X.509 certificate"); my $delete_class; eval { $delete_class = $firefox->delete_certificate($certificate); } or do { diag("\$firefox->delete_certificate() threw exeception:$@"); }; if (($ENV{RELEASE_TESTING}) || (defined $delete_class)) { ok(ref $delete_class eq $class, "Deleted " . Encode::encode('UTF-8', display_name($certificate)) . " from the certificate database"); } if ($certificate->is_ca_cert()) { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " is a CA cert"); } else { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " is NOT a CA cert"); } if ($certificate->is_any_cert()) { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " is any cert"); } else { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " is NOT any cert"); } if ($certificate->is_unknown_cert()) { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " is an unknown cert"); } else { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " is NOT an unknown cert"); } if ($certificate->is_built_in_root()) { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " is a built in root cert"); } else { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " is NOT a built in root cert"); } if ($certificate->is_server_cert()) { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " is a server cert"); } else { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " is NOT a server cert"); } if ($certificate->is_user_cert()) { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " is a user cert"); } else { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " is NOT a user cert"); } if ($certificate->is_email_cert()) { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " is an email cert"); } else { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " is NOT an email cert"); } ok($certificate->issuer_name(), Encode::encode('UTF-8', display_name($certificate)) . " has an issuer_name of " . Encode::encode('UTF-8', $certificate->issuer_name())); ok(defined $certificate->common_name(), Encode::encode('UTF-8', display_name($certificate)) . " has a common_name of " . Encode::encode('UTF-8', $certificate->common_name())); if (defined $certificate->email_address()) { ok($certificate->email_address(), Encode::encode('UTF-8', display_name($certificate)) . " has an email_address of " . $certificate->email_address()); } else { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " does not have a specified email_address"); } ok($certificate->sha256_subject_public_key_info_digest(), Encode::encode('UTF-8', display_name($certificate)) . " has a sha256_subject_public_key_info_digest of " . $certificate->sha256_subject_public_key_info_digest()); ok(defined $certificate->issuer_organization(), Encode::encode('UTF-8', display_name($certificate)) . " has an issuer_organization of " . Encode::encode('UTF-8', $certificate->issuer_organization())); ok($certificate->db_key(), Encode::encode('UTF-8', display_name($certificate)) . " has a db_key of " . $certificate->db_key()); ok($certificate->token_name(), Encode::encode('UTF-8', display_name($certificate)) . " has a token_name of " . Encode::encode('UTF-8', $certificate->token_name())); if (defined $certificate->sha256_fingerprint()) { ok($certificate->sha256_fingerprint(), Encode::encode('UTF-8', display_name($certificate)) . " has a sha256_fingerprint of " . $certificate->sha256_fingerprint()); } else { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " has a sha256_fingerprint of " . $certificate->sha256_fingerprint()); } ok($certificate->subject_name(), Encode::encode('UTF-8', display_name($certificate)) . " has a subject_name of " . Encode::encode('UTF-8', $certificate->subject_name())); if (defined $certificate->key_usages()) { ok(defined $certificate->key_usages(), Encode::encode('UTF-8', display_name($certificate)) . " has a key_usages of " . $certificate->key_usages()); } else { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " does not has a key_usage"); } ok(defined $certificate->issuer_organization_unit(), Encode::encode('UTF-8', display_name($certificate)) . " has an issuer_organization_unit of " . Encode::encode('UTF-8', $certificate->issuer_organization_unit())); { local $TODO = "Firefox can neglect old certificates. See https://bugzilla.mozilla.org/show_bug.cgi?id=1710716"; ok($certificate->not_valid_after() > time, Encode::encode('UTF-8', display_name($certificate)) . " has a current not_valid_after value of " . localtime $certificate->not_valid_after()); } ok($certificate->not_valid_before() < $certificate->not_valid_after(), Encode::encode('UTF-8', display_name($certificate)) . " has a not_valid_before that is before the not_valid_after value"); ok($certificate->not_valid_before() < time, Encode::encode('UTF-8', display_name($certificate)) . " has a current not_valid_before value of " . localtime $certificate->not_valid_before()); ok($certificate->serial_number(), Encode::encode('UTF-8', display_name($certificate)) . " has a serial_number of " . $certificate->serial_number()); ok(defined $certificate->issuer_common_name(), Encode::encode('UTF-8', display_name($certificate)) . " has a issuer_common_name of " . Encode::encode('UTF-8', $certificate->issuer_common_name())); ok(defined $certificate->organization(), Encode::encode('UTF-8', display_name($certificate)) . " has a organization of " . Encode::encode('UTF-8', $certificate->organization())); ok($certificate->sha1_fingerprint(), Encode::encode('UTF-8', display_name($certificate)) . " has a sha1_fingerprint of " . $certificate->sha1_fingerprint()); ok(defined $certificate->organizational_unit(), Encode::encode('UTF-8', display_name($certificate)) . " has a organizational_unit of " . Encode::encode('UTF-8', $certificate->organizational_unit())); $count += 1; } ok($count > 0, "There are $count certificates in the firefox database"); } ok($firefox->quit() == $correct_exit_status, "Firefox has closed with an exit status of $correct_exit_status:" . $firefox->child_error()); } sub check_for_window { my ($firefox, $window_handle) = @_; if (defined $window_handle) { foreach my $existing_handle ($firefox->window_handles()) { if ($major_version < 90) { if ($existing_handle == $window_handle) { return 1; } } else { if ($existing_handle eq $window_handle) { return 1; } } } } return 0; } SKIP: { diag("Starting new firefox for testing \%ENV proxy, min/maxing and killing firefox"); local %ENV = %ENV; my $localPort = 8080; $ENV{http_proxy} = 'https://localhost:' . $localPort; $ENV{https_proxy} = 'https://proxy2.example.org:4343'; $ENV{ftp_proxy} = 'ftp://ftp2.example.org:2121'; ($skip_message, $firefox) = start_firefox(1, visible => 1, width => 800, height => 600); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 15); } ok($firefox, "Firefox has started in Marionette mode with visible set to 1"); if ($firefox->xvfb_pid()) { diag("Internal old xvfb pid is " . $firefox->xvfb()); diag("Internal xvfb pid is " . $firefox->xvfb_pid()); ok($firefox->xvfb_pid(), "Internal xvfb PID is " . $firefox->xvfb_pid()); diag("Internal xvfb DISPLAY is " . $firefox->xvfb_display()); ok($firefox->xvfb_display(), "Internal xvfb DISPLAY is " . $firefox->xvfb_display()); diag("Internal xvfb XAUTHORITY is " . $firefox->xvfb_xauthority()); ok($firefox->xvfb_xauthority(), "Internal xvfb XAUTHORITY is " . $firefox->xvfb_xauthority()); } my $window_rect; eval { $window_rect = $firefox->window_rect(); }; SKIP: { if (($major_version < 50) && (!defined $window_rect)) { skip("Firefox $major_version does not appear to support the \$firefox->window_rect() method", 2); } local $TODO = $^O eq 'linux' ? '' : "Initial width/height parameters not entirely stable in $^O"; ok($window_rect->width() >= 800, "Window has a width of 800 (" . $window_rect->width() . ")"); ok($window_rect->height() >= 600, "Window has a height of 600 (" . $window_rect->height() . ")"); if (($window_rect->width() >= 800) && ($window_rect->height() >= 600)) { } else { diag("Width/Height for $^O set to 800x600, but returned " . $window_rect->width() . "x" . $window_rect->height()); } } my $capabilities = $firefox->capabilities(); ok((ref $capabilities) eq 'Firefox::Marionette::Capabilities', "\$firefox->capabilities() returns a Firefox::Marionette::Capabilities object"); ok(!$capabilities->moz_headless(), "\$capabilities->moz_headless() is set to false"); diag("Final Browser version is " . $capabilities->browser_version()); SKIP: { if (!$capabilities->proxy()) { diag("\$capabilities->proxy is not supported for " . $capabilities->browser_version()); skip("\$capabilities->proxy is not supported for " . $capabilities->browser_version(), 4); } ok($capabilities->proxy()->type() eq 'manual', "\$capabilities->proxy()->type() is 'manual'"); ok($capabilities->proxy()->http() eq 'localhost:' . $localPort, "\$capabilities->proxy()->http() is 'localhost:" . $localPort . "':" . $capabilities->proxy()->http()); ok($capabilities->proxy()->https() eq 'proxy2.example.org:4343', "\$capabilities->proxy()->https() is 'proxy2.example.org:4343'"); if ($major_version < 90) { ok($capabilities->proxy()->ftp() eq 'ftp2.example.org:2121', "\$capabilities->proxy()->ftp() is 'ftp2.example.org:2121'"); } } SKIP: { if ((exists $ENV{XAUTHORITY}) && (defined $ENV{XAUTHORITY}) && ($ENV{XAUTHORITY} =~ /xvfb/smxi)) { skip("Unable to change firefox screen size when xvfb is running", 3); } elsif ($firefox->xvfb_pid()) { skip("Unable to change firefox screen size when xvfb is running", 3); } local $TODO = "Not entirely stable in firefox"; my $full_screen; local $SIG{ALRM} = sub { die "alarm during full screen\n" }; alarm 15; eval { $full_screen = $firefox->full_screen(); } or do { diag("Crashed during \$firefox->full_screen:$@"); }; alarm 0; ok($full_screen, "\$firefox->full_screen()"); my $minimise; local $SIG{ALRM} = sub { die "alarm during minimise\n" }; alarm 15; eval { $minimise = $firefox->minimise(); } or do { diag("Crashed during \$firefox->minimise:$@"); }; alarm 0; ok($minimise, "\$firefox->minimise()"); my $maximise; local $SIG{ALRM} = sub { die "alarm during maximise\n" }; alarm 15; eval { $maximise = $firefox->maximise(); } or do { diag("Crashed during \$firefox->maximise:$@"); }; alarm 0; ok($maximise, "\$firefox->maximise()"); } if ($ENV{FIREFOX_HOST}) { SKIP: { skip("Not testing dead firefox processes with ssh", 2); } ok($firefox->quit() == $correct_exit_status, "Firefox has closed with an exit status of $correct_exit_status:" . $firefox->child_error()); } elsif (($^O eq 'MSWin32') || (!grep /^moz_process_id$/, $capabilities->enumerate())) { SKIP: { skip("Not testing dead firefox processes for win32/early firefox versions", 2); } ok($firefox->quit() == $correct_exit_status, "Firefox has closed with an exit status of $correct_exit_status:" . $firefox->child_error()); } elsif ($^O eq 'cygwin') { SKIP: { skip("Not testing dead firefox processes for cygwin", 2); } ok($firefox->quit() == $correct_exit_status, "Firefox has closed with an exit status of $correct_exit_status:" . $firefox->child_error()); } else { my $xvfb_pid = $firefox->xvfb_pid(); while($firefox->alive()) { diag("Killing PID " . $capabilities->moz_process_id() . " with a signal " . $signals_by_name{TERM}); sleep 1; kill $signals_by_name{TERM}, $capabilities->moz_process_id(); sleep 1; } eval { $firefox->go('https://metacpan.org') }; chomp $@; ok($@ =~ /Firefox[ ]killed[ ]by[ ]a[ ]TERM[ ]signal/smx, "Exception is thrown when a command is issued to a dead firefox process:$@"); eval { $firefox->go('https://metacpan.org') }; chomp $@; ok($@ =~ /Firefox[ ]killed[ ]by[ ]a[ ]TERM[ ]signal/smx, "Consistent exception is thrown when a command is issued to a dead firefox process:$@"); ok($firefox->quit() == $signals_by_name{TERM}, "Firefox has been killed by a signal with value of $signals_by_name{TERM}:" . $firefox->child_error() . ":" . $firefox->error_message()); diag("Error Message was " . $firefox->error_message()); if (defined $xvfb_pid) { ok((!(kill 0, $xvfb_pid)) && ($! == POSIX::ESRCH()), "Xvfb process $xvfb_pid has been cleaned up:$!"); } else { ok(1, "No Xvfb process exists"); } } } SKIP: { if (($^O eq 'cygwin') || ($^O eq 'darwin') || ($^O eq 'MSWin32')) { skip("Skipping exit status tests on $^O", 2); } elsif (out_of_time()) { skip("Skipping exit status b/c out of time", 2); } my $exit_status = system { $^X } $^X, (map { "-I$_" } @INC), '-MFirefox::Marionette', '-e', 'my $f = Firefox::Marionette->new(); exit 0'; ok($exit_status == 0, "Firefox::Marionette doesn't alter the exit code of the parent process if it isn't closed cleanly"); $exit_status = system { $^X } $^X, (map { "-I$_" } @INC), '-MFirefox::Marionette', '-e', 'my $f = Firefox::Marionette->new(); $f = undef; exit 0'; ok($exit_status == 0, "Firefox::Marionette doesn't alter the exit code of the parent process if it is 'undefed'"); if ($ENV{RELEASE_TESTING}) { if ($ENV{FIREFOX_HOST}) { my $user = getpwuid($>);; my $host = $ENV{FIREFOX_HOST}; if ($ENV{FIREFOX_USER}) { $user = $ENV{FIREFOX_USER}; } elsif (($ENV{FIREFOX_HOST} eq 'localhost') && (!$ENV{FIREFOX_PORT})) { $user = 'firefox'; } my $handle = File::Temp->new( TEMPLATE => File::Spec->catfile( File::Spec->tmpdir(), 'firefox_test_ssh_local_directory_XXXXXXXXXXX')) or Firefox::Marionette::Exception->throw( "Failed to open temporary file for writing:$!"); fcntl $handle, Fcntl::F_SETFD(), 0 or Carp::croak("Can't clear close-on-exec flag on temporary file:$!"); my $handle_fileno = fileno $handle; my $command = join q[ ], $^X, (map { "-I$_" } @INC), '-MFirefox::Marionette', '-e', q['open(my $fh, ">&=", ] . $handle_fileno . q[) or die "OPEN:$!"; $f = Firefox::Marionette->new( user => "] . $user . q[", host => "] . $host . q["); $fh->print($f->ssh_local_directory()) or die "PRINT:$!"; close($fh) or die "CLOSE:$!";']; my $output = `$command`; $handle->seek(0,0) or die "Failed to seek on temporary file:$!"; my $result = read($handle, my $directory, 2048) or die "Failed to read from temporary file:$!"; ok(!-d $directory, "Firefox::Marionette->new() cleans up the ssh local directory at $directory"); } else { my $command = join q[ ], $^X, (map { "-I$_" } @INC), '-MFirefox::Marionette', '-e', q['$f = Firefox::Marionette->new(); print $f->root_directory();']; my $directory = `$command`; ok(!-d $directory, "Firefox::Marionette->new() cleans up the local directory at $directory"); } } } ok($at_least_one_success, "At least one firefox start worked"); eval "no warnings; sub File::Temp::newdir { \$! = POSIX::EACCES(); return; } use warnings;"; ok(!$@, "File::Temp::newdir is redefined to fail:$@"); eval { $class->new(); }; my $output = "$@"; chomp $output; ok($@->isa('Firefox::Marionette::Exception'), "When File::Temp::newdir is forced to fail, a Firefox::Marionette::Exception is thrown:$output"); done_testing(); Firefox-Marionette-1.22/Changes0000644000175000017500000003640014175143706015073 0ustar davedaveRevision history for Firefox-Marionette 1.22 Sat Jan 29 15:49 2022 Fixes to cygwin/Win32 support 1.21 Sat Jan 29 10:43 2022 Fixes to cygwin/Win32 support, test suite, startup time, script/async_script methods 1.20 Mon Jan 24 19:55 2022 Improving documentation 1.19 Sun Jan 23 07:24 2022 Fixing MANIFEST file 1.18 Sun Jan 23 07:09 2022 Adding the shadow_root, shadowy, logins_from_csv and logins_from_zip methods Change to allow directly returning Firefox::Marionette::Element elements from script calls Clear user and password fields before typing in them for the fill_login method Adding page_ranges parameter for pdf method Adding 1Password support for firefox-passwords Adding --check-only support to firefox-passwords when importing passwords 1.17 Mon Jan 3 10:30 2022 Fixes for tmp directory cleanups for Firefox::Marionette objects when defined as globals. Thanks to eserte. Documentation for firefox workaround for downloading via the go method. Thanks to aluaces. Adding the debug method Fixes to image/links objects firefox-passwords can now just print the password (with --password) option 1.16 Sun Oct 31 17:46 2021 Fixes to update for Firefox 94. Thanks to prozorecJP. Small fixes for Win32 CPAN Tester failures. 1.15 Sun Oct 31 17:46 2021 Updates for Firefox 94 1.14 Wed Oct 27 20:45 2021 Improving links method to return a Links object Adding images method Improving inheritance support with Scalar::Util 1.13 Sun Oct 17 20:45 2021 Adding devtools, kiosk parameters to the new method Adding links method more debug for check-firefox-certificate-authorities 1.12 Sat Aug 7 09:30 2021 Fixing application.ini support diag improvements for tests small changes to update method. 1.11 Sat Jul 31 08:15:00 2021 Improving support for update method. Allow firefox-passwords to modify passwords Changes to mouse_move implementation 1.10 Sun Jul 11 08:15:00 2021 Fixing MANIFEST to include UpdateStatus.pm 1.09 Sun Jul 11 08:00:00 2021 Adding update/restart methods to allow Firefox patching. Improving sub-classing support and Waterfox support. Adding check-firefox-certificate-authorities Fixes for using profile in GH#10. Thanks to prozorecJP. Fixes to EXE_FILES in GH#11. Thanks to bokutin. 1.08 Fri Jun 18 21:00:00 2021 Fixes to tests after HTML changes in metacpan.org and CPAN Tester failures 1.07 Sat Jun 12 21:38:00 2021 Adding support for the Firefox Password Manager Fixes to tests for Firefox 89. 1.06 Sat May 22 17:14:00 2021 Adding support for the Firefox certificate database Fixes to loading profiles in GH#8. Thanks to stuart-little. Increasing default window size to allow older firefoxes to pass test suite Adding perform/release methods for fine grained control of user input in GH#9. Thanks to stuart-little. 1.05 Thu May 6 22:15:00 2021 Fixing UTF-8 handling for strip/json methods. 1.04 Wed May 5 06:42:00 2021 Fixing bugs GH#2 to GH#6. Thanks to eserte. Adding xvfb_display and xvfb_authority methods Adding has_* methods Improving documentation Replacing xvfb with xvfb_pid method (xvfb method is deprecated) 1.03 Sat Apr 24 10:38:00 2021 Cleanups to Makefile including github changes. Changes for CPAN Testers in test suite 1.02 Thu Apr 22 19:48:00 2021 Fixes to tests for Firefox 88. Adjustments for github issue tracking. 1.01 Wed Jan 20 18:47:00 2021 Fixes to Makefile.PL for Ubuntu to fix GH#1. Thanks to rai-gaurav. Fixes to add_header for Firefox 84.0.1 Improving documentation 1.00 Sat Dec 5 21:00:00 2020 Added README.md Added github reference Adding add_header and delete_header methods. Adding add_site_header and delete_site_header methods. Adding application.ini support to fix RT#133427. 0.99 Sat Oct 10 08:22:00 2020 Correcting searching Path for firefox on Win32/cygwin. Adding support for 32 bit Firefox on Win64. Adding some support for other gecko based browsers, adding support for Firefox 80. Reworked ssh support, added nightly, developer keys to new method, added same_site support for cookies. 0.98 Tue Apr 14 07:27:00 2020 Additions to chatty option. 0.97 Tue Apr 14 07:01:00 2020 Adding reconnect parameter to new, private parameter to new_window. 0.96 Mon Mar 9 18:14:00 2020 Improved cleanups and added chatty/seer options to new method. 0.95 Thu Feb 27 08:56:00 2020 Increasing PDF::API2 requirement, moving har testing into RELEASE_TESTING only. 0.94 Tue Feb 25 17:09:00 2020 Fix a profile bug preventing remote downloads. 0.93 Sun Feb 23 20:22:00 2020 Allow install method to package source code directories. 0.92 Thu Jan 28 06:44:00 2020 Documentation fixes. 0.91 Tue Jan 28 22:06:00 2020 Conditionally clearing HOME environment variable to address RT#131304. Adding to default profile. Initial support for Print command 0.90 Sat Jan 18 15:26:00 2020 Clear HOME environment variable for tests in an attempt to fix RT#131304. 0.89 Wed Dec 25 07:01:00 2019 Fixing test suite for Perl 5.8. 0.88 Tue Dec 24 17:08:00 2019 Correcting exit status for parent process. Thanks to Tomohiro Hosaka for the bug report in RT#131227. 0.87 Sat Dec 14 16:16:00 2019 Removing PIPE handler. 0.86 Sat Dec 14 14:04:00 2019 Stopping a crash due to a PIPE signal. Thanks to John Denker for the bug report in RT#131173. 0.85 Wed Nov 12 18:40:00 2019 Fixing session cookie support. Thanks to BOKUTIN in RT#130955. 0.84 Tue Nov 5 not sure 2019 Allowing esr in version string b/c of debian breakage. Thanks to QUATTRO in RT#130889. Removed mention of highlight parameter in selfie after Firefox 70 dropped support for it. 0.83 Tue Oct 10 21:48:00 2019 Added proxy host parameter. 0.82 Mon Sep 4 21:15:00 2019 Fixes for HAR (HTTP Archive) files. Added experimental support for adding CAs using certutil. 0.81 Sun Aug 11 20:22:00 2019 Improved ssh support for remote firefox, including addons and downloads. Added experimental support for HAR (HTTP Archive) files. 0.80 Sat Aug 3 16:57:00 2019 Added survive, user and host parameters to new method. 0.79 Thu Aug 1 06:32:00 2019 Adding remote support for linux/bsd. Adding window width/height parameters for initial firefox window. Improving documentation. Fixing delete_session bug in RT#130236. 0.78 Thu Jun 11 17:18:00 2019 Adding support for Firefox 68. Adding strict_file_interactability, unhandled_prompt_behavior, set_window_rect and moz_shutdown_timeout to capabilities. Added the raw parameter for the selfie method 0.77 Sun Jun 7 21:34:00 2019 Adding support for insecure certificate exceptions. 0.76 Sat Jun 29 19:22:00 2019 Fixes to cygwin test suite. Fixes to cope with dbus crashes in RHEL6. 0.75 Sat Jun 29 11:36:00 2019 Set minimum version of IPC::Open3 after CPAN Tester issues. 0.74 Sat Jun 29 08:01:00 2019 Adding cygwin support. Dropped IPC::Run in favour of IPC::Open3. 0.73 Thu Jun 27 06:58:00 2019 Fixing test suite. 0.72 Wed Jun 26 19:37:00 2019 Adding moz_build_id in capabilities. Cleanup socket close. 0.71 Fri Mar 29 20:12:00 2019 Fixing new_window tests for only Firefox 66+ 0.70 Thu Mar 28 20:29:00 2019 Adding support for new_window, accept_alert. Fixing window_type. Deprecating accept_dialog 0.69 Mon Mar 25 19:6:00 2019 Including support for Firefox 24 and Firefox 66 0.68 Thu Feb 14 21:08:00 2018 Adding support for Firefox 64. Required sandboxing window.find in scripts 0.67 Sun Oct 28 16:14:00 2018 Adding support for Firefox 63 0.66 Mon Sep 10 20:32:00 2018 Improving synchronisation of commands for old and new marionette protocols 0.65 Sun Sep 9 20:58:00 2018 Dropped the minimum acceptable firefox version to 31.8.0esr for solaris. Accepting pre version 3 marionette. 0.64 Fri Aug 31 20:12:00 2018 Removed unnecessary debug statements from the script method 0.63 Fri Aug 31 06:51:00 2018 Fixed Win32 tests. Updated script parameters to match current firefox script parameters. 0.62 Thu Aug 30 06:35:00 2018 Corrected documentation 0.61 Wed Aug 29 06:50:00 2018 Forcing HTTP::Daemon to use LocalAddr of 'localhost' to stop CPAN Tester errors 0.60 Tue Aug 28 21:10:00 2018 Adding json and strip methods 0.59 Thu Aug 23 06:48:00 2018 Provided direct shortcuts to timeout parameters for the new method. 0.58 Sat Aug 18 14:56:00 2018 Fixed timeouts parameters for new. Thanks to Alexander Welsch for the bug report. 0.57 Sat Jun 30 11:04:00 2018 Corrected downloads to work with Firefix 61 0.56 Fri Jun 15 22:02:00 2018 Defining Win32 Connection Refused error code during initial startup 0.55 Fri Jun 15 18:43:00 2018 Allowing test suite to run with an existing proxy, handling bad window width on darwin, other test fixes 0.54 Mon Jun 11 13:30:00 2018 Coping with the little changes for Firefox 60 0.53 Fri Mar 15 19:49:00 2018 Fixed test suite. 0.52 Thu Mar 15 21:28:00 2018 Reduced the minimum acceptable firefox version to 50. Adding enumerate, moz_use_non_spec_compliant_pointer_origin methods to Capabilities. 0.51 Sat Mar 10 15:44:00 2018 Adding loaded and interactive methods. 0.50 Sun Mar 4 20:07:00 2018 Adding test suite alarm for download test. 0.49 Sun Mar 4 16:13:00 2018 Adding test suite timeout checking for 80 seconds. More code coverage improvements. 0.48 Fri Mar 2 22:44:00 2018 Adding proxy environment variables. 0.47 Wed Feb 28 22:57:00 2018 Code clean up. 0.46 Wed Feb 28 22:19:00 2018 Adding bye and mime_types methods. 0.45 Tue Feb 27 18:45:00 2018 Adding await method. 0.44 Mon Feb 26 21:19:00 2018 s/find_by/find/g. Adding download support. 0.43 Thu Feb 22 20:48:00 2018 Exception handling fixes. 0.42 Mon Feb 19 22:29:00 2018 Documentation/test coverage fixes. 0.41 Mon Feb 19 00:38:00 2018 Adding proxy support. 0.40 Thu Feb 15 21:32:00 2018 Merge find/list queries. 0.39 Web Feb 14 19:25:00 2018 Allow nested find/list queries. 0.38 Sun Feb 11 07:08:00 2018 More test fixes for Firefox 52.6 on darwin. 0.37 Sat Feb 10 08:57:00 2018 Test fixes for Firefox 52.6 on darwin. 0.36 Fri Feb 9 07:02:00 2018 Correcting Win32/cygwin/darwin dependencies. 0.35 Thu Feb 8 19:07:00 2018 Adding support for Dragonfly BSD. More exit 11 detection. 0.34 Web Feb 7 07:03:00 2018 Adding support for RHEL7/RHEL6/jessie. Reduced the minimum acceptable firefox version to 52 0.33 Sat Feb 3 16:53:00 2018 Adding support for NetBSD. Reduced the minimum acceptable firefox version to 55 Added support for older Marionette protocol commands 0.32 Fri Feb 2 19:38:00 2018 Adding support for OpenBSD, reduced the minimum acceptable firefox version to 56. Requirement/test suite cleanups. 0.31 Tue Jan 30 05:55:00 2018 Deprecating page_source (for html), find_element (find), find_elements (list) and send_keys (type). Removed driver.js from the MANIFEST. Adding virtual memory detection in Makefile.PL. 0.30 Tue Jan 30 05:55:00 2018 Coping with Ubuntu implementating rpm. 0.29 Mon Jan 29 20:21:00 2018 Adding network capture into debug output. Added delete_session method. 0.28 Mon Jan 29 05:36:00 2018 Cleanup for the test suite. Adding freebsd signal 11 detection and max/full/min alarm/TODO. 0.27 Sun Jan 28 22:07:00 2018 Cleanup for the test suite. Rolling 11 exit detection throughout test suite. 0.26 Sun Jan 28 19:48:00 2018 Cleanup for the test suite. 11 exit code not caused by low memory SEGV faults. 0.25 Sun Jan 28 15:30:00 2018 Checking for firefox exiting with an 11 error code in in test suite 0.24 Sat Jan 27 21:16:00 2018 Cleanup of the cross-platform code 0.23 Fri Jan 26 21:27:00 2018 Not running Xvfb at all unless moz_headless has been set to false or the visible parameter has been set to true 0.22 Fri Jan 26 21:07:00 2018 Coping with a unix environment with no X11 available. Tested on freebsd. 0.21 Fri Jan 26 19:11:00 2018 Another attempt at fixing test failures. 0.20 Fri Jan 26 14:36:00 2018 Adding property and documenting property vs attribute methods. Testing cygwin support. Adding support for Firefox 58. 0.19 Thu Jan 25 02:34:00 2018 Using headless as the default launch mode. Adding the visible parameter to Firefox::Marionette->new (defaults to 0). Changed method name of Firefox::Marionette::Window::Rect->state to wstate. 0.18 Wed Jan 24 06:18:00 2018 Corrected MANIFEST to include t/addons/test.xpi. 0.17 Tue Jan 23 22:13:00 2018 Added methods for installing/uninstalling addons. More debugging for test failures. 0.16 Sun Jan 21 20:19:00 2018 Added support for -safe-mode as a firefox argument via the addons argument. 0.15 Sat Jan 20 22:23:00 2018 Fixed documentation. 0.14 Sat Jan 20 22:01:00 2018 Improved the test suite to cope with high load averages on test machines. Added support for closing a tab/window. 0.13 Sat Jan 20 15:22:00 2018 Added additional dependencies to Makefile.PL. 0.12 Sat Jan 20 15:12:00 2018 Removing methods to update the profile while firefox is running until a method that works is discovered. Added debug parameter to cleanup firefox STDERR unless required. Building Xvfb support into the module instead of just including it in the test suite. 0.11 Fri Jan 19 21:14:00 2018 Improving Profile support. Adding more debugging for 'X_GetImage: BadMatch' exceptions. 0.10 Wed Jan 17 06:30:00 2018 Catching negative window positions in test suite. Adding Profile support. 0.09 Mon Jan 15 21:30:00 2018 Improved handling of 'X_GetImage: BadMatch' exceptions in test suite. 0.08 Mon Jan 15 21:02:00 2018 Cleaned up documentation. Added support for is_displayed, is_enabled, is_selected, window_rect. Removed locally patched Test::NeedsDisplay. Added TODO support in tests for 'X_GetImage: BadMatch' exceptions for screenshots. Thanks to SREZIC for assistance in RT#12407. 0.07 Sun Jan 14 20:43:00 2018 Included locally patched Test::NeedsDisplay in MANIFEST. 0.06 Sun Jan 14 17:59:00 2018 Adding support for MacOS (darwin) and custom firefox binaries. 0.05 Sun Jan 14 09:15:00 2018 Adding locally patched Test::NeedsDisplay to get around screenshot test failures. 0.04 Sat Jan 13 19:50:00 2018 Adding Test::NeedsDisplay as a build pre-requisite for non Win32 platforms. 0.03 Sat Jan 13 18:04:00 2018 Added element and css methods. 0.02 Sat Jan 13 16:48:00 2018 Updated documentation, removed Build.PL, added LICENSE key to Makefile.PL. 0.01 Sat Jan 6 17:28:44 2018 Initial release. Firefox-Marionette-1.22/LICENSE0000644000175000017500000004702014175143706014605 0ustar davedaveThis is free software; you can redistribute it and/or modify it under the same terms as the Perl5 (v5.0.0 ~ v5.20.0) programming language system itself: under the terms of either: a) the "Artistic License 1.0" as published by The Perl Foundation http://www.perlfoundation.org/artistic_license_1_0 b) the GNU General Public License as published by the Free Software Foundation; either version 1 http://www.gnu.org/licenses/gpl-1.0.html or (at your option) any later version PLEASE NOTE: It is the current maintainers intention to keep the dual licensing intact. Until this notice is removed, releases will continue to be available under both the standard GPL and the less restrictive Artistic licenses. Verbatim copies of both licenses are included below: --- The Artistic License 1.0 --- The "Artistic License" Preamble The intent of this document is to state the conditions under which a Package may be copied, such that the Copyright Holder maintains some semblance of artistic control over the development of the package, while giving the users of the package the right to use and distribute the Package in a more-or-less customary fashion, plus the right to make reasonable modifications. Definitions: "Package" refers to the collection of files distributed by the Copyright Holder, and derivatives of that collection of files created through textual modification. "Standard Version" refers to such a Package if it has not been modified, or has been modified in accordance with the wishes of the Copyright Holder as specified below. "Copyright Holder" is whoever is named in the copyright or copyrights for the package. "You" is you, if you're thinking about copying or distributing this Package. "Reasonable copying fee" is whatever you can justify on the basis of media cost, duplication charges, time of people involved, and so on. (You will not be required to justify it to the Copyright Holder, but only to the computing community at large as a market that must bear the fee.) "Freely Available" means that no fee is charged for the item itself, though there may be fees involved in handling the item. It also means that recipients of the item may redistribute it under the same conditions they received it. 1. You may make and give away verbatim copies of the source form of the Standard Version of this Package without restriction, provided that you duplicate all of the original copyright notices and associated disclaimers. 2. You may apply bug fixes, portability fixes and other modifications derived from the Public Domain or from the Copyright Holder. A Package modified in such a way shall still be considered the Standard Version. 3. You may otherwise modify your copy of this Package in any way, provided that you insert a prominent notice in each changed file stating how and when you changed that file, and provided that you do at least ONE of the following: a) place your modifications in the Public Domain or otherwise make them Freely Available, such as by posting said modifications to Usenet or an equivalent medium, or placing the modifications on a major archive site such as uunet.uu.net, or by allowing the Copyright Holder to include your modifications in the Standard Version of the Package. b) use the modified Package only within your corporation or organization. c) rename any non-standard executables so the names do not conflict with standard executables, which must also be provided, and provide a separate manual page for each non-standard executable that clearly documents how it differs from the Standard Version. d) make other distribution arrangements with the Copyright Holder. 4. You may distribute the programs of this Package in object code or executable form, provided that you do at least ONE of the following: a) distribute a Standard Version of the executables and library files, together with instructions (in the manual page or equivalent) on where to get the Standard Version. b) accompany the distribution with the machine-readable source of the Package with your modifications. c) give non-standard executables non-standard names, and clearly document the differences in manual pages (or equivalent), together with instructions on where to get the Standard Version. d) make other distribution arrangements with the Copyright Holder. 5. You may charge a reasonable copying fee for any distribution of this Package. You may charge any fee you choose for support of this Package. You may not charge a fee for this Package itself. However, you may distribute this Package in aggregate with other (possibly commercial) programs as part of a larger (possibly commercial) software distribution provided that you do not advertise this Package as a product of your own. You may embed this Package's interpreter within an executable of yours (by linking); this shall be construed as a mere form of aggregation, provided that the complete Standard Version of the interpreter is so embedded. 6. The scripts and library files supplied as input to or produced as output from the programs of this Package do not automatically fall under the copyright of this Package, but belong to whoever generated them, and may be sold commercially, and may be aggregated with this Package. If such scripts or library files are aggregated with this Package via the so-called "undump" or "unexec" methods of producing a binary executable image, then distribution of such an image shall neither be construed as a distribution of this Package nor shall it fall under the restrictions of Paragraphs 3 and 4, provided that you do not represent such an executable image as a Standard Version of this Package. 7. C subroutines (or comparably compiled subroutines in other languages) supplied by you and linked into this Package in order to emulate subroutines and variables of the language defined by this Package shall not be considered part of this Package, but are the equivalent of input as in Paragraph 6, provided these subroutines do not change the language in any way that would cause it to fail the regression tests for the language. 8. Aggregation of this Package with a commercial distribution is always permitted provided that the use of this Package is embedded; that is, when no overt attempt is made to make this Package's interfaces visible to the end user of the commercial distribution. Such use shall not be construed as a distribution of this Package. 9. The name of the Copyright Holder may not be used to endorse or promote products derived from this software without specific prior written permission. 10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. --- end of The Artistic License 1.0 --- --- The GNU General Public License, Version 1, February 1989 --- GNU GENERAL PUBLIC LICENSE Version 1, February 1989 Copyright (C) 1989 Free Software Foundation, Inc. 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The license agreements of most software companies try to keep users at the mercy of those companies. By contrast, our General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. The General Public License applies to the Free Software Foundation's software and to any other program whose authors commit to using it. You can use it for your programs, too. When we speak of free software, we are referring to freedom, not price. Specifically, the General Public License is designed to make sure that you have the freedom to give away or sell copies of free software, that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of a such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must tell them their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any work containing the Program or a portion of it, either verbatim or with modifications. Each licensee is addressed as "you". 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this General Public License and to the absence of any warranty; and give any other recipients of the Program a copy of this General Public License along with the Program. You may charge a fee for the physical act of transferring a copy. 2. You may modify your copy or copies of the Program or any portion of it, and copy and distribute such modifications under the terms of Paragraph 1 above, provided that you also do the following: a) cause the modified files to carry prominent notices stating that you changed the files and the date of any change; and b) cause the whole of any work that you distribute or publish, that in whole or in part contains the Program or any part thereof, either with or without modifications, to be licensed at no charge to all third parties under the terms of this General Public License (except that you may choose to grant warranty protection to some or all third parties, at your option). c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the simplest and most usual way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this General Public License. d) You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. Mere aggregation of another independent work with the Program (or its derivative) on a volume of a storage or distribution medium does not bring the other work under the scope of these terms. 3. You may copy and distribute the Program (or a portion or derivative of it, under Paragraph 2) in object code or executable form under the terms of Paragraphs 1 and 2 above provided that you also do one of the following: a) accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Paragraphs 1 and 2 above; or, b) accompany it with a written offer, valid for at least three years, to give any third party free (except for a nominal charge for the cost of distribution) a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Paragraphs 1 and 2 above; or, c) accompany it with the information you received as to where the corresponding source code may be obtained. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form alone.) Source code for a work means the preferred form of the work for making modifications to it. For an executable file, complete source code means all the source code for all modules it contains; but, as a special exception, it need not include source code for modules which are standard libraries that accompany the operating system on which the executable file runs, or for standard header files or definitions files that accompany that operating system. 4. You may not copy, modify, sublicense, distribute or transfer the Program except as expressly provided under this General Public License. Any attempt otherwise to copy, modify, sublicense, distribute or transfer the Program is void, and will automatically terminate your rights to use the Program under this License. However, parties who have received copies, or rights to use copies, from you under this General Public License will not have their licenses terminated so long as such parties remain in full compliance. 5. By copying, distributing or modifying the Program (or any work based on the Program) you indicate your acceptance of this license to do so, and all its terms and conditions. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. 7. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of the license which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the license, you may choose any version ever published by the Free Software Foundation. 8. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS Appendix: How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to humanity, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 19yy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) 19xx name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (a program to direct compilers to make passes at assemblers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice That's all there is to it! --- end of The GNU General Public License, Version 1, February 1989 --- Firefox-Marionette-1.22/ca-bundle-for-firefox0000755000175000017500000001153114175143706017602 0ustar davedave#! /usr/bin/perl use strict; use warnings; use Getopt::Long(); use English qw( -no_match_vars ); use Firefox::Marionette(); use FileHandle(); use Encode(); our $VERSION = '1.22'; my %options; Getopt::Long::GetOptions( \%options, 'help', 'version', 'binary:s', 'file:s' ); if ( $options{help} ) { require Pod::Simple::Text; my $parser = Pod::Simple::Text->new(); $parser->parse_from_file($PROGRAM_NAME); exit 0; } elsif ( $options{version} ) { print "$VERSION\n" or die "Failed to print to STDOUT:$EXTENDED_OS_ERROR\n"; exit 0; } my %parameters; if ( $options{binary} ) { $parameters{binary} = $options{binary}; } my $firefox = Firefox::Marionette->new(%parameters); my $handle = *{STDOUT}; my $output_name = 'STDOUT'; if ( $options{file} ) { $handle = FileHandle->new( $options{file}, Fcntl::O_CREAT() | Fcntl::O_WRONLY() | Fcntl::O_EXCL(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR() | Fcntl::S_IRGRP() | Fcntl::S_IROTH() ) or die "Failed to open $options{file} for writing:$EXTENDED_OS_ERROR\n"; $output_name = $options{file}; } foreach my $certificate ( sort { $a->display_name() cmp $b->display_name } $firefox->certificates() ) { if ( $certificate->is_ca_cert() ) { my $output_line = q[# ] . $certificate->display_name() . "\n" . $firefox->certificate_as_pem($certificate) . "\n"; $handle->print( Encode::encode( 'UTF-8', $output_line, 1 ) ) or die "Failed to print to $output_name:$EXTENDED_OS_ERROR\n"; } } if ( $options{file} ) { $handle->close() or die "Failed to close $output_name:$EXTENDED_OS_ERROR\n"; } __END__ =head1 NAME ca-bundle-for-firefox - generate the ca-bundle.crt for the current firefox instance =head1 VERSION Version 1.22 =head1 USAGE $ ca-bundle-for-firefox >/etc/pki/tls/certs/ca-bundle.crt $ ca-bundle-for-firefox --file current.crt $ ca-bundle-for-firefox --binary=/path/to/old/firefox --file old.crt $ diff -Naur old.crt current.crt =head1 DESCRIPTION This program is intended to generate the ca-bundle.crt file from the Certificate Authorities maintained in firefox. By default, the only firefox version that may be used will be present in the PATH environment variable. However, the user may specify a different path with the --binary parameter. =head1 REQUIRED ARGUMENTS None =head1 OPTIONS Option names can be abbreviated to uniqueness and can be stated with singe or double dashes, and option values can be separated from the option name by a space or '=' (as with Getopt::Long). Option names are also case- sensitive. =over 4 =item * --help - This page. =item * --binary - Use this firefox binary instead of the default firefox instance =item * --file - Write the Certificate Authority bundle out to this file =back =head1 CONFIGURATION ca-bundle-for-firefox requires no configuration files or environment variables. =head1 DEPENDENCIES ca-bundle-for-firefox requires the following non-core Perl modules =over =item * L =back =head1 DIAGNOSTICS None. =head1 INCOMPATIBILITIES None known. =head1 EXIT STATUS This program will exit with a zero after successfully completing. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/META.json0000664000175000017500000000467514175144502015227 0ustar davedave{ "abstract" : "Automate the Firefox browser with the Marionette protocol", "author" : [ "David Dick " ], "dynamic_config" : 1, "generated_by" : "ExtUtils::MakeMaker version 7.64, CPAN::Meta::Converter version 2.150010", "license" : [ "perl_5" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : 2 }, "name" : "Firefox-Marionette", "no_index" : { "directory" : [ "t", "inc" ] }, "prereqs" : { "build" : { "requires" : { "Compress::Zlib" : "0", "Cwd" : "0", "Digest::SHA" : "0", "File::HomeDir" : "0", "HTTP::Daemon" : "0", "HTTP::Response" : "0", "HTTP::Status" : "0", "IO::Socket::SSL" : "0", "PDF::API2" : "2.036", "Test::More" : "0" } }, "configure" : { "requires" : { "ExtUtils::MakeMaker" : "0" } }, "runtime" : { "requires" : { "Archive::Zip" : "0", "Config" : "0", "Config::INI::Reader" : "0", "Crypt::URandom" : "0", "DirHandle" : "0", "Encode" : "0", "English" : "0", "Exporter" : "0", "Fcntl" : "0", "File::Find" : "0", "File::Spec" : "0", "File::Temp" : "0", "FileHandle" : "0", "IPC::Open3" : "1.03", "JSON" : "0", "MIME::Base64" : "0", "POSIX" : "0", "Pod::Simple::Text" : "0", "Scalar::Util" : "0", "Socket" : "0", "Term::ReadKey" : "0", "Text::CSV_XS" : "0", "Time::HiRes" : "0", "URI" : "0", "URI::Escape" : "0", "URI::URL" : "0", "XML::Parser" : "0", "base" : "0", "overload" : "0", "perl" : "5.006" } } }, "release_status" : "stable", "resources" : { "bugtracker" : { "web" : "https://github.com/david-dick/firefox-marionette/issues" }, "repository" : { "type" : "git", "url" : "https://github.com/david-dick/firefox-marionette", "web" : "https://github.com/david-dick/firefox-marionette" } }, "version" : "1.22", "x_serialization_backend" : "JSON::PP version 4.06" } Firefox-Marionette-1.22/MANIFEST0000644000175000017500000000326414175143706014733 0ustar davedaveChanges lib/Firefox/Marionette.pm lib/Firefox/Marionette/Buttons.pm lib/Firefox/Marionette/Capabilities.pm lib/Firefox/Marionette/Certificate.pm lib/Firefox/Marionette/Cookie.pm lib/Firefox/Marionette/Extension/HarExportTrigger.pm lib/Firefox/Marionette/Image.pm lib/Firefox/Marionette/Keys.pm lib/Firefox/Marionette/Link.pm lib/Firefox/Marionette/Login.pm lib/Firefox/Marionette/Profile.pm lib/Firefox/Marionette/Proxy.pm lib/Firefox/Marionette/ShadowRoot.pm lib/Firefox/Marionette/Window/Rect.pm lib/Firefox/Marionette/Element.pm lib/Firefox/Marionette/Element/Rect.pm lib/Firefox/Marionette/Exception.pm lib/Firefox/Marionette/Exception/Response.pm lib/Firefox/Marionette/Exception/InsecureCertificate.pm lib/Firefox/Marionette/Exception/NotFound.pm lib/Firefox/Marionette/Exception/StaleElement.pm lib/Firefox/Marionette/Exception/NoSuchAlert.pm lib/Firefox/Marionette/Response.pm lib/Firefox/Marionette/Timeouts.pm lib/Firefox/Marionette/UpdateStatus.pm lib/Waterfox/Marionette.pm lib/Waterfox/Marionette/Profile.pm Makefile.PL MANIFEST This list of files LICENSE README ca-bundle-for-firefox check-firefox-certificate-authorities ssh-auth-cmd-marionette firefox-passwords t/author/bulk_test.pl t/data/elements.html t/data/iframe.html t/data/logins.json t/data/key4.db t/data/1Passwordv7.csv t/data/1Passwordv8.1pux t/data/bitwarden_export_org.csv t/data/keepass.csv t/data/last_pass_example.csv t/00.load.t t/01-marionette.t t/pod-coverage.t t/pod.t t/addons/test.xpi t/addons/discogs-search/manifest.json t/addons/discogs-search/README.md META.yml Module YAML meta-data (added by MakeMaker) META.json Module JSON meta-data (added by MakeMaker) Firefox-Marionette-1.22/Makefile.PL0000644000175000017500000003340414175143706015553 0ustar davedaveuse strict; use warnings; use ExtUtils::MakeMaker; use File::Spec(); use Fcntl(); use English qw( -no_match_vars ); sub _win32_registry_query_key { my (%parameters) = @_; my $binary = 'reg'; my @parameters = ( 'query', q["] . ( join q[\\], @{ $parameters{subkey} } ) . q["] ); if ( $parameters{name} ) { push @parameters, ( '/v', q["] . $parameters{name} . q["] ); } my @values; my $command = join q[ ], $binary, @parameters; my $reg_query = `$command 2>nul`; if ( defined $reg_query ) { foreach my $line ( split /\r?\n/smx, $reg_query ) { if ( defined $parameters{name} ) { my $name = $parameters{name} eq q[] ? '(Default)' : $parameters{name}; my $quoted_name = quotemeta $name; if ( $line =~ /^[ ]+${quoted_name}[ ]+(?:REG_SZ)[ ]+(\S.*\S)\s*$/smx ) { push @values, $1; } } else { push @values, $line; } } } return @values; } sub _cygwin_reg_query_value { my ($path) = @_; sysopen my $handle, $path, Fcntl::O_RDONLY(); my $value; if ( defined $handle ) { no warnings; while ( read $handle, my $buffer, 1 ) { $value .= $buffer; } use warnings; if ( defined $value ) { $value =~ s/\0$//smx; } } return $value; } if ( ( $OSNAME eq 'MSWin32' ) || ( $OSNAME eq 'cygwin' ) ) { } elsif ( $EFFECTIVE_USER_ID == 0 ) { # see RT#131304 my $current = $ENV{HOME}; my $correct = ( getpwuid $EFFECTIVE_USER_ID )[7]; if ( $current eq $correct ) { } else { $ENV{HOME} = $correct; warn "Running as root. Resetting HOME environment variable from $current to $ENV{HOME}\n"; } if ( exists $ENV{XAUTHORITY} ) { # see GH#1 delete $ENV{XAUTHORITY}; warn "Running as root. Deleting the XAUTHORITY environment variable\n"; } } my @possibles = qw(firefox waterfox basilisk); my $dev_null = File::Spec->devnull(); sub last_desperate_search { if ( ( $OSNAME eq 'MSWin32' ) ) { foreach my $possible (@possibles) { warn "Checking for $possible in Path:" . `"$possible.exe" -version 2>$dev_null` . "\n"; } } elsif ( ( $OSNAME eq 'darwin' ) or ( $OSNAME eq 'cygwin' ) ) { foreach my $possible (@possibles) { warn "Checking for $possible in PATH:" . `$possible -version $dev_null` . "\n"; } } my $glob_path = '/usr/share/applications/firefox*.desktop'; foreach my $path ( glob $glob_path ) { warn `$EXECUTABLE_NAME -nle 'print "\$ARGV:\$_" if (/(Exec|^\\[)/);' $path`; } return; } my $binary; # = 'firefox'; my $suffix = ( ( $OSNAME eq 'MSWin32' ) || ( $OSNAME eq 'cygwin' ) ) ? '.exe' : q[]; my %known_win32_preferred_names = ( 'Mozilla Firefox' => 1, 'Mozilla Firefox ESR' => 2, 'Firefox Developer Edition' => 3, Nightly => 4, 'Waterfox' => 5, 'Waterfox Current' => 6, 'Waterfox Classic' => 7, Basilisk => 8, 'Pale Moon' => 9, ); my %_known_win32_organisations = ( 'Mozilla Firefox' => 'Mozilla', 'Mozilla Firefox ESR' => 'Mozilla', 'Firefox Developer Edition' => 'Mozilla', Nightly => 'Mozilla', 'Waterfox' => 'Waterfox', 'Waterfox Current' => 'Waterfox', 'Waterfox Classic' => 'Waterfox', Basilisk => 'Mozilla', 'Pale Moon' => 'Mozilla', ); my $version_regex = qr/(\d+)[.](\d+(?:\w\d+)?)(?:[.](\d+))?/smx; my $version_string; if ( $OSNAME eq 'MSWin32' ) { NAME: foreach my $name ( sort { $known_win32_preferred_names{$a} <=> $known_win32_preferred_names{$b} } keys %known_win32_preferred_names ) { ROOT_SUBKEY: foreach my $root_subkey ( ['SOFTWARE'], [ 'SOFTWARE', 'WOW6432Node' ] ) { my $organisation = $_known_win32_organisations{$name}; my ($version) = _win32_registry_query_key( subkey => [ 'HKLM', @{$root_subkey}, $organisation, $name ], name => 'CurrentVersion' ); if ( !defined $version ) { next ROOT_SUBKEY; } my ($initial_version) = _win32_registry_query_key( subkey => [ 'HKLM', @{$root_subkey}, $organisation, $name ], name => q[] # (Default) value ); my $name_for_path_to_exe = $name; $name_for_path_to_exe =~ s/[ ]ESR//smx; my ($path) = _win32_registry_query_key( subkey => [ 'HKLM', @{$root_subkey}, $organisation, $name_for_path_to_exe, $version, 'Main' ], name => 'PathToExe' ); if ( ( defined $path ) && ( -e $path ) ) { $binary = $path; last NAME; } } } } elsif ( $OSNAME eq 'darwin' ) { foreach my $path ( '/Applications/Firefox.app/Contents/MacOS/firefox', '/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox', '/Applications/Firefox Nightly.app/Contents/MacOS/firefox', '/Applications/Waterfox Current.app/Contents/MacOS/waterfox', ) { if ( -e $path ) { $binary = $path; } } } elsif ( $OSNAME eq 'cygwin' ) { NAME: foreach my $name ( sort { $known_win32_preferred_names{$a} <=> $known_win32_preferred_names{$b} } keys %known_win32_preferred_names ) { ROOT_SUBKEY: foreach my $root_subkey (qw(SOFTWARE SOFTWARE/WOW6432Node)) { my $organisation = $_known_win32_organisations{$name}; my $version = _cygwin_reg_query_value( '/proc/registry/HKEY_LOCAL_MACHINE/' . $root_subkey . q[/] . $organisation . q[/] . $name . '/CurrentVersion' ); if ( !defined $version ) { next ROOT_SUBKEY; } my $initial_version = _cygwin_reg_query_value( '/proc/registry/HKEY_LOCAL_MACHINE/' . $root_subkey . q[/] . $organisation . q[/] . $name . q[/@] ); # (Default) value my $name_for_path_to_exe = $name; $name_for_path_to_exe =~ s/[ ]ESR//smx; my $path = _cygwin_reg_query_value( '/proc/registry/HKEY_LOCAL_MACHINE/' . $root_subkey . q[/] . $organisation . q[/] . $name_for_path_to_exe . q[/] . $version . '/Main/PathToExe' ); if ( ( defined $path ) && ( -e $path ) && ( $initial_version =~ /^$version_regex$/smx ) ) { $binary = `cygpath -s -m "$path"`; $version_string = "$name $initial_version"; $version_string =~ s/[ ]ESR//smx; last NAME; } } } } if ( !defined $binary ) { POSSIBLE: foreach my $possible (@possibles) { foreach my $path ( split /$Config::Config{path_sep}/smx, defined $ENV{PATH} ? $ENV{PATH} : $ENV{Path} ) { if ( -e "$path/$possible$suffix" ) { $binary = "$path/$possible"; last POSSIBLE; } } } } if ( ( !defined $version_string ) && ( defined $binary ) ) { my $ini_path = $binary; $ini_path =~ s/(firefox|waterfox)(?:[.]exe)?$/application.ini/smx; if ( open my $ini_handle, '<', $ini_path ) { my $vendor; my $name; while ( my $line = <$ini_handle> ) { chomp $line; if ( $line =~ /^Vendor=(.*)$/smx ) { ($vendor) = ($1); } elsif ( $line =~ /^Name=(.*)$/smx ) { ($name) = ($1); } elsif ( $line =~ /^Version=($version_regex)$/smx ) { $version_string = "$vendor $name $1\n"; warn "Determining version string from $ini_path\n"; } } } } if ( ( !defined $version_string ) && ( defined $binary ) ) { $version_string = `"$binary" -version 2>$dev_null`; } my $major; if ($version_string) { warn $version_string; if ( $version_string =~ /^(?:Mozilla[ ]Firefox|Firefox[ ]Developer[ ]Edition|Nightly)[ ](\d+)[.]\d+(?:a\d+)?([.]\d+)?\s*/smx ) { ($major) = ($1); if ( $major < 31 ) { last_desperate_search(); warn "Please install a more recent version of Mozilla Firefox. Current version is $major\n"; } } elsif ( $version_string =~ /^Waterfox[ ]/smx ) { } elsif ( $version_string =~ /^Moonchild[ ]/smx ) { } else { last_desperate_search(); die "Unable to parse $version_string\n"; } } else { last_desperate_search(); die "Mozilla Firefox cannot be discovered in $ENV{PATH}\n"; } if ( $OSNAME eq 'linux' ) { local $ENV{PATH} = '/usr/bin:/bin:/usr/sbin:/sbin'; warn "grep -r Mem /proc/meminfo\n"; warn `grep -r Mem /proc/meminfo`; warn "ulimit -a | grep -i mem\n"; warn `ulimit -a | grep -i mem`; } elsif ( $OSNAME =~ /bsd/smxi ) { local $ENV{PATH} = '/usr/bin:/bin:/usr/sbin:/sbin'; warn "sysctl hw | egrep 'hw.(phys|user|real)'\n"; warn `sysctl hw | egrep 'hw.(phys|user|real)'`; warn "ulimit -a | grep -i mem\n"; warn `ulimit -a | grep -i mem`; } if ( ( $OSNAME eq 'linux' ) || ( $OSNAME eq 'freebsd' ) ) { local $ENV{PATH} = '/usr/bin:/bin:/usr/sbin:/sbin'; my $virtual_memory = `ulimit -v 2>/dev/null`; if ( $CHILD_ERROR == 0 ) { chomp $virtual_memory; if ( $virtual_memory eq 'unlimited' ) { } elsif ( ( $OSNAME eq 'freebsd' ) && ( $virtual_memory < 1_800_000 ) ) { die "$virtual_memory bytes of virtual memory is less than the required 1.8Gb to run test suite in $OSNAME\n"; } elsif ( $virtual_memory < 2_400_000 ) { die "$virtual_memory bytes of virtual memory is less than the required 2.4Gb to run test suite in $OSNAME\n"; } } } if ( ( $OSNAME eq 'MSWin32' ) || ( $OSNAME eq 'darwin' ) || ( $OSNAME eq 'cygwin' ) ) { } elsif ( $ENV{DISPLAY} ) { } elsif ( $major > 55 ) { # -headless is supported } else { `Xvfb -help 2>/dev/null >/dev/null`; if ( $CHILD_ERROR != 0 ) { die "Unable to run tests when not in an X11 environment and Xvfb is not available. Please install Xvfb\n"; } } WriteMakefile( NAME => 'Firefox::Marionette', AUTHOR => 'David Dick ', VERSION_FROM => 'lib/Firefox/Marionette.pm', ABSTRACT_FROM => 'lib/Firefox/Marionette.pm', ( $ExtUtils::MakeMaker::VERSION >= 6.3002 ? ( 'LICENSE' => 'perl' ) : () ), ( $ExtUtils::MakeMaker::VERSION >= 6.48 ? ( 'MIN_PERL_VERSION' => '5.006' ) : () ), META_MERGE => { 'meta-spec' => { version => 2 }, resources => { repository => { url => 'https://github.com/david-dick/firefox-marionette', web => 'https://github.com/david-dick/firefox-marionette', type => 'git', }, bugtracker => { web => 'https://github.com/david-dick/firefox-marionette/issues' }, }, }, PL_FILES => {}, EXE_FILES => [ 'ssh-auth-cmd-marionette', 'ca-bundle-for-firefox', 'check-firefox-certificate-authorities', 'firefox-passwords' ], BUILD_REQUIRES => { 'Compress::Zlib' => 0, 'Cwd' => 0, 'Digest::SHA' => 0, 'File::HomeDir' => 0, 'HTTP::Daemon' => 0, 'HTTP::Response' => 0, 'HTTP::Status' => 0, 'IO::Socket::SSL' => 0, $] ge '5.010' ? ( 'PDF::API2' => 2.036 ) : (), 'Test::More' => 0, }, PREREQ_PM => { 'Archive::Zip' => 0, 'base' => 0, 'Config' => 0, 'Config::INI::Reader' => 0, 'DirHandle' => 0, 'Encode' => 0, 'English' => 0, 'Exporter' => 0, 'Fcntl' => 0, 'FileHandle' => 0, 'File::Find' => 0, 'File::Spec' => 0, 'File::Temp' => 0, 'IPC::Open3' => 1.03, 'JSON' => 0, 'MIME::Base64' => 0, 'overload' => 0, 'Pod::Simple::Text' => 0, 'POSIX' => 0, 'Scalar::Util' => 0, 'Socket' => 0, 'Text::CSV_XS' => 0, 'Term::ReadKey' => 0, 'Time::HiRes' => 0, 'URI' => 0, 'URI::Escape' => 0, 'URI::URL' => 0, ( $OSNAME eq 'MSWin32' ? ( 'Win32' => 0, 'Win32::Process' => 0, 'Win32API::Registry' => 0, ) : () ), 'XML::Parser' => 0, ( ( $OSNAME ne 'MSWin32' and $OSNAME ne 'darwin' and $OSNAME ne 'cygwin' ) ? ( 'Crypt::URandom' => 0, ) : () ), }, dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, clean => { FILES => 'Firefox-Marionette-*' }, ); Firefox-Marionette-1.22/firefox-passwords0000755000175000017500000004246114175143706017217 0ustar davedave#! /usr/bin/perl use strict; use warnings; use Getopt::Long(); use English qw( -no_match_vars ); use Firefox::Marionette(); use Firefox::Marionette::Profile(); use Text::CSV_XS(); use File::Temp(); use FileHandle(); use POSIX(); use Term::ReadKey(); use charnames ':full'; our $VERSION = '1.22'; sub _NUMBER_OF_BYTES_FOR_ZIP_MAGIC_NUMBER { return 4 } MAIN: { my %options; Getopt::Long::GetOptions( \%options, 'help', 'version', 'binary:s', 'import:s', 'export:s', 'only-host-regex:s', 'only-user:s', 'visible', 'debug', 'console', 'profile-name:s', 'password', 'list-profile-names', 'check-only', ); my %parameters = _check_options(%options); my @logins; if ( defined $options{'list-profile-names'} ) { foreach my $name ( Firefox::Marionette::Profile->names() ) { print "$name\n" or die "Failed to print to STDOUT:$EXTENDED_OS_ERROR\n"; } exit 0; } elsif ( defined $options{import} ) { @logins = _handle_import(%options); if ( $options{'check-only'} ) { exit 0; } } elsif ( !$options{export} ) { $options{export} = q[]; } my $firefox = Firefox::Marionette->new(%parameters); if ( $firefox->pwd_mgr_needs_login() ) { my $prompt = 'Firefox requires the primary password to unlock Password Manager from ' . ( $parameters{profile_copied_from} || $parameters{profile_name} ) . q[: ]; print "$prompt" or die "Failed to print to STDOUT:$EXTENDED_OS_ERROR\n"; Term::ReadKey::ReadMode(2); # noecho my $password; my $key = q[]; while ( $key ne "\n" ) { $password .= $key; $key = Term::ReadKey::ReadKey(0); } Term::ReadKey::ReadMode(0); # restore print "\n" or die "Failed to print to STDOUT:$EXTENDED_OS_ERROR\n"; eval { $firefox->pwd_mgr_login($password); } or do { chomp $EVAL_ERROR; die "$EVAL_ERROR\n"; }; } if (@logins) { _add_or_modify_logins( $firefox, @logins ); } if ( defined $options{export} ) { my $export_handle; if ( $options{export} ) { open $export_handle, '>', $options{export} or die "Failed to open $options{export}:$EXTENDED_OS_ERROR\n"; } else { $options{export} = 'STDOUT'; $export_handle = *{STDOUT}; } _export_logins( $firefox, $export_handle, %options ); if ( $options{export} ne 'STDOUT' ) { close $export_handle or die "Failed to close $options{export}:$EXTENDED_OS_ERROR\n"; } } $firefox->quit(); } sub _add_or_modify_logins { my ( $firefox, @logins ) = @_; my %form_auth_exists; my %http_auth_exists; foreach my $existing ( $firefox->logins() ) { if ( $existing->realm() ) { $http_auth_exists{ $existing->host() }{ $existing->realm() } { $existing->user() } = $existing; } else { $form_auth_exists{ $existing->host() }{ $existing->user() } = $existing; } } foreach my $login (@logins) { if ( $login->realm() ) { if ( my $existing = $http_auth_exists{ $login->host() }{ $login->realm() } { $login->user() } ) { $firefox->delete_login($existing); } } else { if ( my $existing = $form_auth_exists{ $login->host() }{ $login->user() } ) { $firefox->delete_login($existing); } } $firefox->add_login($login); } return; } sub _read_stdin_into_temp_file { my $import_handle = File::Temp::tempfile( File::Spec->catfile( File::Spec->tmpdir(), 'firefox_password_import_stdin_XXXXXXXXXXX' ) ) or die "Failed to open temporary file for writing:$EXTENDED_OS_ERROR\n"; while ( my $line = <> ) { $import_handle->print($line) or die "Failed to write to temporary file:$EXTENDED_OS_ERROR\n"; } seek $import_handle, Fcntl::SEEK_SET(), 0 or die "Failed to seek to start of file:$EXTENDED_OS_ERROR\n"; return $import_handle; } sub _handle_import { my (%options) = @_; my @logins; my $import_handle; if ( $options{import} ) { open $import_handle, '<', $options{import} or die "Failed to open '$options{import}':$EXTENDED_OS_ERROR\n"; } else { $options{import} = 'STDIN'; $import_handle = _read_stdin_into_temp_file(); } @logins = _read_logins($import_handle); if ( $options{import} ne 'STDIN' ) { close $import_handle or die "Failed to close '$options{import}':$EXTENDED_OS_ERROR\n"; } return @logins; } sub _read_logins { my ($import_handle) = @_; sysread $import_handle, my $magic_number, _NUMBER_OF_BYTES_FOR_ZIP_MAGIC_NUMBER() or die "Failed to read from file:$EXTENDED_OS_ERROR\n"; sysseek $import_handle, Fcntl::SEEK_SET(), 0 or die "Failed to seek to start of file:$EXTENDED_OS_ERROR\n"; foreach my $zip_magic_number ( "PK\N{END OF TEXT}\N{END OF TRANSMISSION}", "PK\N{ENQUIRY}\N{ACKNOWLEDGE}", "PK\N{ALERT}\N{BACKSPACE}" ) { if ( $magic_number eq $zip_magic_number ) { return Firefox::Marionette->logins_from_zip($import_handle); } } return Firefox::Marionette->logins_from_csv($import_handle); } sub _export_logins { my ( $firefox, $export_handle, %options ) = @_; binmode $export_handle, ':encoding(utf8)'; my $csv = Text::CSV_XS->new( { binary => 1, auto_diag => 1, always_quote => 1 } ); my $headers = [ qw(url username password httpRealm formActionOrigin guid timeCreated timeLastUsed timePasswordChanged) ]; my @passwords; my $count = 0; foreach my $login ( $firefox->logins() ) { if ( ( $options{'only-user'} ) && ( $login->user() ne $options{'only-user'} ) ) { next; } if ( ( $options{'only-host-regex'} ) && ( $login->host() !~ /$options{'only-host-regex'}/smx ) ) { next; } if ( $options{password} ) { push @passwords, $login->password(); } else { if ( $count == 0 ) { $csv->say( $export_handle, $headers ); } my $row = [ $login->host(), $login->user(), $login->password(), $login->realm(), ( defined $login->origin() ? $login->origin() : ( defined $login->realm() ? undef : q[] ) ), $login->guid(), $login->creation_in_ms(), $login->last_used_in_ms(), $login->password_changed_in_ms() ]; $csv->say( $export_handle, $row ); } $count += 1; } if ( $options{password} ) { my %different_passwords; foreach my $password (@passwords) { $different_passwords{$password} = 1; } if ( ( scalar keys %different_passwords ) == 1 ) { print {$export_handle} "$passwords[0]\n" or die "Failed to write password:$EXTENDED_OS_ERROR\n"; } else { die "More than one password could be selected. Use --only-host-regex and --only-user to restrict the password selection\n"; } } return; } sub _check_options { my (%options) = @_; if ( $options{help} ) { require Pod::Simple::Text; my $parser = Pod::Simple::Text->new(); $parser->parse_from_file($PROGRAM_NAME); exit 0; } elsif ( $options{version} ) { print "$VERSION\n" or die "Failed to print to STDOUT:$EXTENDED_OS_ERROR\n"; exit 0; } my %parameters = ( logins => {} ); foreach my $key (qw(visible debug console)) { if ( $options{$key} ) { $parameters{$key} = 1; } } if ( $options{binary} ) { $parameters{binary} = $options{binary}; } if ( $options{'profile-name'} ) { $parameters{profile_name} = $options{'profile-name'}; } elsif ( !defined $options{import} ) { my $profile_name = Firefox::Marionette::Profile->default_name(); $parameters{profile_copied_from} = $profile_name; my $directory = Firefox::Marionette::Profile->directory($profile_name); foreach my $name (qw(key3.db key4.db logins.json)) { my $path = File::Spec->catfile( $directory, $name ); if ( my $handle = FileHandle->new( $path, Fcntl::O_RDONLY() ) ) { push @{ $parameters{import_profile_paths} }, $path; } elsif ( $OS_ERROR == POSIX::ENOENT() ) { } else { warn "Skipping $path:$EXTENDED_OS_ERROR\n"; } } } return %parameters; } __END__ =head1 NAME firefox-passwords - import and export passwords from firefox =head1 VERSION Version 1.22 =head1 USAGE $ firefox-passwords >logins.csv # export from the default profile $ firefox-passwords --export logins.csv # same thing but exporting directly to the file $ firefox-passwords --list-profile-names # print out the available profile names $ firefox-passwords --profile new --import logins.csv # imports logins from logins.csv into the new profile $ firefox-passwords --profile new --import logins.csv --check-only # exit with a zero if the import file can be recognised. Do not import $ firefox-passwords --export | firefox --import --profile-name new # export from the default profile into the new profile $ firefox-passwords --export --only-host-regex "(pause|github)" # export logins with a host matching qr/(pause|github)/smx from the default profile $ firefox-passwords --export --only-user "me@example.org" # export logins with user "me@example.org" from the default profile $ firefox-passwords --only-user "me@example.org" --password # just print password for the me@example.org (assuming there is only one password) =head1 DESCRIPTION This program is intended to import and export passwords from firefox. It uses the L and the L to access the L. This has been tested to work with Firefox 24 and above and has been designed to work with L =head1 REQUIRED ARGUMENTS Either --export, --import or --list-profile-names must be specified. If none of these is specified, --export is the assumed default =head1 OPTIONS Option names can be abbreviated to uniqueness and can be stated with singe or double dashes, and option values can be separated from the option name by a space or '=' (as with Getopt::Long). Option names are also case- sensitive. =over 4 =item * --help - This page. =item * --version - Print the current version of this binary to STDOUT. =item * --binary - Use this firefox binary instead of the default firefox instance =item * --export - export passwords to STDOUT or the file name specified. =item * --import - import passwords from STDIN or the file name specified. =item * --check-only - when combined with --import, exit with a zero (0) exit code if the import file can be processed =item * --list-profile-name - print out the available profile names =item * --profile-name - specify the name of the profile to work with. =item * --visible - allow firefox to be visible while exporting or importing logins =item * --debug - turn on debug to show binary execution and network traffic during exporting or importing logins =item * --console - make the browser javascript console appear during exporting or importing logins =item * --only-host-regex - restrict the export of logins to those that have a hostname matching the supplied regex. =item * --only-user - restrict the export of logins to those that have a user exactly matching the value. =item * --password - when exporting only print the password, and only print the password if all passwords in the export match =back =head1 AUTOMATIC AND MANUAL PROFILE SELECTION firefox-passwords will automatically work with the default L. You can select other profiles with the --profile-name option =head1 PRIMARY PASSWORDS firefox-passwords will request the L if required when importing or exporting from the L. =head1 EXPORTING AND IMPORTING TO GOOGLE CHROME OR MICROSOFT EDGE firefox-passwords will natively read and write login csv files for Google Chrome and Microsoft Edge. =head1 PASSWORD IMPORT/EXPORT FORMAT firefox-passwords will export data in CSV with the following column headers "url","username","password","httpRealm","formActionOrigin","guid","timeCreated","timeLastUsed","timePasswordChanged" firefox-passwords will import data in CSV. It will recognise different formats for importing passwords, including the export format and the others listed below. Plrease let me know if other formats would be useful. =head1 PASSWORD IMPORTING FROM BITWARDEN firefox-passwords will also accept input data in L, which includes the following column headers; ...,"login_uri","login_username","login_password",... =head1 PASSWORD IMPORTING FROM LASTPASS firefox-passwords will also accept input data in L, which includes the following column headers; url,username,password,totp,extra,name,grouping,fav The LastPass CSV export also can include an unusual "url" value of "http://sn" for server logins, database logins, etc. All logins with a "url" value of "http://sn" AND an "extra" value matching the regular expression /^NoteType:/ will be skipped (as there is no use for these types of login records in firefox. =head1 PASSWORD IMPORTING FROM KEEPASS firefox-passwords will also accept input data in L, which includes the following column headers; ...,"Login Name","Password","Web Site",... =head1 PASSWORD IMPORTING FROM 1PASSWORD firefox-passwords will also accept the L<1Password Unencrypted Export format|https://support.1password.com/1pux-format/> =head1 CONFIGURATION firefox-passwords requires no configuration files or environment variables. =head1 DEPENDENCIES firefox-passwords requires the following non-core Perl modules =over =item * L L =back =head1 DIAGNOSTICS None. =head1 INCOMPATIBILITIES None known. =head1 EXIT STATUS This program will exit with a zero after successfully completing. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR David Dick C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2021, David Dick C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. Firefox-Marionette-1.22/META.yml0000664000175000017500000000257614175144502015055 0ustar davedave--- abstract: 'Automate the Firefox browser with the Marionette protocol' author: - 'David Dick ' build_requires: Compress::Zlib: '0' Cwd: '0' Digest::SHA: '0' File::HomeDir: '0' HTTP::Daemon: '0' HTTP::Response: '0' HTTP::Status: '0' IO::Socket::SSL: '0' PDF::API2: '2.036' Test::More: '0' configure_requires: ExtUtils::MakeMaker: '0' dynamic_config: 1 generated_by: 'ExtUtils::MakeMaker version 7.64, CPAN::Meta::Converter version 2.150010' license: perl meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html version: '1.4' name: Firefox-Marionette no_index: directory: - t - inc requires: Archive::Zip: '0' Config: '0' Config::INI::Reader: '0' Crypt::URandom: '0' DirHandle: '0' Encode: '0' English: '0' Exporter: '0' Fcntl: '0' File::Find: '0' File::Spec: '0' File::Temp: '0' FileHandle: '0' IPC::Open3: '1.03' JSON: '0' MIME::Base64: '0' POSIX: '0' Pod::Simple::Text: '0' Scalar::Util: '0' Socket: '0' Term::ReadKey: '0' Text::CSV_XS: '0' Time::HiRes: '0' URI: '0' URI::Escape: '0' URI::URL: '0' XML::Parser: '0' base: '0' overload: '0' perl: '5.006' resources: bugtracker: https://github.com/david-dick/firefox-marionette/issues repository: https://github.com/david-dick/firefox-marionette version: '1.22' x_serialization_backend: 'CPAN::Meta::YAML version 0.018'